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.Serializable;
020import java.util.HashMap;
021import java.util.LinkedList;
022import java.util.Map;
023import java.util.function.Supplier;
024
025import org.apache.wicket.MetaDataEntry;
026import org.apache.wicket.MetaDataKey;
027import org.apache.wicket.Session;
028import org.apache.wicket.WicketRuntimeException;
029import org.apache.wicket.page.IManageablePage;
030import org.apache.wicket.util.string.Strings;
031
032/**
033 * An {@link IPageStore} that groups pages.
034 * <p>
035 * By default all pages are stored in a single group, you'll have to override {@link #getGroup(IManageablePage)} to provide the actual group
036 * for a stored page, e.g. using a single group for all pages inside a single browser tab.
037 */
038public abstract class GroupingPageStore extends DelegatingPageStore
039{
040
041        private static final String DEFAULT_GROUP = "default";
042
043        private static final MetaDataKey<SessionData> KEY = new MetaDataKey<>()
044        {
045                private static final long serialVersionUID = 1L;
046        };
047
048        private final int maxGroups;
049        
050        /**
051         * Is a group of a page stable.
052         */
053        private boolean stableGroups = false;
054
055        /**
056         * @param delegate
057         *            store to delegate to
058         * @param maxGroups
059         *            maximum groups to keep
060         */
061        public GroupingPageStore(IPageStore delegate, int maxGroups)
062        {
063                super(delegate);
064
065                this.maxGroups = maxGroups;
066        }
067
068        /**
069         * Indicate that groups are stable, i.e. the group of a page never changes.
070         */
071        public GroupingPageStore withStableGroups()
072        {
073                stableGroups = true;
074
075                return this;
076        }
077
078        /**
079         * Get the group of a page, default is <code>"default"</code>
080         * 
081         * @return group of page, must not be empty
082         */
083        protected String getGroup(IManageablePage page)
084        {
085                return DEFAULT_GROUP;
086        }
087
088        private String getGroupInternal(IManageablePage page)
089        {
090                String group = getGroup(page);
091
092                if (Strings.isEmpty(group))
093                {
094                        throw new WicketRuntimeException("group must not be empy");
095                }
096
097                return group;
098        }
099
100        @Override
101        public void addPage(IPageContext context, IManageablePage page)
102        {
103                SessionData sessionData = getSessionData(context, true);
104
105                sessionData.addPage(context, page, getGroupInternal(page), maxGroups, stableGroups, getDelegate());
106        }
107
108        @Override
109        public void removePage(IPageContext context, IManageablePage page)
110        {
111                SessionData sessionData = getSessionData(context, false);
112                if (sessionData == null) {
113                        return;
114                }
115                
116                sessionData.removePage(context, page, getDelegate());
117        }
118
119        @Override
120        public void removeAllPages(IPageContext context)
121        {
122                SessionData sessionData = getSessionData(context, false);
123                if (sessionData == null) {
124                        return;
125                }
126
127                sessionData.removeAllPages(context, getDelegate());
128        }
129
130        @Override
131        public IManageablePage getPage(IPageContext context, int id)
132        {
133                SessionData sessionData = getSessionData(context, false);
134                if (sessionData == null) {
135                        return null;
136                }
137                
138                return sessionData.getPage(context, id, getDelegate());
139        }
140
141        private SessionData getSessionData(IPageContext context, boolean create)
142        {
143                return context.getSessionData(KEY, create ? () -> {
144                        return new SessionData();
145                } : null);
146        }
147
148        /**
149         * Data kept in the {@link Session}.
150         */
151        static class SessionData implements Serializable
152        {
153                private final LinkedList<String> groups = new LinkedList<>();
154
155                private final Map<String, MetaDataEntry<?>[]> metaData = new HashMap<>();
156
157                public synchronized <T> void setMetaData(String group, MetaDataKey<T> key, T value)
158                {
159                        metaData.put(group, key.set(metaData.get(group), value));
160                }
161
162                public synchronized <T> T getMetaData(String group, MetaDataKey<T> key)
163                {
164                        return key.get(metaData.get(group));
165                }
166                
167                public synchronized void addPage(IPageContext context, IManageablePage page, String group, int maxGroups, boolean stableGroups, IPageStore delegate)
168                {
169                        if (stableGroups == false)
170                        {
171                                // group might have changed, so remove page first from all groups
172                                for (String other : groups)
173                                {
174                                        delegate.removePage(new GroupContext(context, this, other), page);
175                                }
176                        }
177
178                        // add as last
179                        groups.remove(group);
180                        groups.addLast(group);
181                        
182                        // delegate
183                        delegate.addPage(new GroupContext(context, this, group), page);
184
185                        while (groups.size() > maxGroups)
186                        {
187                                String first = groups.removeFirst();
188                                
189                                delegate.removeAllPages(new GroupContext(context, this, first));
190                        }
191                }
192                
193                public IManageablePage getPage(IPageContext context, int id, IPageStore delegate)
194                {
195                        for (String group : groups)
196                        {
197                                IManageablePage page = delegate.getPage(new GroupContext(context, this, group), id);
198                                if (page != null)
199                                {
200                                        return page;
201                                }
202                        }
203                        return null;
204                }
205
206                public synchronized void removePage(IPageContext context, IManageablePage page, IPageStore delegate)
207                {
208                        for (String group : groups)
209                        {
210                                delegate.removePage(new GroupContext(context, this, group), page);
211                        }
212                }
213
214                public synchronized void removeAllPages(IPageContext context, IPageStore delegate)
215                {
216                        for (String group : groups)
217                        {
218                                delegate.removeAllPages(new GroupContext(context, this, group));
219                        }
220                }
221        }
222
223        /**
224         * Context passed to the delegate store to group data and attributes.
225         */
226        static class GroupContext implements IPageContext
227        {
228
229                private final IPageContext context;
230
231                private final SessionData sessionData;
232
233                private final String group;
234
235                public GroupContext(IPageContext context, SessionData sessionData, String group)
236                {
237                        this.context = context;
238                        this.sessionData = sessionData;
239                        this.group = group;
240                }
241
242                @Override
243                public String getSessionId(boolean bind)
244                {
245                        return context.getSessionId(true) + "_" + group;
246                }
247
248                @Override
249                public <T extends Serializable> T getSessionData(MetaDataKey<T> key, Supplier<T> defaultValue)
250                {
251                        synchronized (sessionData)
252                        {
253                                T data = sessionData.getMetaData(group, key);
254                                if (data == null) {
255                                        data = defaultValue.get();
256                                        
257                                        if (data != null) {
258                                                sessionData.setMetaData(group, key, data);
259                                        }
260                                }
261                                
262                                return data;
263                        }
264                }
265
266                @Override
267                public <T extends Serializable> T getSessionAttribute(String key, Supplier<T> defaultValue)
268                {
269                        return context.getSessionAttribute(key + "_" + group, defaultValue);
270                }
271
272                @Override
273                public <T> T getRequestData(MetaDataKey<T> key, Supplier<T> defaultValue)
274                {
275                        throw new WicketRuntimeException("no request available for group");
276                }
277        }
278}