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}