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.concurrent.ConcurrentHashMap;
021import java.util.concurrent.ConcurrentMap;
022
023import javax.servlet.http.HttpSessionBindingEvent;
024import javax.servlet.http.HttpSessionBindingListener;
025
026import org.apache.wicket.Session;
027import org.apache.wicket.page.IManageablePage;
028import org.apache.wicket.util.lang.Args;
029import org.apache.wicket.util.lang.Bytes;
030import org.apache.wicket.util.lang.Classes;
031import org.slf4j.Logger;
032import org.slf4j.LoggerFactory;
033
034/**
035 * Abstract base class for stores that keep an identifier in the session only, while holding the actual pages
036 * in a secondary persistent storage.
037 * 
038 * @see #getSessionIdentifier(IPageContext, boolean)
039 */
040public abstract class AbstractPersistentPageStore implements IPageStore
041{
042        private static final String KEY_PREFIX = "wicket:";
043
044        private static final Logger log = LoggerFactory.getLogger(AbstractPersistentPageStore.class);
045
046        /**
047         * A cache holding all store, the key is the application name suffixed with the page store implementation class.
048         */
049        private static final ConcurrentMap<String, AbstractPersistentPageStore> STORES = new ConcurrentHashMap<>();
050
051        private static final ThreadLocal<Boolean> gettingSessionAttribute = new ThreadLocal<>()
052        {
053                protected Boolean initialValue()
054                {
055                        return Boolean.FALSE;
056                }
057        };
058
059        private final String storeKey;
060
061        protected AbstractPersistentPageStore(String applicationName)
062        {
063                this.storeKey = Args.notNull(applicationName, "applicationName") + ":" + getClass().getSimpleName();
064
065                if (STORES.containsKey(storeKey))
066                {
067                        throw new IllegalStateException(
068                                "Store with key '" + storeKey + "' already exists.");
069                }
070                STORES.put(storeKey, this);
071        }
072
073        @Override
074        public void destroy()
075        {
076                STORES.remove(storeKey);
077        }
078
079        @Override
080        public boolean canBeAsynchronous(IPageContext context)
081        {
082                // session attribute must be added here *before* any asynchronous calls
083                // when session is no longer available
084                getSessionIdentifier(context, true);
085
086                return true;
087        }
088
089        @Override
090        public IManageablePage getPage(IPageContext context, int id)
091        {
092                String sessionIdentifier = getSessionIdentifier(context, false);
093                if (sessionIdentifier == null)
094                {
095                        return null;
096                }
097
098                return getPersistedPage(sessionIdentifier, id);
099        }
100
101        protected abstract IManageablePage getPersistedPage(String sessionIdentifier, int id);
102        
103        @Override
104        public void removePage(IPageContext context, IManageablePage page)
105        {
106                String sessionIdentifier = getSessionIdentifier(context, false);
107                if (sessionIdentifier == null)
108                {
109                        return;
110                }
111                
112                removePersistedPage(sessionIdentifier, page);
113        }
114
115        protected abstract void removePersistedPage(String sessionIdentifier, IManageablePage page);
116
117        @Override
118        public void removeAllPages(IPageContext context)
119        {
120                String sessionIdentifier = getSessionIdentifier(context, false);
121                if (sessionIdentifier == null)
122                {
123                        return;
124                }
125                
126                removeAllPersistedPages(sessionIdentifier);
127        }
128
129        protected abstract void removeAllPersistedPages(String sessionIdentifier);
130
131        @Override
132        public void addPage(IPageContext context, IManageablePage page)
133        {
134                String sessionIdentifier = getSessionIdentifier(context, true);
135                
136                addPersistedPage(sessionIdentifier, page);
137        }
138
139        /**
140         * Add a page.
141         * 
142         * @param sessionIdentifier identifier of session
143         * @param page page to add
144         */
145        protected abstract void addPersistedPage(String sessionIdentifier, IManageablePage page);
146
147        /**
148         * Get the distinct and stable identifier for the given context.
149         * 
150         * @param context the context to identify
151         * @param create should a new identifier be created if not there already
152         */
153        private String getSessionIdentifier(IPageContext context, boolean create)
154        {
155                gettingSessionAttribute.set(Boolean.TRUE);
156                try {
157                        String key = KEY_PREFIX + Classes.simpleName(getClass());
158                        
159                        SessionAttribute attribute = context.getSessionAttribute(key, create ? () -> {
160                                return new SessionAttribute(storeKey, createSessionIdentifier(context));
161                        } : null);
162                        
163                        if (attribute == null)
164                        {
165                                return null;
166                        }
167                        return attribute.sessionIdentifier;
168                } finally {
169                        gettingSessionAttribute.set(Boolean.FALSE);
170                }
171        }
172
173        /**
174         * Create an identifier for the given context.
175         * <p>
176         * Default implementation uses {@link IPageContext#getSessionId(boolean)}}.
177         * 
178         * @param context context
179         * @return identifier for the session
180         */
181        protected String createSessionIdentifier(IPageContext context)
182        {
183                return context.getSessionId(true);
184        }
185
186        /**
187         * Attribute held in session.
188         */
189        private static class SessionAttribute implements Serializable, HttpSessionBindingListener
190        {
191
192                private final String storeKey;
193
194                /**
195                 * The identifier of the session, may not be equal to {@link Session#getId()}, e.g. when
196                 * the container changes the id after authorization.
197                 */
198                public final String sessionIdentifier;
199
200                public SessionAttribute(String storeKey, String sessionIdentifier)
201                {
202                        this.storeKey = Args.notNull(storeKey, "storeKey");
203                        this.sessionIdentifier = Args.notNull(sessionIdentifier, "sessionIdentifier");
204                }
205
206
207                @Override
208                public void valueBound(HttpSessionBindingEvent event)
209                {
210                }
211
212                @Override
213                public void valueUnbound(HttpSessionBindingEvent event)
214                {
215                        AbstractPersistentPageStore store = STORES.get(storeKey);
216                        if (store == null)
217                        {
218                                log.warn(
219                                        "Cannot remove data '{}' because disk store '{}' is no longer present.", sessionIdentifier, storeKey);
220                        }
221                        else
222                        {
223                                if (Boolean.FALSE.equals(gettingSessionAttribute.get()))
224                                {
225                                        store.removeAllPersistedPages(sessionIdentifier);
226                                }               
227                        }
228                }
229        }
230
231        public String getSessionIdentifier(IPageContext context)
232        {
233                return getSessionIdentifier(context, true);
234        }
235
236        protected static class PersistedPage implements IPersistedPage
237        {
238                private final int pageId;
239
240                private final String pageType;
241
242                private final long pageSize;
243
244                public PersistedPage(int pageId, String pageType, long pageSize)
245                {
246                        this.pageId = pageId;
247                        this.pageType = pageType;
248                        this.pageSize = pageSize;
249                }
250
251                @Override
252                public int getPageId()
253                {
254                        return pageId;
255                }
256
257                @Override
258                public Bytes getPageSize()
259                {
260                        return Bytes.bytes(pageSize);
261                }
262
263                @Override
264                public String getPageType()
265                {
266                        return pageType;
267                }
268
269        }
270}