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.File; 020import java.io.IOException; 021import java.nio.ByteBuffer; 022import java.nio.MappedByteBuffer; 023import java.nio.channels.FileChannel; 024import java.nio.channels.FileChannel.MapMode; 025import java.nio.charset.Charset; 026import java.nio.file.StandardOpenOption; 027import java.nio.file.attribute.UserDefinedFileAttributeView; 028import java.util.ArrayList; 029import java.util.Arrays; 030import java.util.Comparator; 031import java.util.HashSet; 032import java.util.List; 033import java.util.Set; 034 035import org.apache.wicket.WicketRuntimeException; 036import org.apache.wicket.page.IManageablePage; 037import org.apache.wicket.pageStore.disk.NestedFolders; 038import org.apache.wicket.util.file.Files; 039import org.apache.wicket.util.io.IOUtils; 040import org.apache.wicket.util.lang.Args; 041import org.apache.wicket.util.lang.Bytes; 042import org.slf4j.Logger; 043import org.slf4j.LoggerFactory; 044 045/** 046 * A storage of pages in files. 047 * <p> 048 * All pages passed into this store are restricted to be {@link SerializedPage}s. 049 * <p> 050 * While {@link DiskPageStore} uses a single file per session, this implementation stores each page 051 * in its own file. This improves on a {@link DiskPageStore disadvantage of DiskPageStore} surfacing 052 * with alternating Ajax requests from different browser tabs. 053 */ 054public class FilePageStore extends AbstractPersistentPageStore implements IPersistentPageStore 055{ 056 private static final String ATTRIBUTE_PAGE_TYPE = "user.wicket_page_type"; 057 058 private static final String FILE_SUFFIX = ".data"; 059 060 private static final Logger log = LoggerFactory.getLogger(FilePageStore.class); 061 062 private final Bytes maxSizePerSession; 063 064 private final NestedFolders folders; 065 066 /** 067 * Create a store that supports {@link SerializedPage}s only. 068 * 069 * @param applicationName 070 * name of application 071 * @param fileStoreFolder 072 * folder to store to 073 * @param maxSizePerSession 074 * maximum size per session 075 * 076 * @see SerializingPageStore 077 */ 078 public FilePageStore(String applicationName, File fileStoreFolder, Bytes maxSizePerSession) 079 { 080 super(applicationName); 081 082 this.folders = new NestedFolders(new File(fileStoreFolder, applicationName + "-filestore")); 083 this.maxSizePerSession = Args.notNull(maxSizePerSession, "maxSizePerSession"); 084 } 085 086 /** 087 * Pages are always serialized, so versioning is supported. 088 */ 089 @Override 090 public boolean supportsVersioning() 091 { 092 return true; 093 } 094 095 private File getPageFile(String sessionId, int id, boolean create) 096 { 097 File folder = folders.get(sessionId, create); 098 099 return new File(folder, id + FILE_SUFFIX); 100 } 101 102 @Override 103 protected IManageablePage getPersistedPage(String sessionIdentifier, int id) 104 { 105 File file = getPageFile(sessionIdentifier, id, false); 106 if (file.exists() == false) 107 { 108 return null; 109 } 110 111 byte[] data; 112 try 113 { 114 data = readFile(file); 115 } 116 catch (IOException ex) 117 { 118 log.warn("cannot read page data for session {} page {}", sessionIdentifier, id, ex); 119 return null; 120 } 121 122 return new SerializedPage(id, "unknown", data); 123 } 124 125 /** 126 * Read a file. 127 * <p> 128 * Note: This implementation uses a {@link FileChannel}. 129 * 130 * @param file 131 * file to read 132 * @throws IOException 133 */ 134 protected byte[] readFile(File file) throws IOException 135 { 136 byte[] data; 137 138 FileChannel channel = FileChannel.open(file.toPath()); 139 try 140 { 141 int size = (int)channel.size(); 142 MappedByteBuffer buf = channel.map(MapMode.READ_ONLY, 0, size); 143 data = new byte[size]; 144 buf.get(data); 145 } 146 finally 147 { 148 IOUtils.closeQuietly(channel); 149 } 150 151 return data; 152 } 153 154 @Override 155 protected void removePersistedPage(String sessionIdentifier, IManageablePage page) 156 { 157 File file = getPageFile(sessionIdentifier, page.getPageId(), false); 158 if (file.exists()) 159 { 160 if (!file.delete()) 161 { 162 log.warn("cannot remove page data for session {} page {}", sessionIdentifier, page.getPageId()); 163 } 164 } 165 } 166 167 @Override 168 protected void removeAllPersistedPages(String sessionIdentifier) 169 { 170 folders.remove(sessionIdentifier); 171 } 172 173 @Override 174 protected void addPersistedPage(String sessionIdentifier, IManageablePage page) 175 { 176 if (page instanceof SerializedPage == false) 177 { 178 throw new WicketRuntimeException("FilePageStore works with serialized pages only"); 179 } 180 SerializedPage serializedPage = (SerializedPage)page; 181 182 byte[] data = serializedPage.getData(); 183 184 File file = getPageFile(sessionIdentifier, serializedPage.getPageId(), true); 185 try 186 { 187 writeFile(file, data); 188 } 189 catch (IOException ex) 190 { 191 log.warn("cannot store page data for session {} page {}", sessionIdentifier, 192 serializedPage.getPageId(), ex); 193 } 194 195 setPageType(file, serializedPage.getPageType()); 196 197 checkMaxSize(sessionIdentifier); 198 } 199 200 /** 201 * Write a file with given data. 202 * <p> 203 * Note: This implementation uses a {@link FileChannel} with 204 * {@link StandardOpenOption#TRUNCATE_EXISTING}. This might fail on Windows systems with 205 * "The requested operation cannot be performed on a file with a user-mapped section open", so subclasses 206 * can omit this option to circumvent this error, although this prevents files from shrinking when pages become smaller. 207 * Alternatively a completely different implementation can be chosen. 208 * 209 * @param file 210 * file to write 211 * @param data 212 * data to write 213 * @throws IOException 214 */ 215 protected void writeFile(File file, byte[] data) throws IOException 216 { 217 FileChannel channel = FileChannel.open(file.toPath(), StandardOpenOption.CREATE, 218 StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.WRITE); 219 try 220 { 221 ByteBuffer buffer = ByteBuffer.wrap(data); 222 channel.write(buffer); 223 } 224 finally 225 { 226 IOUtils.closeQuietly(channel); 227 } 228 } 229 230 private void checkMaxSize(String sessionIdentifier) 231 { 232 File[] files = folders.get(sessionIdentifier, true).listFiles(); 233 Arrays.sort(files, new LastModifiedComparator()); 234 235 long total = 0; 236 for (File candidate : files) 237 { 238 total += candidate.length(); 239 240 if (total > maxSizePerSession.bytes()) 241 { 242 if (!Files.remove(candidate)) 243 { 244 log.warn("cannot remove page data for session {} page {}", sessionIdentifier, candidate.getName()); 245 } 246 } 247 } 248 } 249 250 public static class LastModifiedComparator implements Comparator<File> 251 { 252 @Override 253 public int compare(File f1, File f2) 254 { 255 return Long.compare(f2.lastModified(), f1.lastModified()); 256 } 257 } 258 259 @Override 260 public Set<String> getSessionIdentifiers() 261 { 262 Set<String> sessionIdentifiers = new HashSet<>(); 263 264 for (File folder : folders.getAll()) 265 { 266 sessionIdentifiers.add(folder.getName()); 267 } 268 269 return sessionIdentifiers; 270 } 271 272 @Override 273 public List<IPersistedPage> getPersistedPages(String sessionIdentifier) 274 { 275 List<IPersistedPage> pages = new ArrayList<>(); 276 277 File folder = folders.get(sessionIdentifier, false); 278 if (folder.exists()) 279 { 280 File[] files = folder.listFiles(); 281 Arrays.sort(files, new LastModifiedComparator()); 282 for (File file : files) 283 { 284 String name = file.getName(); 285 if (name.endsWith(FILE_SUFFIX)) 286 { 287 int pageId; 288 try 289 { 290 pageId = Integer.parseInt(name.substring(0, name.length() - FILE_SUFFIX.length()), 10); 291 } 292 catch (Exception ex) 293 { 294 log.debug("unexpected file {}", file.getAbsolutePath()); 295 continue; 296 } 297 298 String pageType = getPageType(file); 299 300 pages.add(new PersistedPage(pageId, pageType, file.length())); 301 } 302 } 303 } 304 305 return pages; 306 } 307 308 /** 309 * Get the type of page from the given file. 310 * <p> 311 * This is an optional operation that returns <code>null</code> in case of any error. 312 * 313 * @param file 314 * @return pageType 315 */ 316 protected String getPageType(File file) 317 { 318 String pageType = null; 319 try 320 { 321 UserDefinedFileAttributeView view = getAttributeView(file); 322 323 ByteBuffer buffer = ByteBuffer.allocate(view.size(ATTRIBUTE_PAGE_TYPE)); 324 view.read(ATTRIBUTE_PAGE_TYPE, buffer); 325 buffer.flip(); 326 pageType = Charset.defaultCharset().decode(buffer).toString(); 327 } 328 catch (Exception ex) 329 { 330 log.debug("cannot get pageType for {}", file); 331 } 332 333 return pageType; 334 } 335 336 private UserDefinedFileAttributeView getAttributeView(File file) 337 { 338 return java.nio.file.Files 339 .getFileAttributeView(file.toPath(), UserDefinedFileAttributeView.class); 340 } 341 342 /** 343 * Set the type of page on the given file. 344 * <p> 345 * This is an optional operation that silently fails in case of an error. 346 * 347 * @param file 348 * @param pageType 349 */ 350 protected void setPageType(File file, String pageType) 351 { 352 try 353 { 354 UserDefinedFileAttributeView view = getAttributeView(file); 355 356 view.write(ATTRIBUTE_PAGE_TYPE, Charset.defaultCharset().encode(pageType)); 357 } 358 catch (Exception ex) 359 { 360 log.debug("cannot set pageType for {}", file, ex); 361 } 362 } 363 364 @Override 365 public Bytes getTotalSize() 366 { 367 long total = 0; 368 369 for (File folder : folders.getAll()) 370 { 371 for (File file : folder.listFiles()) 372 { 373 String name = file.getName(); 374 if (name.endsWith(FILE_SUFFIX)) 375 { 376 total += file.length(); 377 } 378 } 379 } 380 381 return Bytes.bytes(total); 382 } 383}