UpdateOperationWithCacheFileTask.java

/*******************************************************************************
 * Copyright 2013 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.FileOutputStream;
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.uadetector.exception.CanNotOpenStreamException;
import net.sf.uadetector.internal.data.Data;
import net.sf.uadetector.internal.util.Closeables;
import net.sf.uadetector.internal.util.FileUtil;
import net.sf.uadetector.internal.util.UrlUtil;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

final class UpdateOperationWithCacheFileTask extends AbstractUpdateOperation {

	/**
	 * Corresponding default logger of this class
	 */
	private static final Logger LOG = LoggerFactory.getLogger(UpdateOperationWithCacheFileTask.class);

	/**
	 * Message for the log when issues occur during reading of or writing to the cache file.
	 */
	private static final String MSG_CACHE_FILE_ISSUES = "Issues occured during reading of or writing to the cache file: %s";

	/**
	 * Message for the log if the passed resources are the same and an update makes no sense
	 */
	private static final String MSG_SAME_RESOURCES = "The passed URL and file resources are the same. An update was not performed.";

	/**
	 * Creates a temporary file near the passed file. The name of the given one will be used and the suffix ".temp" will
	 * be added.
	 * 
	 * @param file
	 *            file in which the entire contents from the given URL can be saved
	 * @throws IllegalStateException
	 *             if the file can not be deleted
	 */
	protected static File createTemporaryFile(@Nonnull final File file) {
		Check.notNull(file, "file");

		final File tempFile = new File(file.getParent(), file.getName() + ".temp");

		// remove orphaned temporary file
		deleteFile(tempFile);

		return tempFile;
	}

	/**
	 * Removes the given file.
	 * 
	 * @param file
	 *            a file which should be deleted
	 * 
	 * @throws net.sf.qualitycheck.exception.IllegalNullArgumentException
	 *             if the given argument is {@code null}
	 * @throws net.sf.qualitycheck.exception.IllegalStateOfArgumentException
	 *             if the file can not be deleted
	 */
	protected static void deleteFile(@Nonnull final File file) {
		Check.notNull(file, "file");
		Check.stateIsTrue(!file.exists() || file.delete(), "Cannot delete file '%s'.", file.getPath());
	}

	/**
	 * 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 IllegalStateException
	 *             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 IllegalStateException("The given file could not be read.");
		}
	}

	/**
	 * Checks that {@code older} {@link Data} has a lower version number than the {@code newer} one.
	 * 
	 * @param older
	 *            possibly older {@code Data}
	 * @param newer
	 *            possibly newer {@code Data}
	 * @return {@code true} if the {@code newer} Data is really newer, otherwise {@code false}
	 */
	protected static boolean isNewerData(@Nonnull final Data older, @Nonnull final Data newer) {
		return newer.getVersion().compareTo(older.getVersion()) > 0;
	}

	/**
	 * Reads the content from the given {@link URL} and saves it to the passed file.
	 * 
	 * @param file
	 *            file in which the entire contents from the given URL can be saved
	 * @param store
	 *            a data store for <em>UAS data</em>
	 * @throws net.sf.qualitycheck.exception.IllegalNullArgumentException
	 *             if any of the passed arguments is {@code null}
	 * @throws IOException
	 *             if an I/O error occurs
	 */
	protected static void readAndSave(@Nonnull final File file, @Nonnull final DataStore store) throws IOException {
		Check.notNull(file, "file");
		Check.notNull(store, "store");

		final URL url = store.getDataUrl();
		final Charset charset = store.getCharset();

		final boolean isEqual = url.toExternalForm().equals(UrlUtil.toUrl(file).toExternalForm());
		if (!isEqual) {

			// check if the data can be read in successfully
			final String data = UrlUtil.read(url, charset);
			if (Data.EMPTY.equals(store.getDataReader().read(data))) {
				throw new IllegalStateException("The read in content can not be transformed to an instance of 'Data'.");
			}

			final File tempFile = createTemporaryFile(file);

			FileOutputStream outputStream = null;
			boolean threw = true;
			try {
				// write data to temporary file
				outputStream = new FileOutputStream(tempFile);
				outputStream.write(data.getBytes(charset));

				// delete the original file
				deleteFile(file);

				threw = false;
			} finally {
				Closeables.close(outputStream, threw);
			}

			// rename the new file to the original one
			renameFile(tempFile, file);
		} else {
			LOG.debug(MSG_SAME_RESOURCES);
		}
	}

	/**
	 * Renames the given file {@code from} to the new file {@code to}.
	 * 
	 * @param from
	 *            an existing file
	 * @param to
	 *            a new file
	 * 
	 * @throws net.sf.qualitycheck.exception.IllegalNullArgumentException
	 *             if one of the given arguments is {@code null}
	 * @throws net.sf.qualitycheck.exception.IllegalStateOfArgumentException
	 *             if the file can not be renamed
	 */
	protected static void renameFile(@Nonnull final File from, @Nonnull final File to) {
		Check.notNull(from, "from");
		Check.stateIsTrue(from.exists(), "Argument 'from' must not be an existing file.");
		Check.notNull(to, "to");
		Check.stateIsTrue(from.renameTo(to), "Renaming file from '%s' to '%s' failed.", from.getAbsolutePath(), to.getAbsolutePath());
	}

	/**
	 * File to cache read in <em>UAS data</em>
	 */
	private final File cacheFile;

	/**
	 * The data store for instances that implements {@link net.sf.uadetector.internal.data.Data}
	 */
	private final AbstractRefreshableDataStore store;

	public UpdateOperationWithCacheFileTask(@Nonnull final AbstractRefreshableDataStore dataStore, @Nonnull final File cacheFile) {
		super(dataStore);
		Check.notNull(dataStore, "dataStore");
		Check.notNull(cacheFile, "cacheFile");
		store = dataStore;
		this.cacheFile = cacheFile;
	}

	@Override
	public void call() {
		readDataIfNewerAvailable();
	}

	private boolean isCacheFileEmpty() {
		return isEmpty(cacheFile, store.getCharset());
	}

	private void readDataIfNewerAvailable() {
		try {
			if (isUpdateAvailable() || isCacheFileEmpty()) {
				readAndSave(cacheFile, store);
				store.setData(store.getDataReader().read(cacheFile.toURI().toURL(), store.getCharset()));
			}
		} catch (final CanNotOpenStreamException e) {
			LOG.warn(String.format(RefreshableDataStore.MSG_URL_NOT_READABLE, e.getLocalizedMessage()));
			readFallbackData();
		} catch (final RuntimeException e) {
			LOG.warn(RefreshableDataStore.MSG_FAULTY_CONTENT, e);
			readFallbackData();
		} catch (final IOException e) {
			LOG.warn(String.format(MSG_CACHE_FILE_ISSUES, e.getLocalizedMessage()), e);
			readFallbackData();
		}
	}

	private void readFallbackData() {
		LOG.info("Reading fallback data...");
		try {
			if (isCacheFileEmpty()) {
				readAndSave(cacheFile, store.getFallback());
				final Data data = store.getDataReader().read(cacheFile.toURI().toURL(), store.getCharset());
				if (isNewerData(store.getData(), data)) {
					store.setData(data);
				}
			}
		} catch (final CanNotOpenStreamException e) {
			LOG.warn(String.format(RefreshableDataStore.MSG_URL_NOT_READABLE, e.getLocalizedMessage()));
		} catch (final RuntimeException e) {
			LOG.warn(RefreshableDataStore.MSG_FAULTY_CONTENT, e);
		} catch (final IOException e) {
			LOG.warn(String.format(MSG_CACHE_FILE_ISSUES, e.getLocalizedMessage()), e);
		}
	}

}