001/* 002 * Licensed to the Apache Software Foundation (ASF) under one or more 003 * contributor license agreements. See the NOTICE file distributed with 004 * this work for additional information regarding copyright ownership. 005 * The ASF licenses this file to You under the Apache License, Version 2.0 006 * (the "License"); you may not use this file except in compliance with 007 * the License. You may obtain a copy of the License at 008 * 009 * http://www.apache.org/licenses/LICENSE-2.0 010 * 011 * Unless required by applicable law or agreed to in writing, software 012 * distributed under the License is distributed on an "AS IS" BASIS, 013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 014 * See the License for the specific language governing permissions and 015 * limitations under the License. 016 */ 017package org.apache.wicket.pageStore; 018 019import java.io.IOException; 020import java.io.ObjectOutputStream; 021import java.io.Serializable; 022import java.util.Iterator; 023import java.util.LinkedList; 024import java.util.List; 025import java.util.function.Supplier; 026 027import javax.servlet.http.HttpSession; 028 029import org.apache.wicket.MetaDataKey; 030import org.apache.wicket.Session; 031import org.apache.wicket.WicketRuntimeException; 032import org.apache.wicket.page.IManageablePage; 033import org.apache.wicket.serialize.ISerializer; 034import org.apache.wicket.util.lang.Args; 035import org.apache.wicket.util.lang.Bytes; 036import org.apache.wicket.util.lang.Classes; 037 038/** 039 * A store keeping a configurable maximum of pages in the session. 040 * <p> 041 * Note: see {@link #getKey()} for using more than once instance in an application 042 */ 043public class InSessionPageStore implements IPageStore 044{ 045 046 private static final MetaDataKey<SessionData> KEY = new MetaDataKey<>() 047 { 048 private static final long serialVersionUID = 1L; 049 }; 050 051 private final ISerializer serializer; 052 053 private final Supplier<SessionData> dataCreator; 054 055 /** 056 * Keep {@code maxPages} persistent in each session. 057 * <p> 058 * Any page added to this store <em>not</em> being a {@code SerializedPage} will be dropped 059 * on serialization of the session. 060 * 061 * @param maxPages 062 * maximum pages to keep in session 063 */ 064 public InSessionPageStore(int maxPages) 065 { 066 this(null, () -> new CountLimitedData(maxPages)); 067 } 068 069 /** 070 * Keep page up to {@code maxBytes} persistent in each session. 071 * <p> 072 * All pages added to this store <em>must</em> be {@code SerializedPage}s. You can achieve this 073 * by letting a {@link SerializingPageStore} delegate to this store. 074 * 075 * @param maxBytes 076 * maximum bytes to keep in session 077 */ 078 public InSessionPageStore(Bytes maxBytes) 079 { 080 this(null, () -> new SizeLimitedData(maxBytes)); 081 } 082 083 /** 084 * Keep a cache of {@code maxPages} in each session. 085 * <p> 086 * If the container serializes sessions to disk, any non-{@code SerializedPage} added to this 087 * store will be automatically serialized. 088 * 089 * @param maxPages 090 * maximum pages to keep in session 091 * @param serializer 092 * optional serializer used only in case session serialization 093 */ 094 public InSessionPageStore(int maxPages, ISerializer serializer) 095 { 096 this(serializer, () -> new CountLimitedData(maxPages)); 097 } 098 099 private InSessionPageStore(ISerializer serializer, Supplier<SessionData> dataCreator) 100 { 101 this.serializer = serializer; 102 103 this.dataCreator = dataCreator; 104 } 105 106 @Override 107 public IManageablePage getPage(IPageContext context, int id) 108 { 109 SessionData data = getSessionData(context, false); 110 if (data != null) 111 { 112 IManageablePage page = data.get(id); 113 if (page != null) 114 { 115 return page; 116 } 117 } 118 119 return null; 120 } 121 122 @Override 123 public void addPage(IPageContext context, IManageablePage page) 124 { 125 SessionData data = getSessionData(context, true); 126 127 data.add(page); 128 } 129 130 @Override 131 public void removePage(IPageContext context, IManageablePage page) 132 { 133 SessionData data = getSessionData(context, false); 134 if (data != null) 135 { 136 data.remove(page.getPageId()); 137 } 138 } 139 140 @Override 141 public void removeAllPages(IPageContext context) 142 { 143 SessionData data = getSessionData(context, false); 144 if (data != null) 145 { 146 data.removeAll(); 147 } 148 } 149 150 private SessionData getSessionData(IPageContext context, boolean create) 151 { 152 SessionData data = context.getSessionData(getKey(), create ? () -> { 153 return dataCreator.get(); 154 } : null); 155 156 if (data != null && serializer != null) 157 { 158 // data might be deserialized so initialize again 159 data.supportSessionSerialization(serializer); 160 } 161 162 return data; 163 } 164 165 /** 166 * Session data is stored under a {@link MetaDataKey}. 167 * <p> 168 * In the unlikely case that an application utilizes more than one instance of this store, 169 * this method has to be overridden to provide a separate key for each instance. 170 */ 171 protected MetaDataKey<SessionData> getKey() 172 { 173 return KEY; 174 } 175 176 /** 177 * Data kept in the {@link Session}, might get serialized along with its containing 178 * {@link HttpSession}. 179 */ 180 protected abstract static class SessionData implements Serializable 181 { 182 183 transient ISerializer serializer; 184 185 /** 186 * Pages, may partly be serialized. 187 * <p> 188 * Kept in list instead of map, since non-serialized pages might change their id during a 189 * request. 190 */ 191 List<IManageablePage> pages = new LinkedList<>(); 192 193 /** 194 * Call this method if session serialization should be supported, i.e. all pages get 195 * serialized along with the session. 196 */ 197 public void supportSessionSerialization(ISerializer serializer) 198 { 199 this.serializer = Args.notNull(serializer, "serializer"); 200 } 201 202 public synchronized void add(IManageablePage page) 203 { 204 // move to end 205 remove(page.getPageId()); 206 207 pages.add(page); 208 } 209 210 protected synchronized void removeOldest() 211 { 212 IManageablePage page = pages.get(0); 213 214 remove(page.getPageId()); 215 } 216 217 public synchronized IManageablePage remove(int pageId) 218 { 219 Iterator<IManageablePage> iterator = pages.iterator(); 220 while (iterator.hasNext()) 221 { 222 IManageablePage page = iterator.next(); 223 224 if (page.getPageId() == pageId) 225 { 226 iterator.remove(); 227 return page; 228 } 229 } 230 return null; 231 } 232 233 public synchronized void removeAll() 234 { 235 pages.clear(); 236 } 237 238 public synchronized IManageablePage get(int id) 239 { 240 for (int p = 0; p < pages.size(); p++) 241 { 242 IManageablePage candidate = pages.get(p); 243 244 if (candidate.getPageId() == id) 245 { 246 if (candidate instanceof SerializedPage && serializer != null) 247 { 248 candidate = (IManageablePage)serializer 249 .deserialize(((SerializedPage)candidate).getData()); 250 251 pages.set(p, candidate); 252 } 253 254 return candidate; 255 } 256 } 257 258 return null; 259 } 260 261 /** 262 * Serialize pages before writing to output. 263 */ 264 private synchronized void writeObject(final ObjectOutputStream output) throws IOException 265 { 266 // handle non-serialized pages 267 for (int p = 0; p < pages.size(); p++) 268 { 269 IManageablePage page = pages.get(p); 270 271 if ((page instanceof SerializedPage) == false) 272 { 273 // remove if not already serialized 274 pages.remove(p); 275 276 if (serializer == null) 277 { 278 // cannot be serialized, thus skip 279 p--; 280 } 281 else 282 { 283 // serialize first 284 byte[] bytes = serializer.serialize(page); 285 SerializedPage serializedPage = new SerializedPage(page.getPageId(), Classes.name(page.getClass()), bytes); 286 287 // and then re-add (to prevent a serialization loop, 288 // in case the page holds a reference to the session) 289 pages.add(p, serializedPage); 290 } 291 } 292 } 293 294 output.defaultWriteObject(); 295 } 296 } 297 298 /** 299 * Limit pages by count. 300 */ 301 static class CountLimitedData extends SessionData 302 { 303 private final int maxPages; 304 305 public CountLimitedData(int maxPages) 306 { 307 this.maxPages = Args.withinRange(1, Integer.MAX_VALUE, maxPages, "maxPages"); 308 } 309 310 @Override 311 public synchronized void add(IManageablePage page) 312 { 313 super.add(page); 314 315 while (pages.size() > maxPages) 316 { 317 removeOldest(); 318 } 319 } 320 } 321 322 /** 323 * Limit pages by size. 324 */ 325 static class SizeLimitedData extends SessionData 326 { 327 private final Bytes maxBytes; 328 329 private long size; 330 331 public SizeLimitedData(Bytes maxBytes) 332 { 333 Args.notNull(maxBytes, "maxBytes"); 334 335 this.maxBytes = Args.withinRange(Bytes.bytes(1), Bytes.MAX, maxBytes, "maxBytes"); 336 } 337 338 @Override 339 public synchronized void add(IManageablePage page) 340 { 341 if (page instanceof SerializedPage == false) 342 { 343 throw new WicketRuntimeException( 344 "InSessionPageStore limited by size works with serialized pages only"); 345 } 346 347 super.add(page); 348 349 size += ((SerializedPage)page).getData().length; 350 351 while (size > maxBytes.bytes()) 352 { 353 removeOldest(); 354 } 355 } 356 357 @Override 358 public synchronized IManageablePage remove(int pageId) 359 { 360 SerializedPage page = (SerializedPage)super.remove(pageId); 361 if (page != null) 362 { 363 size -= page.getData().length; 364 } 365 366 return page; 367 } 368 369 @Override 370 public synchronized void removeAll() 371 { 372 super.removeAll(); 373 374 size = 0; 375 } 376 } 377 378 @Override 379 public boolean supportsVersioning() 380 { 381 return false; 382 } 383}