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.util.ArrayList;
020import java.util.Iterator;
021import java.util.LinkedList;
022import java.util.List;
023import java.util.Map;
024import java.util.Set;
025import java.util.concurrent.ConcurrentHashMap;
026import java.util.function.Supplier;
027import java.util.stream.Collectors;
028import java.util.stream.StreamSupport;
029
030import org.apache.wicket.Application;
031import org.apache.wicket.WicketRuntimeException;
032import org.apache.wicket.core.util.lang.WicketObjects;
033import org.apache.wicket.page.IManageablePage;
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 storage of pages in memory.
040 */
041public class InMemoryPageStore extends AbstractPersistentPageStore implements IPersistentPageStore
042{
043
044        private final Map<String, IMemoryData> datas;
045
046        private final Supplier<IMemoryData> dataCreator;
047
048        /**
049         * Keep {@code maxPages} for each session.
050         * 
051         * @param applicationName
052         *            {@link Application#getName()}
053         * @param maxPages
054         *            max pages per session
055         */
056        public InMemoryPageStore(String applicationName, int maxPages)
057        {
058                this(applicationName, () -> new CountLimitedData(maxPages), new ConcurrentHashMap<>());
059        }
060
061        /**
062         * Keep page up to {@code maxBytes} for each session.
063         * <p>
064         * All pages added to this store <em>must</em> be {@code SerializedPage}s. You can achieve this
065         * by letting a {@link SerializingPageStore} delegate to this store.
066         * 
067         * @param applicationName
068         *            {@link Application#getName()}
069         * @param maxBytes
070         *            maximum bytes to keep in session
071         */
072        public InMemoryPageStore(String applicationName, Bytes maxBytes)
073        {
074                this(applicationName, () -> new SizeLimitedData(maxBytes), new ConcurrentHashMap<>());
075        }
076
077        /**
078         * @param applicationName
079         *            {@link Application#getName()}
080         * @param dataCreator
081         *            creator of new data
082         * @param datas
083         *            storage for datas
084         */
085        protected InMemoryPageStore(String applicationName, Supplier<IMemoryData> dataCreator,
086                Map<String, IMemoryData> datas)
087        {
088                super(applicationName);
089
090                this.dataCreator = dataCreator;
091
092                this.datas = datas;
093        }
094
095        /**
096         * Versioning is not supported.
097         */
098        @Override
099        public boolean supportsVersioning()
100        {
101                return false;
102        }
103
104        @Override
105        protected IManageablePage getPersistedPage(String sessionIdentifier, int id)
106        {
107                IMemoryData data = getMemoryData(sessionIdentifier, false);
108                if (data != null)
109                {
110                        return data.get(id);
111                }
112
113                return null;
114        }
115
116        @Override
117        protected void removePersistedPage(String sessionIdentifier, IManageablePage page)
118        {
119                IMemoryData data = getMemoryData(sessionIdentifier, false);
120                if (data != null)
121                {
122                        synchronized (data)
123                        {
124                                data.remove(page.getPageId());
125                        }
126                }
127        }
128
129        @Override
130        protected void removeAllPersistedPages(String sessionIdentifier)
131        {
132                datas.remove(sessionIdentifier);
133        }
134
135        @Override
136        protected void addPersistedPage(String sessionIdentifier, IManageablePage page)
137        {
138                IMemoryData data = getMemoryData(sessionIdentifier, true);
139
140                data.add(page);
141        }
142
143        @Override
144        public Set<String> getSessionIdentifiers()
145        {
146                return datas.keySet();
147        }
148
149        @Override
150        public List<IPersistedPage> getPersistedPages(String sessionIdentifier)
151        {
152                IMemoryData data = datas.get(sessionIdentifier);
153                if (data == null)
154                {
155                        return new ArrayList<>();
156                }
157
158                synchronized (data)
159                {
160                        return StreamSupport.stream(data.spliterator(), false).map(page -> {
161                                String pageType = page instanceof SerializedPage
162                                        ? ((SerializedPage)page).getPageType()
163                                        : Classes.name(page.getClass());
164
165                                return new PersistedPage(page.getPageId(), pageType, getSize(page));
166                        }).collect(Collectors.toList());
167                }
168        }
169
170        @Override
171        public Bytes getTotalSize()
172        {
173                int size = 0;
174
175                for (IMemoryData data : datas.values())
176                {
177                        synchronized (data)
178                        {
179                                for (IManageablePage page : data)
180                                {
181                                        size += getSize(page);
182                                }
183                        }
184                }
185
186                return Bytes.bytes(size);
187        }
188
189        /**
190         * Get the size of the given page.
191         */
192        protected long getSize(IManageablePage page)
193        {
194                if (page instanceof SerializedPage)
195                {
196                        return ((SerializedPage)page).getData().length;
197                }
198                else
199                {
200                        return WicketObjects.sizeof(page);
201                }
202        }
203
204        private IMemoryData getMemoryData(String sessionIdentifier, boolean create)
205        {
206                if (!create)
207                {
208                        return datas.get(sessionIdentifier);
209                }
210
211                IMemoryData data = dataCreator.get();
212                IMemoryData existing = datas.putIfAbsent(sessionIdentifier, data);
213                return existing != null ? existing : data;
214        }
215
216        /**
217         * Pages kept in memory for a session.
218         */
219        public interface IMemoryData extends Iterable<IManageablePage>
220        {
221                /**
222                 * Remove a page.
223                 * 
224                 * @param pageId
225                 * @return
226                 */
227                IManageablePage remove(int pageId);
228
229                /**
230                 * Add a page.
231                 * 
232                 * @param page
233                 */
234                void add(IManageablePage page);
235
236                /**
237                 * Get a page.
238                 * 
239                 * @param id
240                 * @return
241                 */
242                IManageablePage get(int id);
243        }
244
245        /**
246         * List based implementation.
247         */
248        protected static class MemoryData implements IMemoryData
249        {
250                /**
251                 * Kept in list instead of map, since non-serialized pages might change their id during a
252                 * request.
253                 */
254                List<IManageablePage> pages = new LinkedList<>();
255                
256                @Override
257                public Iterator<IManageablePage> iterator()
258                {
259                        return pages.iterator();
260                }
261
262                @Override
263                public synchronized void add(IManageablePage page)
264                {
265                        remove(page.getPageId());
266
267                        pages.add(page);
268                }
269
270                @Override
271                public synchronized IManageablePage remove(int pageId)
272                {
273                        Iterator<IManageablePage> iterator = pages.iterator();
274                        while (iterator.hasNext())
275                        {
276                                IManageablePage page = iterator.next();
277
278                                if (page.getPageId() == pageId)
279                                {
280                                        iterator.remove();
281                                        return page;
282                                }
283                        }
284                        return null;
285                }
286
287                @Override
288                public synchronized IManageablePage get(int pageId)
289                {
290                        for (final IManageablePage page : pages)
291                        {
292                                if (page.getPageId() == pageId)
293                                {
294                                        return page;
295                                }
296                        }
297
298                        return null;
299                }
300
301                protected void removeOldest()
302                {
303                        IManageablePage page = pages.iterator().next();
304
305                        remove(page.getPageId());
306                }
307        }
308
309        /**
310         * Limit pages by count.
311         */
312        protected static class CountLimitedData extends MemoryData
313        {
314                private final int maxPages;
315
316                public CountLimitedData(int maxPages)
317                {
318                        this.maxPages = Args.withinRange(1, Integer.MAX_VALUE, maxPages, "maxPages");
319                }
320
321                @Override
322                public synchronized void add(IManageablePage page)
323                {
324                        super.add(page);
325
326                        while (pages.size() > maxPages)
327                        {
328                                removeOldest();
329                        }
330                }
331        }
332
333        /**
334         * Limit pages by size.
335         */
336        protected static class SizeLimitedData extends MemoryData
337        {
338                private final Bytes maxBytes;
339
340                private long size;
341
342                public SizeLimitedData(Bytes maxBytes)
343                {
344                        Args.notNull(maxBytes, "maxBytes");
345
346                        this.maxBytes = Args.withinRange(Bytes.bytes(1), Bytes.MAX, maxBytes, "maxBytes");
347                }
348
349                @Override
350                public synchronized void add(IManageablePage page)
351                {
352                        if (page instanceof SerializedPage == false)
353                        {
354                                throw new WicketRuntimeException(
355                                        "InMemoryPageStore limited by size works with serialized pages only");
356                        }
357
358                        super.add(page);
359
360                        size += ((SerializedPage)page).getData().length;
361
362                        while (size > maxBytes.bytes())
363                        {
364                                removeOldest();
365                        }
366                }
367
368                @Override
369                public synchronized IManageablePage remove(int pageId)
370                {
371                        SerializedPage page = (SerializedPage)super.remove(pageId);
372                        if (page != null)
373                        {
374                                size -= page.getData().length;
375                        }
376                        return page;
377                }
378        }
379}