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}