// Copyright 2013 Michel Kraemer // // 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 de.undercouch.citeproc.helper.tool; import java.io.File; import java.io.IOException; import java.io.StringReader; import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Set; import org.mapdb.DB; import org.mapdb.DBMaker; import de.undercouch.citeproc.csl.CSLItemData; import de.undercouch.citeproc.helper.json.JsonLexer; import de.undercouch.citeproc.helper.json.JsonParser; import de.undercouch.citeproc.helper.json.StringJsonBuilderFactory; import de.undercouch.citeproc.remote.RemoteConnector; import de.undercouch.citeproc.remote.RemoteConnectorAdapter; /** * A {@link de.undercouch.citeproc.remote.RemoteConnector} that caches * items read from the server. * @author Michel Kraemer */ public class CachingRemoteConnector extends RemoteConnectorAdapter { private final DB _db; private final Set<String> _itemIds; private final Map<String, String> _items; private boolean _transaction = false; /** * Creates a connector that caches items read from the server * @param delegate the underlying connector * @param cacheFile a file used to cache items */ public CachingRemoteConnector(RemoteConnector delegate, File cacheFile) { super(delegate); DB db = null; Set<String> itemIds = null; Map<String, String> items = null; int retry = 2; while (retry >= 1) { try { DBMaker<?> dbMaker = DBMaker.newFileDB(cacheFile); dbMaker.closeOnJvmShutdown(); db = dbMaker.make(); itemIds = db.getTreeSet("itemIds"); items = db.getHashMap("items"); break; } catch (Throwable e) { --retry; if (retry == 1 && cacheFile.exists()) { //unable to open disk cache. remove it and try again. try { //close db first if (db != null) { db.close(); db = null; } } catch (Throwable t) { //ignore } cacheFile.delete(); continue; } //disk cache is not available. use in-memory cache db = null; itemIds = new HashSet<>(); items = new HashMap<>(); break; } } _db = db; _itemIds = itemIds; _items = items; } @Override public List<String> getItemIDs() throws IOException { if (!_itemIds.isEmpty()) { return new ArrayList<>(_itemIds); } List<String> ids = super.getItemIDs(); try { _itemIds.addAll(ids); commit(); } catch (RuntimeException e) { rollback(); throw e; } return ids; } @Override public CSLItemData getItem(String itemId) throws IOException { String item = _items.get(itemId); CSLItemData itemData; if (item == null) { itemData = super.getItem(itemId); item = (String)itemData.toJson(new StringJsonBuilderFactory().createJsonBuilder()); try { _items.put(itemId, item); commit(); } catch (RuntimeException e) { rollback(); throw e; } } else { Map<String, Object> m = new JsonParser( new JsonLexer(new StringReader(item))).parseObject(); itemData = CSLItemData.fromJson(m); } return itemData; } @Override public Map<String, CSLItemData> getItems(List<String> itemIds) throws IOException { Map<String, CSLItemData> result = new LinkedHashMap<>(itemIds.size()); List<String> unknownIds = new ArrayList<>(); //load items from cache for (String id : itemIds) { String item = _items.get(id); if (item == null) { unknownIds.add(id); } else { Map<String, Object> m = new JsonParser( new JsonLexer(new StringReader(item))).parseObject(); result.put(id, CSLItemData.fromJson(m)); } } //load items which are not in the cache yet from remote if (!unknownIds.isEmpty()) { Map<String, CSLItemData> newItems = super.getItems(unknownIds); try { for (Map.Entry<String, CSLItemData> e : newItems.entrySet()) { String s = (String)e.getValue().toJson( new StringJsonBuilderFactory().createJsonBuilder()); _items.put(e.getKey(), s); } commit(); } catch (RuntimeException e) { rollback(); throw e; } result.putAll(newItems); } return result; } /** * Checks if the cache contains an item with the given ID * @param itemId the item ID * @return true if the cache contains such an item, false otherwise */ public boolean containsItemId(String itemId) { return _items.containsKey(itemId); } /** * @return true if the cache contains a list of item IDs, false * if the cache is empty */ public boolean hasItemList() { return !_itemIds.isEmpty(); } /** * Clears the cache */ public void clear() { _itemIds.clear(); _items.clear(); } /** * Starts a transaction on the cache. Cached entries will not * be written to disk until {@link #commitTransaction()} is called. */ public void beginTransaction() { _transaction = true; } /** * Ends a transaction. Does not flush to disk. */ public void endTransaction() { _transaction = false; } /** * Flushes cached entries to disk, but does not end transaction */ public void commitTransaction() { commit(true); } /** * Commit database transaction * @see #commit(boolean) */ private void commit() { commit(false); } /** * Commit database transaction. This method is a NOOP if there is no * database or if {@link #_transaction} is currently true. * @param force true if the database transaction should be committed * regardless of the {@link #_transaction} flag. */ private void commit(boolean force) { if (_db == null) { return; } if (_transaction && !force) { return; } try { _db.commit(); } catch (Exception e) { throw new IllegalStateException("Could not commit transaction", e); } } /** * Roll back database transaction. This method is a NOOP if there is no database. */ private void rollback() { if (_db == null) { return; } try { _db.rollback(); } catch (Exception e) { throw new IllegalStateException("Could not roll back transaction", e); } } }