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}