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}