123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484 |
- /*
- * 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 jetbrains.exodus.ArrayByteIterable;
- import jetbrains.exodus.ByteIterable;
- import jetbrains.exodus.InvalidSettingException;
- import jetbrains.exodus.bindings.StringBinding;
- import jetbrains.exodus.env.Cursor;
- import jetbrains.exodus.env.Environment;
- import jetbrains.exodus.env.EnvironmentConfig;
- import jetbrains.exodus.env.EnvironmentStatistics;
- import jetbrains.exodus.env.Environments;
- import jetbrains.exodus.env.Store;
- import jetbrains.exodus.env.StoreConfig;
- import jetbrains.exodus.env.Transaction;
- import jetbrains.exodus.management.Statistics;
- import jetbrains.exodus.management.StatisticsItem;
- import password.pwm.error.ErrorInformation;
- import password.pwm.error.PwmError;
- import password.pwm.util.java.ConditionalTaskExecutor;
- import password.pwm.util.java.JsonUtil;
- import password.pwm.util.java.StringUtil;
- import password.pwm.util.java.TimeDuration;
- import password.pwm.util.logging.PwmLogger;
- import java.io.ByteArrayOutputStream;
- import java.io.File;
- import java.io.IOException;
- import java.io.Serializable;
- import java.time.Instant;
- import java.util.Collection;
- import java.util.Collections;
- import java.util.Date;
- import java.util.HashMap;
- import java.util.LinkedHashMap;
- import java.util.Map;
- import java.util.Set;
- import java.util.concurrent.TimeUnit;
- import java.util.zip.Deflater;
- import java.util.zip.DeflaterOutputStream;
- import java.util.zip.Inflater;
- import java.util.zip.InflaterOutputStream;
- public class Xodus_LocalDB implements LocalDBProvider {
- private static final PwmLogger LOGGER = PwmLogger.forClass(Xodus_LocalDB.class);
- private static final TimeDuration STATS_OUTPUT_INTERVAL = new TimeDuration(24, TimeUnit.HOURS);
- private Environment environment;
- private File fileLocation;
- private boolean readOnly;
- private enum Property {
- Compression_Enabled("xodus.compression.enabled"),
- Compression_MinLength("xodus.compression.minLength"),
- ;
- private final String keyName;
- Property(final String keyName) {
- this.keyName = keyName;
- }
- public String getKeyName() {
- return keyName;
- }
- }
- private LocalDB.Status status = LocalDB.Status.NEW;
- private final Map<LocalDB.DB,Store> cachedStoreObjects = new HashMap<>();
- private final ConditionalTaskExecutor outputLogExecutor = new ConditionalTaskExecutor(
- () -> outputStats(),new ConditionalTaskExecutor.TimeDurationPredicate(STATS_OUTPUT_INTERVAL).setNextTimeFromNow(1, TimeUnit.MINUTES)
- );
- private BindMachine bindMachine = new BindMachine(BindMachine.DEFAULT_ENABLE_COMPRESSION, BindMachine.DEFAULT_MIN_COMPRESSION_LENGTH);
- @Override
- public void init(
- final File dbDirectory,
- final Map<String, String> initParameters,
- final Map<Parameter,String> parameters
- )
- throws LocalDBException
- {
- this.fileLocation = dbDirectory;
- LOGGER.trace("begin environment open");
- final Instant startTime = Instant.now();
- final EnvironmentConfig environmentConfig = makeEnvironmentConfig(initParameters);
- {
- final boolean compressionEnabled = initParameters.containsKey(Property.Compression_Enabled.getKeyName())
- ? Boolean.parseBoolean(initParameters.get(Property.Compression_Enabled.getKeyName()))
- : BindMachine.DEFAULT_ENABLE_COMPRESSION;
- final int compressionMinLength = initParameters.containsKey(Property.Compression_MinLength.getKeyName())
- ? Integer.parseInt(initParameters.get(Property.Compression_MinLength.getKeyName()))
- : BindMachine.DEFAULT_MIN_COMPRESSION_LENGTH;
- bindMachine = new BindMachine(compressionEnabled, compressionMinLength);
- }
- readOnly = parameters.containsKey(Parameter.readOnly) && Boolean.parseBoolean(parameters.get(Parameter.readOnly));
- LOGGER.trace("preparing to open with configuration " + JsonUtil.serializeMap(environmentConfig.getSettings()));
- environment = Environments.newInstance(dbDirectory.getAbsolutePath() + File.separator + "xodus", environmentConfig);
- LOGGER.trace("environment open (" + TimeDuration.fromCurrent(startTime).asCompactString() + ")");
- environment.executeInTransaction(txn -> {
- for (final LocalDB.DB db : LocalDB.DB.values()) {
- final Store store = initStore(db, txn);
- cachedStoreObjects.put(db,store);
- }
- });
- status = LocalDB.Status.OPEN;
- for (final LocalDB.DB db : LocalDB.DB.values()) {
- LOGGER.trace("opened " + db + " with " + this.size(db) + " records");
- }
- }
- @Override
- public void close() throws LocalDBException {
- if (environment != null && environment.isOpen()) {
- environment.close();
- }
- status = LocalDB.Status.CLOSED;
- LOGGER.debug("closed");
- }
- private EnvironmentConfig makeEnvironmentConfig(final Map<String, String> initParameters) {
- final EnvironmentConfig environmentConfig = new EnvironmentConfig();
- environmentConfig.setEnvCloseForcedly(true);
- environmentConfig.setMemoryUsage(50 * 1024 * 1024);
- for (final String key : initParameters.keySet()) {
- final String value = initParameters.get(key);
- final Map<String,String> singleMap = Collections.singletonMap(key,value);
- try {
- environmentConfig.setSettings(singleMap);
- LOGGER.trace("set env setting from appProperty: " + key + "=" + value);
- } catch (InvalidSettingException e) {
- LOGGER.warn("problem setting configured env settings: " + e.getMessage());
- }
- }
- return environmentConfig;
- }
- @Override
- public int size(final LocalDB.DB db) throws LocalDBException {
- checkStatus(false);
- return environment.computeInReadonlyTransaction(transaction -> {
- final Store store = getStore(db);
- return (int) store.count(transaction);
- });
- }
- @Override
- public boolean contains(final LocalDB.DB db, final String key) throws LocalDBException {
- checkStatus(false);
- return get(db, key) != null;
- }
- @Override
- public String get(final LocalDB.DB db, final String key) throws LocalDBException {
- checkStatus(false);
- return environment.computeInReadonlyTransaction(transaction -> {
- final Store store = getStore(db);
- final ByteIterable returnValue = store.get(transaction, bindMachine.keyToEntry(key));
- if (returnValue != null) {
- return bindMachine.entryToValue(returnValue);
- }
- return null;
- });
- }
- @Override
- public LocalDB.LocalDBIterator<String> iterator(final LocalDB.DB db) throws LocalDBException {
- return new InnerIterator(db);
- }
- private class InnerIterator implements LocalDB.LocalDBIterator<String> {
- private final Transaction transaction;
- private final Cursor cursor;
- private boolean closed;
- private String nextValue = "";
- InnerIterator(final LocalDB.DB db) {
- this.transaction = environment.beginReadonlyTransaction();
- this.cursor = getStore(db).openCursor(transaction);
- doNext();
- }
- private void doNext() {
- try {
- checkStatus(false);
- } catch (LocalDBException e) {
- throw new IllegalStateException(e);
- }
- try {
- if (closed) {
- return;
- }
- if (!cursor.getNext()) {
- close();
- return;
- }
- final ByteIterable nextKey = cursor.getKey();
- if (nextKey == null || nextKey.getLength() == 0) {
- close();
- return;
- }
- final String decodedValue = bindMachine.entryToKey(nextKey);
- if (decodedValue == null) {
- close();
- return;
- }
- nextValue = decodedValue;
- } catch (Exception e) {
- e.printStackTrace();
- throw e;
- }
- }
- @Override
- public void close() {
- if (closed) {
- return;
- }
- cursor.close();
- transaction.abort();
- nextValue = null;
- closed = true;
- }
- @Override
- public boolean hasNext() {
- return !closed && nextValue != null;
- }
- @Override
- public String next() {
- if (closed) {
- return null;
- }
- final String value = nextValue;
- doNext();
- return value;
- }
- @Override
- public void remove() {
- throw new UnsupportedOperationException("remove not supported");
- }
- }
- @Override
- public void putAll(final LocalDB.DB db, final Map<String, String> keyValueMap) throws LocalDBException {
- checkStatus(true);
- environment.executeInTransaction(transaction -> {
- final Store store = getStore(db);
- for (final String key : keyValueMap.keySet()) {
- final String value = keyValueMap.get(key);
- final ByteIterable k = bindMachine.keyToEntry(key);
- final ByteIterable v = bindMachine.valueToEntry(value);
- store.put(transaction,k,v);
- }
- });
- outputLogExecutor.conditionallyExecuteTask();
- }
- @Override
- public boolean put(final LocalDB.DB db, final String key, final String value) throws LocalDBException {
- checkStatus(true);
- return environment.computeInTransaction(transaction -> {
- final ByteIterable k = bindMachine.keyToEntry(key);
- final ByteIterable v = bindMachine.valueToEntry(value);
- final Store store = getStore(db);
- return store.put(transaction,k,v);
- });
- }
- @Override
- public boolean remove(final LocalDB.DB db, final String key) throws LocalDBException {
- checkStatus(true);
- return environment.computeInTransaction(transaction -> {
- final Store store = getStore(db);
- return store.delete(transaction, bindMachine.keyToEntry(key));
- });
- }
- @Override
- public void removeAll(final LocalDB.DB db, final Collection<String> keys) throws LocalDBException {
- checkStatus(true);
- environment.executeInTransaction(transaction -> {
- final Store store = getStore(db);
- for (final String key : keys) {
- store.delete(transaction, bindMachine.keyToEntry(key));
- }
- });
- }
- @Override
- public void truncate(final LocalDB.DB db) throws LocalDBException {
- checkStatus(true);
- LOGGER.trace("begin truncate of " + db.toString() + ", size=" + this.size(db));
- final Date startDate = new Date();
- environment.executeInTransaction(transaction -> {
- environment.truncateStore(db.toString(), transaction);
- final Store newStoreReference = environment.openStore(db.toString(), StoreConfig.USE_EXISTING, transaction);
- cachedStoreObjects.put(db, newStoreReference);
- });
- LOGGER.trace("completed truncate of " + db.toString()
- + " (" + TimeDuration.fromCurrent(startDate).asCompactString() + ")"
- + ", size=" + this.size(db));
- }
- @Override
- public File getFileLocation() {
- return fileLocation;
- }
- @Override
- public LocalDB.Status getStatus() {
- return status;
- }
- private Store getStore(final LocalDB.DB db) {
- return cachedStoreObjects.get(db);
- }
- private Store initStore(final LocalDB.DB db, final Transaction txn) {
- return environment.openStore(db.toString(), StoreConfig.WITHOUT_DUPLICATES, txn);
- }
- private void checkStatus(final boolean writeOperation) throws LocalDBException {
- if (status != LocalDB.Status.OPEN) {
- throw new LocalDBException(new ErrorInformation(PwmError.ERROR_LOCALDB_UNAVAILABLE, "cannot perform operation, localdb instance is not open"));
- }
- if (writeOperation && readOnly) {
- throw new LocalDBException(new ErrorInformation(PwmError.ERROR_LOCALDB_UNAVAILABLE, "cannot perform operation, localdb is in read-only mode"));
- }
- outputLogExecutor.conditionallyExecuteTask();
- }
- private void outputStats() {
- LOGGER.trace("xodus environment stats: " + StringUtil.mapToString(debugInfo()));
- }
- @Override
- public Map<String, Serializable> debugInfo() {
- final Statistics statistics = environment.getStatistics();
- final Map<String,Serializable> outputStats = new LinkedHashMap<>();
- for (final EnvironmentStatistics.Type type : EnvironmentStatistics.Type.values()) {
- final String name = type.name();
- final StatisticsItem item = statistics.getStatisticsItem(name);
- if (item != null) {
- outputStats.put(name, String.valueOf(item.getTotal()));
- }
- }
- return outputStats;
- }
- private static class BindMachine {
- private static final byte COMPRESSED_PREFIX = 98;
- private static final byte UNCOMPRESSED_PREFIX = 99;
- private static final int DEFAULT_MIN_COMPRESSION_LENGTH = 16;
- private static final boolean DEFAULT_ENABLE_COMPRESSION = false;
- private final int minCompressionLength;
- private final boolean enableCompression;
- BindMachine(final boolean enableCompression, final int minCompressionLength) {
- this.enableCompression = enableCompression;
- this.minCompressionLength = minCompressionLength;
- }
- ByteIterable keyToEntry(final String key) {
- return StringBinding.stringToEntry(key);
- }
- String entryToKey(final ByteIterable entry) {
- return StringBinding.entryToString(entry);
- }
- ByteIterable valueToEntry(final String value) {
- if (!enableCompression || value == null || value.length() < minCompressionLength) {
- final ByteIterable byteIterable = StringBinding.stringToEntry(value);
- return new ArrayByteIterable(UNCOMPRESSED_PREFIX, byteIterable);
- }
- final ByteIterable byteIterable = StringBinding.stringToEntry(value);
- final byte[] rawArray = byteIterable.getBytesUnsafe();
- final byte[] compressedArray = compressData(rawArray);
- if (compressedArray.length < rawArray.length) {
- return new ArrayByteIterable(COMPRESSED_PREFIX, new ArrayByteIterable(compressedArray));
- } else {
- return new ArrayByteIterable(UNCOMPRESSED_PREFIX, byteIterable);
- }
- }
- String entryToValue(final ByteIterable value) {
- final byte[] rawValue = value.getBytesUnsafe();
- final byte[] strippedArray = new byte[rawValue.length -1];
- System.arraycopy(rawValue,1,strippedArray,0,rawValue.length -1);
- if (rawValue[0] == UNCOMPRESSED_PREFIX) {
- return StringBinding.entryToString(new ArrayByteIterable(strippedArray));
- } else if (rawValue[0] == COMPRESSED_PREFIX) {
- final byte[] decompressedValue = decompressData(strippedArray);
- return StringBinding.entryToString(new ArrayByteIterable(decompressedValue));
- }
- throw new IllegalStateException("unknown value prefix " + Byte.toString(rawValue[0]));
- }
- static byte[] compressData(final byte[] data) {
- final ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
- final DeflaterOutputStream deflaterOutputStream = new DeflaterOutputStream(byteArrayOutputStream, new Deflater());
- try {
- deflaterOutputStream.write(data);
- deflaterOutputStream.close();
- } catch (IOException e) {
- throw new IllegalStateException("unexpected exception compressing data stream: " + e.getMessage(), e);
- }
- return byteArrayOutputStream.toByteArray();
- }
- static byte[] decompressData(final byte[] data) {
- final ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
- final InflaterOutputStream inflaterOutputStream = new InflaterOutputStream(byteArrayOutputStream, new Inflater());
- try {
- inflaterOutputStream.write(data);
- inflaterOutputStream.close();
- } catch (IOException e) {
- throw new IllegalStateException("unexpected exception decompressing data stream: " + e.getMessage(), e);
- }
- return byteArrayOutputStream.toByteArray();
- }
- }
- @Override
- public Set<Flag> flags() {
- return Collections.emptySet();
- }
- }
|