CachingXmlDataStore.java

  1. /*******************************************************************************
  2.  * Copyright 2012 André Rouél
  3.  *
  4.  * Licensed under the Apache License, Version 2.0 (the "License");
  5.  * you may not use this file except in compliance with the License.
  6.  * You may obtain a copy of the License at
  7.  *
  8.  *   http://www.apache.org/licenses/LICENSE-2.0
  9.  *
  10.  * Unless required by applicable law or agreed to in writing, software
  11.  * distributed under the License is distributed on an "AS IS" BASIS,
  12.  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13.  * See the License for the specific language governing permissions and
  14.  * limitations under the License.
  15.  ******************************************************************************/
  16. package net.sf.uadetector.datastore;

  17. import java.io.File;
  18. import java.io.IOException;
  19. import java.net.URL;
  20. import java.nio.charset.Charset;

  21. import javax.annotation.Nonnull;

  22. import net.sf.qualitycheck.Check;
  23. import net.sf.qualitycheck.exception.IllegalStateOfArgumentException;
  24. import net.sf.uadetector.datareader.DataReader;
  25. import net.sf.uadetector.datareader.XmlDataReader;
  26. import net.sf.uadetector.internal.data.Data;
  27. import net.sf.uadetector.internal.util.FileUtil;
  28. import net.sf.uadetector.internal.util.UrlUtil;

  29. import org.slf4j.Logger;
  30. import org.slf4j.LoggerFactory;

  31. /**
  32.  * Implementation of a {@link DataStore} which is able to recover <em>UAS data</em> in XML format from a cache file. If
  33.  * the cache file is empty, the data will be read from the given data URL.<br>
  34.  * <br>
  35.  * You can also update the data of the store at any time if you trigger {@link CachingXmlDataStore#refresh()}.
  36.  *
  37.  * @author André Rouél
  38.  */
  39. public final class CachingXmlDataStore extends AbstractRefreshableDataStore {

  40.     /**
  41.      * Internal data store which will be used to load previously saved <em>UAS data</em> from a cache file.
  42.      */
  43.     private static class CacheFileDataStore extends AbstractDataStore {
  44.         protected CacheFileDataStore(final Data data, final DataReader reader, final URL dataUrl, final Charset charset) {
  45.             super(data, reader, dataUrl, dataUrl, charset);
  46.         }
  47.     }

  48.     /**
  49.      * The default temporary-file directory
  50.      */
  51.     private static final String CACHE_DIR = System.getProperty("java.io.tmpdir");

  52.     /**
  53.      * Corresponding default logger of this class
  54.      */
  55.     private static final Logger LOG = LoggerFactory.getLogger(CachingXmlDataStore.class);

  56.     /**
  57.      * Message for the log if the cache file is filled
  58.      */
  59.     private static final String MSG_CACHE_FILE_IS_EMPTY = "The cache file is empty. The given UAS data source will be imported.";

  60.     /**
  61.      * Message for the log if the cache file is empty
  62.      */
  63.     private static final String MSG_CACHE_FILE_IS_FILLED = "The cache file is filled and will be imported.";

  64.     /**
  65.      * Message if the cache file contains unexpected data and must be deleted manually
  66.      */
  67.     private static final String MSG_CACHE_FILE_IS_DAMAGED = "The cache file '%s' is damaged and must be removed manually.";

  68.     /**
  69.      * Message if the cache file contains unexpected data and has been removed
  70.      */
  71.     private static final String MSG_CACHE_FILE_IS_DAMAGED_AND_DELETED = "The cache file '%s' is damaged and has been deleted.";

  72.     /**
  73.      * The prefix string to be used in generating the cache file's name; must be at least three characters long
  74.      */
  75.     private static final String PREFIX = "uas";

  76.     /**
  77.      * The suffix string to be used in generating the cache file's name; may be {@code null}, in which case the suffix "
  78.      * {@code .tmp}" will be used
  79.      */
  80.     private static final String SUFFIX = ".xml";

  81.     /**
  82.      * Constructs a new instance of {@code CachingXmlDataStore} with the given arguments. The given {@code cacheFile}
  83.      * can be empty or filled with previously cached data in XML format. The file must be writable otherwise an
  84.      * exception will be thrown.
  85.      *
  86.      * @param fallback
  87.      *            <em>UAS data</em> as fallback in case the data on the specified resource can not be read correctly
  88.      * @return new instance of {@link CachingXmlDataStore}
  89.      * @throws net.sf.qualitycheck.exception.IllegalNullArgumentException
  90.      *             if one of the given arguments is {@code null}
  91.      * @throws net.sf.qualitycheck.exception.IllegalStateOfArgumentException
  92.      *             if the given cache file can not be read
  93.      * @throws net.sf.qualitycheck.exception.IllegalStateOfArgumentException
  94.      *             if no URL can be resolved to the given given file
  95.      */
  96.     @Nonnull
  97.     public static CachingXmlDataStore createCachingXmlDataStore(@Nonnull final DataStore fallback) {
  98.         return createCachingXmlDataStore(findOrCreateCacheFile(), fallback);
  99.     }

  100.     /**
  101.      * Constructs a new instance of {@code CachingXmlDataStore} with the given arguments. The given {@code cacheFile}
  102.      * can be empty or filled with previously cached data in XML format. The file must be writable otherwise an
  103.      * exception will be thrown.
  104.      *
  105.      * @param cacheFile
  106.      *            file with cached <em>UAS data</em> in XML format or empty file
  107.      * @param fallback
  108.      *            <em>UAS data</em> as fallback in case the data on the specified resource can not be read correctly
  109.      * @return new instance of {@link CachingXmlDataStore}
  110.      * @throws net.sf.qualitycheck.exception.IllegalNullArgumentException
  111.      *             if one of the given arguments is {@code null}
  112.      * @throws net.sf.qualitycheck.exception.IllegalStateOfArgumentException
  113.      *             if the given cache file can not be read
  114.      * @throws net.sf.qualitycheck.exception.IllegalStateOfArgumentException
  115.      *             if no URL can be resolved to the given given file
  116.      */
  117.     @Nonnull
  118.     public static CachingXmlDataStore createCachingXmlDataStore(@Nonnull final File cacheFile, @Nonnull final DataStore fallback) {
  119.         return createCachingXmlDataStore(cacheFile, UrlUtil.build(DEFAULT_DATA_URL), UrlUtil.build(DEFAULT_VERSION_URL), DEFAULT_CHARSET,
  120.                 fallback);
  121.     }

  122.     /**
  123.      * Constructs a new instance of {@code CachingXmlDataStore} with the given arguments. The given {@code cacheFile}
  124.      * can be empty or filled with previously cached data in XML format. The file must be writable otherwise an
  125.      * exception will be thrown.
  126.      *
  127.      * @param cacheFile
  128.      *            file with cached <em>UAS data</em> in XML format or empty file
  129.      * @param dataUrl
  130.      *            URL to <em>UAS data</em>
  131.      * @param versionUrl
  132.      *            URL to version information about the given <em>UAS data</em>
  133.      * @param charset
  134.      *            the character set in which the data should be read
  135.      * @param fallback
  136.      *            <em>UAS data</em> as fallback in case the data on the specified resource can not be read correctly
  137.      * @return new instance of {@link CachingXmlDataStore}
  138.      * @throws net.sf.qualitycheck.exception.IllegalNullArgumentException
  139.      *             if one of the given arguments is {@code null}
  140.      * @throws net.sf.qualitycheck.exception.IllegalNullArgumentException
  141.      *             if the given cache file can not be read
  142.      * @throws net.sf.qualitycheck.exception.IllegalStateOfArgumentException
  143.      *             if no URL can be resolved to the given given file
  144.      */
  145.     @Nonnull
  146.     public static CachingXmlDataStore createCachingXmlDataStore(@Nonnull final File cacheFile, @Nonnull final URL dataUrl,
  147.             @Nonnull final URL versionUrl, @Nonnull final Charset charset, @Nonnull final DataStore fallback) {
  148.         Check.notNull(cacheFile, "cacheFile");
  149.         Check.notNull(charset, "charset");
  150.         Check.notNull(dataUrl, "dataUrl");
  151.         Check.notNull(fallback, "fallback");
  152.         Check.notNull(versionUrl, "versionUrl");

  153.         final DataReader reader = new XmlDataReader();
  154.         final DataStore fallbackDataStore = readCacheFileAsFallback(reader, cacheFile, charset, fallback);
  155.         return new CachingXmlDataStore(reader, dataUrl, versionUrl, charset, cacheFile, fallbackDataStore);
  156.     }

  157.     /**
  158.      * Constructs a new instance of {@code CachingXmlDataStore} with the given arguments. The file used to cache the
  159.      * read in <em>UAS data</em> will be called from {@link CachingXmlDataStore#findOrCreateCacheFile()}. This file may
  160.      * be empty or filled with previously cached data in XML format. The file must be writable otherwise an exception
  161.      * will be thrown.
  162.      *
  163.      * @param dataUrl
  164.      *            URL to <em>UAS data</em>
  165.      * @param versionUrl
  166.      *            URL to version information about the given <em>UAS data</em>
  167.      * @param charset
  168.      *            the character set in which the data should be read
  169.      * @param fallback
  170.      *            <em>UAS data</em> as fallback in case the data on the specified resource can not be read correctly
  171.      * @return new instance of {@link CachingXmlDataStore}
  172.      * @throws net.sf.qualitycheck.exception.IllegalNullArgumentException
  173.      *             if one of the given arguments is {@code null}
  174.      * @throws net.sf.qualitycheck.exception.IllegalStateOfArgumentException
  175.      *             if the given cache file can not be read
  176.      */
  177.     @Nonnull
  178.     public static CachingXmlDataStore createCachingXmlDataStore(@Nonnull final URL dataUrl, @Nonnull final URL versionUrl,
  179.             @Nonnull final Charset charset, @Nonnull final DataStore fallback) {
  180.         return createCachingXmlDataStore(findOrCreateCacheFile(), dataUrl, versionUrl, charset, fallback);
  181.     }

  182.     /**
  183.      * Removes the given cache file because it contains damaged content.
  184.      *
  185.      * @param cacheFile
  186.      *            cache file to delete
  187.      */
  188.     private static void deleteCacheFile(final File cacheFile) {
  189.         try {
  190.             if (cacheFile.delete()) {
  191.                 LOG.warn(String.format(MSG_CACHE_FILE_IS_DAMAGED_AND_DELETED, cacheFile.getPath()));
  192.             } else {
  193.                 LOG.warn(String.format(MSG_CACHE_FILE_IS_DAMAGED, cacheFile.getPath()));
  194.             }
  195.         } catch (final Exception e) {
  196.             LOG.warn(String.format(MSG_CACHE_FILE_IS_DAMAGED, cacheFile.getPath()));
  197.         }
  198.     }

  199.     /**
  200.      * Gets the cache file for <em>UAS data</em> in the default temporary-file directory. If no cache file exists, a new
  201.      * empty file in the default temporary-file directory will be created, using the default prefix and suffix to
  202.      * generate its name.
  203.      *
  204.      * @return file to cache read in <em>UAS data</em>
  205.      * @throws net.sf.qualitycheck.exception.IllegalStateOfArgumentException
  206.      *             if the cache file can not be created
  207.      */
  208.     @Nonnull
  209.     public static File findOrCreateCacheFile() {
  210.         final File file = new File(CACHE_DIR, PREFIX + SUFFIX);
  211.         if (!file.exists()) {
  212.             try {
  213.                 file.createNewFile();
  214.             } catch (final IOException e) {
  215.                 throw new IllegalStateOfArgumentException("Can not create a cache file.", e);
  216.             }
  217.         }
  218.         return file;
  219.     }

  220.     /**
  221.      * Checks if the given file is empty.
  222.      *
  223.      * @param file
  224.      *            the file that could be empty
  225.      * @return {@code true} when the file is accessible and empty otherwise {@code false}
  226.      * @throws net.sf.qualitycheck.exception.IllegalStateOfArgumentException
  227.      *             if an I/O error occurs
  228.      */
  229.     private static boolean isEmpty(@Nonnull final File file, @Nonnull final Charset charset) {
  230.         try {
  231.             return FileUtil.isEmpty(file, charset);
  232.         } catch (final IOException e) {
  233.             throw new IllegalStateOfArgumentException("The given file could not be read.", e);
  234.         }
  235.     }

  236.     /**
  237.      * Tries to read the content of specified cache file and returns them as fallback data store. If the cache file
  238.      * contains unexpected data the given fallback data store will be returned instead.
  239.      *
  240.      * @param reader
  241.      *            data reader to read the given {@code dataUrl}
  242.      * @param cacheFile
  243.      *            file with cached <em>UAS data</em> in XML format or empty file
  244.      * @param versionUrl
  245.      *            URL to version information about the given <em>UAS data</em>
  246.      * @param charset
  247.      *            the character set in which the data should be read
  248.      * @param fallback
  249.      *            <em>UAS data</em> as fallback in case the data on the specified resource can not be read correctly
  250.      * @return a fallback data store
  251.      */
  252.     private static DataStore readCacheFileAsFallback(@Nonnull final DataReader reader, @Nonnull final File cacheFile,
  253.             @Nonnull final Charset charset, @Nonnull final DataStore fallback) {
  254.         DataStore fallbackDataStore;
  255.         if (!isEmpty(cacheFile, charset)) {
  256.             final URL cacheFileUrl = UrlUtil.toUrl(cacheFile);
  257.             try {
  258.                 fallbackDataStore = new CacheFileDataStore(reader.read(cacheFileUrl, charset), reader, cacheFileUrl, charset);
  259.                 LOG.debug(MSG_CACHE_FILE_IS_FILLED);
  260.             } catch (final RuntimeException e) {
  261.                 fallbackDataStore = fallback;
  262.                 deleteCacheFile(cacheFile);
  263.             }
  264.         } else {
  265.             fallbackDataStore = fallback;
  266.             LOG.debug(MSG_CACHE_FILE_IS_EMPTY);
  267.         }
  268.         return fallbackDataStore;
  269.     }

  270.     /**
  271.      * Constructs an {@code CachingXmlDataStore} with the given arguments.
  272.      *
  273.      * @param data
  274.      *            first <em>UAS data</em> which will be available in the store
  275.      * @param reader
  276.      *            data reader to read the given {@code dataUrl}
  277.      * @param dataUrl
  278.      *            URL to <em>UAS data</em>
  279.      * @param versionUrl
  280.      *            URL to version information about the given <em>UAS data</em>
  281.      * @param charset
  282.      *            the character set in which the data should be read
  283.      * @param cacheFile
  284.      *            file with cached <em>UAS data</em> in XML format or an empty file
  285.      * @throws net.sf.qualitycheck.exception.IllegalNullArgumentException
  286.      *             if one of the given arguments is {@code null}
  287.      */
  288.     private CachingXmlDataStore(@Nonnull final DataReader reader, @Nonnull final URL dataUrl, @Nonnull final URL versionUrl,
  289.             @Nonnull final Charset charset, @Nonnull final File cacheFile, @Nonnull final DataStore fallback) {
  290.         super(reader, dataUrl, versionUrl, charset, fallback);
  291.         setUpdateOperation(new UpdateOperationWithCacheFileTask(this, cacheFile));
  292.     }

  293. }