/** * Copyright (C) 2012 52°North Initiative for Geospatial Open Source Software GmbH * * 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 org.n52.sos.cache; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.nio.file.Files; import java.nio.file.StandardCopyOption; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.Map; import java.util.Scanner; import org.n52.sos.db.AccessGDB; import org.n52.util.CommonUtilities; import org.n52.util.logging.Logger; public abstract class AbstractEntityCache<T extends CacheEntity> { public Logger LOGGER = Logger.getLogger(AbstractEntityCache.class.getName()); private File cacheFile; private Object cacheFileMutex = new Object(); private long lastUpdateDuration; private int maximumEntries; private int latestEntryIndex; private String dbName; public AbstractEntityCache(String dbName) throws FileNotFoundException { this.dbName = dbName; initializeCacheFile(); } protected void initializeCacheFile() throws FileNotFoundException { File baseDir = CommonUtilities.resolveCacheBaseDir(dbName); synchronized (cacheFileMutex) { this.cacheFile = new File(baseDir, getCacheFileName()); if (this.cacheFile == null) { throw new FileNotFoundException("cache file "+ getCacheFileName() +" not found."); } if (!this.cacheFile.exists()) { try { this.cacheFile.createNewFile(); } catch (IOException e) { throw new FileNotFoundException("Could not create cache file "+ getCacheFileName()); } } } } protected abstract String getCacheFileName(); protected abstract String serializeEntity(T entity) throws CacheException; protected abstract T deserializeEntity(String line); protected abstract Collection<T> getCollectionFromDAO(AccessGDB geoDB) throws IOException; protected abstract AbstractEntityCache<T> getSingleInstance(); public void storeTemporaryEntity(T et) { FileOutputStream fs = null; synchronized (cacheFileMutex) { try { fs = new FileOutputStream(getTempCacheFile(), true); storeEntity(et.getItemId(), et, fs); } catch (FileNotFoundException | CacheException e) { LOGGER.warn(e.getMessage(), e); } finally { try { if (fs != null) { fs.close(); } } catch (IOException e) { LOGGER.warn(e.getMessage(), e); } } } } protected void clearTempCacheFile() throws IOException { synchronized (cacheFileMutex) { File f = getTempCacheFile(); f.delete(); f.createNewFile(); } } protected void setMaximumEntries(int c) { this.maximumEntries = c; } protected void setLatestEntryIndex(int currentOfferingIndex) { this.latestEntryIndex = currentOfferingIndex; } public synchronized void storeEntity(String id, T entity, FileOutputStream fos) throws CacheException { StringBuilder sb = new StringBuilder(); sb.append(id); sb.append("="); sb.append(serializeEntity(entity)); sb.append(System.getProperty("line.separator")); try { fos.write(sb.toString().getBytes()); fos.flush(); } catch (IOException e) { throw new CacheException(e); } } protected Map<String, T> deserializeEntityCollection( InputStream fis) { Map<String, T> result = new HashMap<>(); Scanner sc = new Scanner(fis); String line; while (sc.hasNext()) { line = sc.nextLine(); String id = line.substring(0, line.indexOf("=")); result.put(id, deserializeEntity(line.substring(line.indexOf("=")+1, line.length()))); } sc.close(); return result; } public synchronized void storeEntityCollection(Collection<T> entities) throws CacheException { Map<String, T> result = new HashMap<>(); for (T t : entities) { result.put(t.getItemId(), t); } storeEntityCollection(result); } public synchronized void storeEntityCollection(Map<String, T> entities) throws CacheException { File tempCacheFile = getTempCacheFile(); if (entities.isEmpty()) { LOGGER.info("reloading entities from cache file"); try { entities = deserializeEntityCollection(new FileInputStream(tempCacheFile)); } catch (FileNotFoundException e) { LOGGER.warn("could not access the temp cache file contents", e); } } if (mergeWithPreviousEntries()) { mergePreviousEntries(entities, cacheFile); } if (entities.size() > 0) { FileOutputStream fileStream; try { fileStream = new FileOutputStream(tempCacheFile); } catch (FileNotFoundException e) { throw new CacheException(e); } LOGGER.info("storing cache to temporary file "+ tempCacheFile.getAbsolutePath()); for (String id : entities.keySet()) { storeEntity(id, entities.get(id), fileStream); } try { fileStream.close(); } catch (IOException e) { throw new CacheException(e); } fileStream = null; synchronized (cacheFileMutex) { try { LOGGER.info("replacing target cache file "+ cacheFile.getAbsolutePath()); Files.copy(tempCacheFile.toPath(), this.cacheFile.toPath(), StandardCopyOption.REPLACE_EXISTING); tempCacheFile.delete(); } catch (IOException e) { throw new CacheException(e); } } } } private void mergePreviousEntries(Map<String, T> entities, File fileToMerge) { try { Map<String, T> oldEntries = deserializeEntityCollection(new FileInputStream(fileToMerge)); for (String key : oldEntries.keySet()) { if (!entities.containsKey(key)) { entities.put(key, oldEntries.get(key)); } } } catch (FileNotFoundException e) { LOGGER.warn("Could not read previous cache file", e); } } protected boolean mergeWithPreviousEntries() { return false; } private File getTempCacheFile() { synchronized (cacheFileMutex) { return new File(this.cacheFile.getParent(), getCacheFileName()+".tmp"); } } public Map<String, T> getEntityCollection(AccessGDB geoDB) throws CacheException, CacheNotYetAvailableException { LOGGER.info("getEntityCollection for cache "+getClass().getSimpleName()); synchronized (cacheFileMutex) { if (this.cacheFile == null || !(this.isCacheAvailable() && this.hasCacheContent())) { if (geoDB != null) { try { initializeCacheFile(); scheduleCacheUpdate(); } catch (IOException e) { throw new CacheException(e); } } else { throw new CacheException("Could not access or create the cache file. AccessGDB not available! "+getCacheFileName()); } } try { LOGGER.info("Returning data from cache file..."); return deserializeCacheFile(); } catch (IOException e) { throw new CacheException(e); } } } private void scheduleCacheUpdate() { AbstractCacheScheduler.Instance.instance().forceUpdate(); } private Map<String, T> deserializeCacheFile() throws IOException, CacheNotYetAvailableException { synchronized (cacheFileMutex) { FileInputStream fis; if (hasCacheContent()) { fis = new FileInputStream(this.cacheFile); } else if (hasCacheContent(getTempCacheFile())) { fis = new FileInputStream(getTempCacheFile()); } else { throw new CacheNotYetAvailableException(); } return deserializeEntityCollection(fis); } } protected String readStreamContent(InputStream is) { Scanner sc = new Scanner(is); StringBuilder sb = new StringBuilder(); while (sc.hasNext()) { sb.append(sc.nextLine()); sb.append(System.getProperty("line.separator")); } sc.close(); return sb.toString(); } protected String[] decodeStringArray(String string) { String trimmed = string.substring(1, string.length() - 1); String[] splitted = trimmed.split(", "); return splitted; } public boolean isCacheAvailable() { synchronized (cacheFileMutex) { return cacheFile.exists(); } } public boolean hasCacheContent() { return hasCacheContent(cacheFile); } private boolean hasCacheContent(File f) { synchronized (cacheFileMutex) { return f.exists() && f.length() > 0; } } public long lastUpdated() { synchronized (cacheFileMutex) { if (isCacheAvailable() && hasCacheContent()) { return cacheFile.lastModified(); } } return 0; } public long getLastUpdateDuration() { return lastUpdateDuration; } public int getMaximumEntries() { return maximumEntries; } public int getLatestEntryIndex() { return latestEntryIndex; } // public boolean requestUpdateLock() { // File f = getCacheLockFile(); // // if (f != null && f.exists()) { // return false; // } // // try { // f.createNewFile(); // return true; // } catch (IOException e) { // LOGGER.warn(e.getMessage(), e); // LOGGER.warn("Could not access cache lock file."); // } // // return false; // } // // public void freeUpdateLock() throws FileNotFoundException { // File f = getCacheLockFile(); // // if (f != null && f.exists()) { // f.delete(); // } // } public boolean isUpdateOngoing() { File f = getCacheLockFile(); if (f.exists()) { return true; } return false; } private File getCacheLockFile() { synchronized (cacheFileMutex) { return new File(this.cacheFile.getParent(), this.cacheFile.getName()+".lock"); } } public void updateCache(AccessGDB geoDB) throws CacheException, IOException { AbstractEntityCache<T> instance = getSingleInstance(); LOGGER.info("Getting DAO data for "+ this.getClass().getSimpleName()); long start = System.currentTimeMillis(); try { Collection<T> entities = getCollectionFromDAO(geoDB); instance.storeEntityCollection(entities); } catch (IOException e) { LOGGER.warn("Cache instance update error: ", e); instance.storeEntityCollection(new ArrayList<T>(0)); } this.lastUpdateDuration = System.currentTimeMillis() - start; LOGGER.info("Update for "+ this.getClass().getSimpleName() +" took ms: "+this.lastUpdateDuration); } public boolean requiresUpdate() { if (this.isCacheAvailable()) { if (this.hasCacheContent()) { long lastUpdated = this.getSingleInstance().lastUpdated(); if (System.currentTimeMillis() - lastUpdated > AbstractCacheScheduler.FIFTEEN_MINS_MS) { return true; } } else { return true; } } return false; } public abstract void cancelCurrentExecution(); }