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.FileInputStream; 021import java.io.FileNotFoundException; 022import java.io.FileOutputStream; 023import java.io.IOException; 024import java.io.InputStream; 025import java.io.ObjectInputStream; 026import java.io.ObjectOutputStream; 027import java.io.OutputStream; 028import java.io.RandomAccessFile; 029import java.io.Serializable; 030import java.nio.ByteBuffer; 031import java.nio.channels.FileChannel; 032import java.util.ArrayList; 033import java.util.Collections; 034import java.util.List; 035import java.util.Set; 036import java.util.concurrent.ConcurrentHashMap; 037import java.util.concurrent.ConcurrentMap; 038 039import org.apache.wicket.WicketRuntimeException; 040import org.apache.wicket.page.IManageablePage; 041import org.apache.wicket.pageStore.disk.NestedFolders; 042import org.apache.wicket.pageStore.disk.PageWindowManager; 043import org.apache.wicket.pageStore.disk.PageWindowManager.FileWindow; 044import org.apache.wicket.protocol.http.PageExpiredException; 045import org.apache.wicket.util.file.Files; 046import org.apache.wicket.util.io.IOUtils; 047import org.apache.wicket.util.lang.Args; 048import org.apache.wicket.util.lang.Bytes; 049import org.slf4j.Logger; 050import org.slf4j.LoggerFactory; 051 052/** 053 * A storage of pages on disk. 054 * <p> 055 * All pages passed into this store are restricted to be {@link SerializedPage}s. 056 * <p> 057 * Implementation note: {@link DiskPageStore} writes pages into a single file, appending new pages while overwriting the oldest pages. 058 * Since Ajax requests do not change the id of a page, {@link DiskPageStore} offers an optimization to overwrite the most recently written 059 * page, if it has the same id as a new page to write.<p> 060 * However this does not help in case of alternating requests between multiple browser windows: In this case requests are processed for 061 * different page ids and the oldest pages are constantly overwritten (this can easily happen with Ajax timers on one or more pages). 062 * This leads to pages with identical id superfluously kept in the file, while older pages are prematurely expelled. 063 * Any following request to these older pages will then fail with {@link PageExpiredException}. 064 */ 065public class DiskPageStore extends AbstractPersistentPageStore implements IPersistentPageStore 066{ 067 private static final Logger log = LoggerFactory.getLogger(DiskPageStore.class); 068 069 /** 070 * Name of the file where the page index is stored. 071 */ 072 private static final String INDEX_FILE_NAME = "DiskPageStoreIndex"; 073 074 private final Bytes maxSizePerSession; 075 076 private final NestedFolders folders; 077 078 private final ConcurrentMap<String, DiskData> diskDatas; 079 080 /** 081 * Create a store that supports {@link SerializedPage}s only. 082 * 083 * @param applicationName 084 * name of application 085 * @param fileStoreFolder 086 * folder to store to 087 * @param maxSizePerSession 088 * maximum size per session 089 * 090 * @see SerializingPageStore 091 */ 092 public DiskPageStore(String applicationName, File fileStoreFolder, Bytes maxSizePerSession) 093 { 094 super(applicationName); 095 096 this.folders = new NestedFolders(new File(fileStoreFolder, applicationName + "-filestore")); 097 this.maxSizePerSession = Args.notNull(maxSizePerSession, "maxSizePerSession"); 098 099 this.diskDatas = new ConcurrentHashMap<>(); 100 101 try 102 { 103 if (folders.getBase().exists() || folders.getBase().mkdirs()) 104 { 105 loadIndex(); 106 } 107 else 108 { 109 log.warn("Cannot create file store folder for some reason."); 110 } 111 } 112 catch (SecurityException e) 113 { 114 throw new WicketRuntimeException( 115 "SecurityException occurred while creating DiskPageStore. Consider using a non-disk based IPageStore implementation. " 116 + "See org.apache.wicket.Application.setPageManagerProvider(IPageManagerProvider)", 117 e); 118 } 119 } 120 121 /** 122 * Pages are already serialized. 123 */ 124 @Override 125 public boolean supportsVersioning() 126 { 127 return true; 128 } 129 130 @Override 131 public void destroy() 132 { 133 log.debug("Destroying..."); 134 saveIndex(); 135 136 super.destroy(); 137 log.debug("Destroyed."); 138 } 139 140 @Override 141 protected IManageablePage getPersistedPage(String sessionIdentifier, int id) 142 { 143 DiskData diskData = getDiskData(sessionIdentifier, false); 144 if (diskData != null) 145 { 146 byte[] data = diskData.loadPage(id); 147 if (data != null) 148 { 149 if (log.isDebugEnabled()) 150 { 151 log.debug("Returning page with id '{}' in session with id '{}'", id, sessionIdentifier); 152 } 153 154 return new SerializedPage(id, "unknown", data); 155 } 156 } 157 158 return null; 159 } 160 161 @Override 162 protected void removePersistedPage(String sessionIdentifier, IManageablePage page) 163 { 164 DiskData diskData = getDiskData(sessionIdentifier, false); 165 if (diskData != null) 166 { 167 if (log.isDebugEnabled()) 168 { 169 log.debug("Removing page with id '{}' in session with id '{}'", page.getPageId(), sessionIdentifier); 170 } 171 172 diskData.removeData(page.getPageId()); 173 } 174 } 175 176 @Override 177 protected void removeAllPersistedPages(String sessionIdentifier) 178 { 179 DiskData diskData = getDiskData(sessionIdentifier, false); 180 if (diskData != null) 181 { 182 synchronized (diskDatas) 183 { 184 diskDatas.remove(diskData.sessionIdentifier); 185 diskData.unbind(); 186 } 187 } 188 } 189 190 @Override 191 protected void addPersistedPage(String sessionIdentifier, IManageablePage page) 192 { 193 if (page instanceof SerializedPage == false) 194 { 195 throw new WicketRuntimeException("DiskPageStore works with serialized pages only"); 196 } 197 SerializedPage serializedPage = (SerializedPage) page; 198 199 DiskData diskData = getDiskData(sessionIdentifier, true); 200 201 log.debug("Storing data for page with id '{}' in session with id '{}'", serializedPage.getPageId(), sessionIdentifier); 202 203 byte[] data = serializedPage.getData(); 204 String type = serializedPage.getPageType(); 205 206 diskData.savePage(serializedPage.getPageId(), type, data); 207 } 208 209 /** 210 * Get the data on disk for the given session identifier. 211 * 212 * @param sessionIdentifier identifier of session 213 * @return matching data 214 */ 215 protected DiskData getDiskData(String sessionIdentifier, boolean create) 216 { 217 if (!create) 218 { 219 return diskDatas.get(sessionIdentifier); 220 } 221 222 DiskData data = new DiskData(this, sessionIdentifier); 223 DiskData existing = diskDatas.putIfAbsent(sessionIdentifier, data); 224 return existing != null ? existing : data; 225 } 226 227 /** 228 * Load the index 229 */ 230 @SuppressWarnings("unchecked") 231 private void loadIndex() 232 { 233 File storeFolder = folders.getBase(); 234 235 File index = new File(storeFolder, INDEX_FILE_NAME); 236 if (index.exists() && index.length() > 0) 237 { 238 try (InputStream stream = new FileInputStream(index)) 239 { 240 ObjectInputStream ois = new ObjectInputStream(stream); 241 242 diskDatas.clear(); 243 244 for (DiskData diskData : (List<DiskData>)ois.readObject()) 245 { 246 diskData.pageStore = this; 247 diskDatas.put(diskData.sessionIdentifier, diskData); 248 } 249 } 250 catch (Exception e) 251 { 252 log.error("Couldn't load DiskPageStore index from file " + index + ".", e); 253 } 254 } 255 Files.remove(index); 256 } 257 258 private void saveIndex() 259 { 260 File storeFolder = folders.getBase(); 261 if (storeFolder.exists()) 262 { 263 File index = new File(storeFolder, INDEX_FILE_NAME); 264 Files.remove(index); 265 try (OutputStream stream = new FileOutputStream(index)) 266 { 267 ObjectOutputStream oos = new ObjectOutputStream(stream); 268 269 ArrayList<DiskData> list = new ArrayList<>(diskDatas.size()); 270 for (DiskData diskData : diskDatas.values()) 271 { 272 if (diskData.sessionIdentifier != null) 273 { 274 list.add(diskData); 275 } 276 } 277 oos.writeObject(list); 278 } 279 catch (Exception e) 280 { 281 log.error("Couldn't write DiskPageStore index to file " + index + ".", e); 282 } 283 } 284 } 285 286 @Override 287 public Set<String> getSessionIdentifiers() 288 { 289 return Collections.unmodifiableSet(diskDatas.keySet()); 290 } 291 292 /** 293 * 294 * @param sessionIdentifier 295 * key 296 * @return a list of the last N page windows 297 */ 298 @Override 299 public List<IPersistedPage> getPersistedPages(String sessionIdentifier) 300 { 301 List<IPersistedPage> pages = new ArrayList<>(); 302 303 DiskData diskData = getDiskData(sessionIdentifier, false); 304 if (diskData != null) 305 { 306 PageWindowManager windowManager = diskData.getManager(); 307 308 pages.addAll(windowManager.getFileWindows()); 309 } 310 return pages; 311 } 312 313 @Override 314 public Bytes getTotalSize() 315 { 316 long size = 0; 317 318 synchronized (diskDatas) 319 { 320 for (DiskData diskData : diskDatas.values()) 321 { 322 size = size + diskData.size(); 323 } 324 } 325 326 return Bytes.bytes(size); 327 } 328 329 /** 330 * Data held on disk. 331 */ 332 protected static class DiskData implements Serializable 333 { 334 private static final long serialVersionUID = 1L; 335 336 private transient DiskPageStore pageStore; 337 338 private transient String fileName; 339 340 private String sessionIdentifier; 341 342 private PageWindowManager manager; 343 344 protected DiskData(DiskPageStore pageStore, String sessionIdentifier) 345 { 346 this.pageStore = pageStore; 347 348 this.sessionIdentifier = sessionIdentifier; 349 } 350 351 public long size() 352 { 353 return manager.getTotalSize(); 354 } 355 356 public PageWindowManager getManager() 357 { 358 if (manager == null) 359 { 360 manager = new PageWindowManager(pageStore.maxSizePerSession.bytes()); 361 } 362 return manager; 363 } 364 365 private String getFileName() 366 { 367 if (fileName == null) 368 { 369 fileName = pageStore.getSessionFileName(sessionIdentifier); 370 } 371 return fileName; 372 } 373 374 /** 375 * @return session id 376 */ 377 public String getKey() 378 { 379 return sessionIdentifier; 380 } 381 382 /** 383 * Saves the serialized page to appropriate file. 384 * 385 * @param pageId 386 * @param pageType 387 * @param data 388 */ 389 public synchronized void savePage(int pageId, String pageType, byte data[]) 390 { 391 if (sessionIdentifier == null) 392 { 393 return; 394 } 395 396 // only save page that has some data 397 if (data != null) 398 { 399 // allocate window for page 400 FileWindow window = getManager().createPageWindow(pageId, pageType, data.length); 401 402 FileChannel channel = getFileChannel(true); 403 if (channel != null) 404 { 405 try 406 { 407 // write the content 408 channel.write(ByteBuffer.wrap(data), window.getFilePartOffset()); 409 } 410 catch (IOException e) 411 { 412 log.error("Error writing to a channel " + channel, e); 413 } 414 finally 415 { 416 IOUtils.closeQuietly(channel); 417 } 418 } 419 else 420 { 421 log.warn( 422 "Cannot save page with id '{}' because the data file cannot be opened.", 423 pageId); 424 } 425 } 426 } 427 428 /** 429 * Removes the page from disk. 430 * 431 * @param pageId 432 */ 433 public synchronized void removeData(int pageId) 434 { 435 if (sessionIdentifier == null) 436 { 437 return; 438 } 439 440 getManager().removePage(pageId); 441 } 442 443 /** 444 * Loads the part of pagemap file specified by the given PageWindow. 445 * 446 * @param window 447 * @return serialized page data 448 */ 449 public byte[] loadData(FileWindow window) 450 { 451 byte[] result = null; 452 FileChannel channel = getFileChannel(false); 453 if (channel != null) 454 { 455 ByteBuffer buffer = ByteBuffer.allocate(window.getFilePartSize()); 456 try 457 { 458 channel.read(buffer, window.getFilePartOffset()); 459 if (buffer.hasArray()) 460 { 461 result = buffer.array(); 462 } 463 } 464 catch (IOException e) 465 { 466 log.error("Error reading from file channel " + channel, e); 467 } 468 finally 469 { 470 IOUtils.closeQuietly(channel); 471 } 472 } 473 return result; 474 } 475 476 private FileChannel getFileChannel(boolean create) 477 { 478 FileChannel channel = null; 479 File file = new File(getFileName()); 480 if (create || file.exists()) 481 { 482 String mode = create ? "rw" : "r"; 483 try 484 { 485 RandomAccessFile randomAccessFile = new RandomAccessFile(file, mode); 486 channel = randomAccessFile.getChannel(); 487 } 488 catch (FileNotFoundException fnfx) 489 { 490 // can happen if the file is locked. WICKET-4176 491 log.error(fnfx.getMessage(), fnfx); 492 } 493 } 494 return channel; 495 } 496 497 /** 498 * Loads the specified page data. 499 * 500 * @param id 501 * @return page data or null if the page is no longer in pagemap file 502 */ 503 public synchronized byte[] loadPage(int id) 504 { 505 if (sessionIdentifier == null) 506 { 507 return null; 508 } 509 510 FileWindow window = getManager().getPageWindow(id); 511 if (window == null) 512 { 513 return null; 514 } 515 516 return loadData(window); 517 } 518 519 /** 520 * Deletes all files for this session. 521 */ 522 public synchronized void unbind() 523 { 524 pageStore.folders.remove(sessionIdentifier); 525 526 sessionIdentifier = null; 527 } 528 } 529 530 /** 531 * Returns the file name for specified session. If the session folder (folder that contains the 532 * file) does not exist, the folder will be created. 533 * 534 * @param sessionIdentifier 535 * @return file name for pagemap 536 */ 537 private String getSessionFileName(String sessionIdentifier) 538 { 539 File sessionFolder = folders.get(sessionIdentifier, true); 540 return new File(sessionFolder, "data").getAbsolutePath(); 541 } 542}