CachingXmlDataStore.java
/*******************************************************************************
* Copyright 2012 André Rouél
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
******************************************************************************/
package net.sf.uadetector.datastore;
import java.io.File;
import java.io.IOException;
import java.net.URL;
import java.nio.charset.Charset;
import javax.annotation.Nonnull;
import net.sf.qualitycheck.Check;
import net.sf.qualitycheck.exception.IllegalStateOfArgumentException;
import net.sf.uadetector.datareader.DataReader;
import net.sf.uadetector.datareader.XmlDataReader;
import net.sf.uadetector.internal.data.Data;
import net.sf.uadetector.internal.util.FileUtil;
import net.sf.uadetector.internal.util.UrlUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Implementation of a {@link DataStore} which is able to recover <em>UAS data</em> in XML format from a cache file. If
* the cache file is empty, the data will be read from the given data URL.<br>
* <br>
* You can also update the data of the store at any time if you trigger {@link CachingXmlDataStore#refresh()}.
*
* @author André Rouél
*/
public final class CachingXmlDataStore extends AbstractRefreshableDataStore {
/**
* Internal data store which will be used to load previously saved <em>UAS data</em> from a cache file.
*/
private static class CacheFileDataStore extends AbstractDataStore {
protected CacheFileDataStore(final Data data, final DataReader reader, final URL dataUrl, final Charset charset) {
super(data, reader, dataUrl, dataUrl, charset);
}
}
/**
* The default temporary-file directory
*/
private static final String CACHE_DIR = System.getProperty("java.io.tmpdir");
/**
* Corresponding default logger of this class
*/
private static final Logger LOG = LoggerFactory.getLogger(CachingXmlDataStore.class);
/**
* Message for the log if the cache file is filled
*/
private static final String MSG_CACHE_FILE_IS_EMPTY = "The cache file is empty. The given UAS data source will be imported.";
/**
* Message for the log if the cache file is empty
*/
private static final String MSG_CACHE_FILE_IS_FILLED = "The cache file is filled and will be imported.";
/**
* Message if the cache file contains unexpected data and must be deleted manually
*/
private static final String MSG_CACHE_FILE_IS_DAMAGED = "The cache file '%s' is damaged and must be removed manually.";
/**
* Message if the cache file contains unexpected data and has been removed
*/
private static final String MSG_CACHE_FILE_IS_DAMAGED_AND_DELETED = "The cache file '%s' is damaged and has been deleted.";
/**
* The prefix string to be used in generating the cache file's name; must be at least three characters long
*/
private static final String PREFIX = "uas";
/**
* The suffix string to be used in generating the cache file's name; may be {@code null}, in which case the suffix "
* {@code .tmp}" will be used
*/
private static final String SUFFIX = ".xml";
/**
* Constructs a new instance of {@code CachingXmlDataStore} with the given arguments. The given {@code cacheFile}
* can be empty or filled with previously cached data in XML format. The file must be writable otherwise an
* exception will be thrown.
*
* @param fallback
* <em>UAS data</em> as fallback in case the data on the specified resource can not be read correctly
* @return new instance of {@link CachingXmlDataStore}
* @throws net.sf.qualitycheck.exception.IllegalNullArgumentException
* if one of the given arguments is {@code null}
* @throws net.sf.qualitycheck.exception.IllegalStateOfArgumentException
* if the given cache file can not be read
* @throws net.sf.qualitycheck.exception.IllegalStateOfArgumentException
* if no URL can be resolved to the given given file
*/
@Nonnull
public static CachingXmlDataStore createCachingXmlDataStore(@Nonnull final DataStore fallback) {
return createCachingXmlDataStore(findOrCreateCacheFile(), fallback);
}
/**
* Constructs a new instance of {@code CachingXmlDataStore} with the given arguments. The given {@code cacheFile}
* can be empty or filled with previously cached data in XML format. The file must be writable otherwise an
* exception will be thrown.
*
* @param cacheFile
* file with cached <em>UAS data</em> in XML format or empty file
* @param fallback
* <em>UAS data</em> as fallback in case the data on the specified resource can not be read correctly
* @return new instance of {@link CachingXmlDataStore}
* @throws net.sf.qualitycheck.exception.IllegalNullArgumentException
* if one of the given arguments is {@code null}
* @throws net.sf.qualitycheck.exception.IllegalStateOfArgumentException
* if the given cache file can not be read
* @throws net.sf.qualitycheck.exception.IllegalStateOfArgumentException
* if no URL can be resolved to the given given file
*/
@Nonnull
public static CachingXmlDataStore createCachingXmlDataStore(@Nonnull final File cacheFile, @Nonnull final DataStore fallback) {
return createCachingXmlDataStore(cacheFile, UrlUtil.build(DEFAULT_DATA_URL), UrlUtil.build(DEFAULT_VERSION_URL), DEFAULT_CHARSET,
fallback);
}
/**
* Constructs a new instance of {@code CachingXmlDataStore} with the given arguments. The given {@code cacheFile}
* can be empty or filled with previously cached data in XML format. The file must be writable otherwise an
* exception will be thrown.
*
* @param cacheFile
* file with cached <em>UAS data</em> in XML format or empty file
* @param dataUrl
* URL to <em>UAS data</em>
* @param versionUrl
* URL to version information about the given <em>UAS data</em>
* @param charset
* the character set in which the data should be read
* @param fallback
* <em>UAS data</em> as fallback in case the data on the specified resource can not be read correctly
* @return new instance of {@link CachingXmlDataStore}
* @throws net.sf.qualitycheck.exception.IllegalNullArgumentException
* if one of the given arguments is {@code null}
* @throws net.sf.qualitycheck.exception.IllegalNullArgumentException
* if the given cache file can not be read
* @throws net.sf.qualitycheck.exception.IllegalStateOfArgumentException
* if no URL can be resolved to the given given file
*/
@Nonnull
public static CachingXmlDataStore createCachingXmlDataStore(@Nonnull final File cacheFile, @Nonnull final URL dataUrl,
@Nonnull final URL versionUrl, @Nonnull final Charset charset, @Nonnull final DataStore fallback) {
Check.notNull(cacheFile, "cacheFile");
Check.notNull(charset, "charset");
Check.notNull(dataUrl, "dataUrl");
Check.notNull(fallback, "fallback");
Check.notNull(versionUrl, "versionUrl");
final DataReader reader = new XmlDataReader();
final DataStore fallbackDataStore = readCacheFileAsFallback(reader, cacheFile, charset, fallback);
return new CachingXmlDataStore(reader, dataUrl, versionUrl, charset, cacheFile, fallbackDataStore);
}
/**
* Constructs a new instance of {@code CachingXmlDataStore} with the given arguments. The file used to cache the
* read in <em>UAS data</em> will be called from {@link CachingXmlDataStore#findOrCreateCacheFile()}. This file may
* be empty or filled with previously cached data in XML format. The file must be writable otherwise an exception
* will be thrown.
*
* @param dataUrl
* URL to <em>UAS data</em>
* @param versionUrl
* URL to version information about the given <em>UAS data</em>
* @param charset
* the character set in which the data should be read
* @param fallback
* <em>UAS data</em> as fallback in case the data on the specified resource can not be read correctly
* @return new instance of {@link CachingXmlDataStore}
* @throws net.sf.qualitycheck.exception.IllegalNullArgumentException
* if one of the given arguments is {@code null}
* @throws net.sf.qualitycheck.exception.IllegalStateOfArgumentException
* if the given cache file can not be read
*/
@Nonnull
public static CachingXmlDataStore createCachingXmlDataStore(@Nonnull final URL dataUrl, @Nonnull final URL versionUrl,
@Nonnull final Charset charset, @Nonnull final DataStore fallback) {
return createCachingXmlDataStore(findOrCreateCacheFile(), dataUrl, versionUrl, charset, fallback);
}
/**
* Removes the given cache file because it contains damaged content.
*
* @param cacheFile
* cache file to delete
*/
private static void deleteCacheFile(final File cacheFile) {
try {
if (cacheFile.delete()) {
LOG.warn(String.format(MSG_CACHE_FILE_IS_DAMAGED_AND_DELETED, cacheFile.getPath()));
} else {
LOG.warn(String.format(MSG_CACHE_FILE_IS_DAMAGED, cacheFile.getPath()));
}
} catch (final Exception e) {
LOG.warn(String.format(MSG_CACHE_FILE_IS_DAMAGED, cacheFile.getPath()));
}
}
/**
* Gets the cache file for <em>UAS data</em> in the default temporary-file directory. If no cache file exists, a new
* empty file in the default temporary-file directory will be created, using the default prefix and suffix to
* generate its name.
*
* @return file to cache read in <em>UAS data</em>
* @throws net.sf.qualitycheck.exception.IllegalStateOfArgumentException
* if the cache file can not be created
*/
@Nonnull
public static File findOrCreateCacheFile() {
final File file = new File(CACHE_DIR, PREFIX + SUFFIX);
if (!file.exists()) {
try {
file.createNewFile();
} catch (final IOException e) {
throw new IllegalStateOfArgumentException("Can not create a cache file.", e);
}
}
return file;
}
/**
* Checks if the given file is empty.
*
* @param file
* the file that could be empty
* @return {@code true} when the file is accessible and empty otherwise {@code false}
* @throws net.sf.qualitycheck.exception.IllegalStateOfArgumentException
* if an I/O error occurs
*/
private static boolean isEmpty(@Nonnull final File file, @Nonnull final Charset charset) {
try {
return FileUtil.isEmpty(file, charset);
} catch (final IOException e) {
throw new IllegalStateOfArgumentException("The given file could not be read.", e);
}
}
/**
* Tries to read the content of specified cache file and returns them as fallback data store. If the cache file
* contains unexpected data the given fallback data store will be returned instead.
*
* @param reader
* data reader to read the given {@code dataUrl}
* @param cacheFile
* file with cached <em>UAS data</em> in XML format or empty file
* @param versionUrl
* URL to version information about the given <em>UAS data</em>
* @param charset
* the character set in which the data should be read
* @param fallback
* <em>UAS data</em> as fallback in case the data on the specified resource can not be read correctly
* @return a fallback data store
*/
private static DataStore readCacheFileAsFallback(@Nonnull final DataReader reader, @Nonnull final File cacheFile,
@Nonnull final Charset charset, @Nonnull final DataStore fallback) {
DataStore fallbackDataStore;
if (!isEmpty(cacheFile, charset)) {
final URL cacheFileUrl = UrlUtil.toUrl(cacheFile);
try {
fallbackDataStore = new CacheFileDataStore(reader.read(cacheFileUrl, charset), reader, cacheFileUrl, charset);
LOG.debug(MSG_CACHE_FILE_IS_FILLED);
} catch (final RuntimeException e) {
fallbackDataStore = fallback;
deleteCacheFile(cacheFile);
}
} else {
fallbackDataStore = fallback;
LOG.debug(MSG_CACHE_FILE_IS_EMPTY);
}
return fallbackDataStore;
}
/**
* Constructs an {@code CachingXmlDataStore} with the given arguments.
*
* @param data
* first <em>UAS data</em> which will be available in the store
* @param reader
* data reader to read the given {@code dataUrl}
* @param dataUrl
* URL to <em>UAS data</em>
* @param versionUrl
* URL to version information about the given <em>UAS data</em>
* @param charset
* the character set in which the data should be read
* @param cacheFile
* file with cached <em>UAS data</em> in XML format or an empty file
* @throws net.sf.qualitycheck.exception.IllegalNullArgumentException
* if one of the given arguments is {@code null}
*/
private CachingXmlDataStore(@Nonnull final DataReader reader, @Nonnull final URL dataUrl, @Nonnull final URL versionUrl,
@Nonnull final Charset charset, @Nonnull final File cacheFile, @Nonnull final DataStore fallback) {
super(reader, dataUrl, versionUrl, charset, fallback);
setUpdateOperation(new UpdateOperationWithCacheFileTask(this, cacheFile));
}
}