/* * Password Management Servlets (PWM) * http://www.pwm-project.org * * Copyright (c) 2006-2009 Novell, Inc. * Copyright (c) 2009-2017 The PWM Project * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ package password.pwm.util.localdb; import password.pwm.PwmApplication; import password.pwm.error.ErrorInformation; import password.pwm.error.PwmError; import password.pwm.svc.stats.Statistic; import password.pwm.util.java.TimeDuration; import password.pwm.util.logging.PwmLogger; import java.io.File; import java.io.Serializable; import java.util.Collection; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; public class LocalDBAdaptor implements LocalDB { private static final PwmLogger LOGGER = PwmLogger.forClass(LocalDBAdaptor.class); private final LocalDBProvider innerDB; private final SizeCacheManager SIZE_CACHE_MANAGER; private final PwmApplication pwmApplication; LocalDBAdaptor(final LocalDBProvider innerDB, final PwmApplication pwmApplication) { this.pwmApplication = pwmApplication; if (innerDB == null) { throw new IllegalArgumentException("innerDB can not be null"); } if (innerDB.flags().contains(LocalDBProvider.Flag.SlowSizeOperations)) { SIZE_CACHE_MANAGER = new SizeCacheManager(); } else { SIZE_CACHE_MANAGER = null; } this.innerDB = innerDB; } public File getFileLocation() { return innerDB.getFileLocation(); } @WriteOperation public void close() throws LocalDBException { innerDB.close(); } public boolean contains(final DB db, final String key) throws LocalDBException { ParameterValidator.validateDBValue(db); ParameterValidator.validateKeyValue(key); final boolean value = innerDB.contains(db, key); markRead(1); return value; } public String get(final DB db, final String key) throws LocalDBException { ParameterValidator.validateDBValue(db); ParameterValidator.validateKeyValue(key); final String value = innerDB.get(db, key); markRead(1); return value; } @WriteOperation public void init(final File dbDirectory, final Map<String, String> initParameters, final Map<LocalDBProvider.Parameter,String> parameters) throws LocalDBException { innerDB.init(dbDirectory, initParameters, parameters); } public LocalDBIterator<String> iterator(final DB db) throws LocalDBException { ParameterValidator.validateDBValue(db); final LocalDBIterator<String> innerIterator = innerDB.iterator(db); return new SizeIterator<String>(db, innerIterator); } private class SizeIterator<K> implements LocalDBIterator<String> { private final LocalDBIterator<String> innerIterator; private final DB db; private String key; SizeIterator(final DB db, final LocalDBIterator<String> innerIterator) { this.innerIterator = innerIterator; this.db = db; } public boolean hasNext() { return innerIterator.hasNext(); } public String next() { key = innerIterator.next(); return key; } public void remove() { innerIterator.remove(); if (SIZE_CACHE_MANAGER != null) { try { SIZE_CACHE_MANAGER.decrementSize(db); } catch (Exception e) { throw new RuntimeException(e); } } } @Override public void close() { innerIterator.close(); } } public Map<String,Serializable> debugInfo() { return innerDB.debugInfo(); } @WriteOperation public void putAll(final DB db, final Map<String, String> keyValueMap) throws LocalDBException { ParameterValidator.validateDBValue(db); for (final String loopKey : keyValueMap.keySet()) { try { ParameterValidator.validateKeyValue(loopKey); ParameterValidator.validateValueValue(keyValueMap.get(loopKey)); } catch (NullPointerException e) { throw new NullPointerException(e.getMessage() + " for transaction record: '" + loopKey + "'"); } catch (IllegalArgumentException e) { throw new IllegalArgumentException(e.getMessage() + " for transaction record: '" + loopKey + "'"); } } try { innerDB.putAll(db, keyValueMap); } finally { if (SIZE_CACHE_MANAGER != null) { SIZE_CACHE_MANAGER.clearSize(db); } } markWrite(keyValueMap.size()); } @WriteOperation public boolean put(final DB db, final String key, final String value) throws LocalDBException { ParameterValidator.validateDBValue(db); ParameterValidator.validateKeyValue(key); ParameterValidator.validateValueValue(value); final boolean preExisting = innerDB.put(db, key, value); if (!preExisting) { if (SIZE_CACHE_MANAGER != null) { SIZE_CACHE_MANAGER.incrementSize(db); } } markWrite(1); return preExisting; } @WriteOperation public boolean remove(final DB db, final String key) throws LocalDBException { ParameterValidator.validateDBValue(db); ParameterValidator.validateKeyValue(key); final boolean result = innerDB.remove(db, key); if (result) { if (SIZE_CACHE_MANAGER != null) { SIZE_CACHE_MANAGER.decrementSize(db); } } markWrite(1); return result; } @WriteOperation public void removeAll(final DB db, final Collection<String> keys) throws LocalDBException { ParameterValidator.validateDBValue(db); for (final String loopKey : keys) { try { ParameterValidator.validateValueValue(loopKey); } catch (NullPointerException e) { throw new NullPointerException(e.getMessage() + " for transaction record: '" + loopKey + "'"); } catch (IllegalArgumentException e) { throw new IllegalArgumentException(e.getMessage() + " for transaction record: '" + loopKey + "'"); } } if (keys.size() > 1) { try { innerDB.removeAll(db, keys); } finally { if (SIZE_CACHE_MANAGER != null) { SIZE_CACHE_MANAGER.clearSize(db); } } } else { for (final String key : keys) { remove(db,key); } } markWrite(keys.size()); } public int size(final DB db) throws LocalDBException { ParameterValidator.validateDBValue(db); if (SIZE_CACHE_MANAGER != null) { return SIZE_CACHE_MANAGER.getSizeForDB(db, innerDB); } else { return innerDB.size(db); } } @WriteOperation public void truncate(final DB db) throws LocalDBException { ParameterValidator.validateDBValue(db); try { innerDB.truncate(db); } finally { if (SIZE_CACHE_MANAGER != null) { SIZE_CACHE_MANAGER.clearSize(db); } } } public Status status() { if (innerDB == null) { return Status.CLOSED; } return innerDB.getStatus(); } private static class SizeCacheManager { private static final Integer CACHE_DIRTY = -1; private static final Integer CACHE_WORKING = -2; private final ConcurrentMap<DB, Integer> sizeCache = new ConcurrentHashMap<>(); private SizeCacheManager() { for (final DB db : DB.values()) { sizeCache.put(db, CACHE_DIRTY); } } private void incrementSize(final DB db) { modifySize(db, +1); } private void decrementSize(final DB db) { modifySize(db, -1); } private void modifySize(final DB db, final int amount) { // retrieve the current cache size. final Integer cachedSize = sizeCache.get(db); //update the cached value only if there is a meaningful value cached. if (cachedSize >= 0) { // calculate the new value final int newSize = cachedSize + amount; // replace the cached value with the new value, only if it hasn't been touched by another thread since it was // retrieved from the cache a few lines ago. if (!sizeCache.replace(db, cachedSize, newSize)) { // the cache was modified by some other thread, and so is no longer accurate. Mark it dirty. clearSize(db); } } } private void clearSize(final DB db) { sizeCache.put(db, CACHE_DIRTY); } private int getSizeForDB(final DB db, final LocalDBProvider localDBProvider) throws LocalDBException { // read the cached size out of the cache store final Integer cachedSize = sizeCache.get(db); if (cachedSize != null && cachedSize >= 0) { // if there is a good cache value and its not dirty (-1) or being populated by another thread (-2) return cachedSize; } final long beginTime = System.currentTimeMillis(); // mark the cache as population in progress sizeCache.put(db, CACHE_WORKING); // read the "real" value. this is the line that might take a long time final int theSize = localDBProvider.size(db); final TimeDuration timeDuration = TimeDuration.fromCurrent(beginTime); // so long as nothing else has touched the cache (perhaps another thread populated it, or someone else marked it dirty, then // go ahead and update it. final boolean savedInCache = sizeCache.replace(db, CACHE_WORKING, theSize); final StringBuilder debugMsg = new StringBuilder(); debugMsg.append("performed real size lookup of ").append(theSize).append(" for ").append(db); debugMsg.append(": ").append(timeDuration.asCompactString()); debugMsg.append(savedInCache ? ", cached" : ", not cached"); LOGGER.debug(debugMsg); return theSize; } } private static class ParameterValidator { private static void validateDBValue(final LocalDB.DB db) { if (db == null) { throw new NullPointerException("db cannot be null"); } } private static void validateKeyValue(final String key) throws LocalDBException { if (key == null) { throw new NullPointerException("key cannot be null"); } if (key.length() < 0) { throw new LocalDBException(new ErrorInformation(PwmError.ERROR_UNKNOWN,"key length cannot be zero length")); } if (key.length() > LocalDB.MAX_KEY_LENGTH) { throw new LocalDBException(new ErrorInformation(PwmError.ERROR_UNKNOWN,"key length " + key.length() + " is greater than max " + LocalDB.MAX_KEY_LENGTH)); } } private static void validateValueValue(final String value) throws LocalDBException { if (value == null) { throw new NullPointerException("value cannot be null"); } if (value.length() > LocalDB.MAX_VALUE_LENGTH) { throw new LocalDBException(new ErrorInformation(PwmError.ERROR_UNKNOWN,"value length " + value.length() + " is greater than max " + LocalDB.MAX_VALUE_LENGTH)); } } } private void markRead(final int events) { if (pwmApplication != null) { if (pwmApplication.getStatisticsManager() != null) { pwmApplication.getStatisticsManager().updateEps(Statistic.EpsType.PWMDB_READS,events); } } } private void markWrite(final int events) { if (pwmApplication != null) { if (pwmApplication.getStatisticsManager() != null) { pwmApplication.getStatisticsManager().updateEps(Statistic.EpsType.PWMDB_WRITES,events); } } } }