Xodus_LocalDB.java 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484
  1. /*
  2. * Password Management Servlets (PWM)
  3. * http://www.pwm-project.org
  4. *
  5. * Copyright (c) 2006-2009 Novell, Inc.
  6. * Copyright (c) 2009-2017 The PWM Project
  7. *
  8. * This program is free software; you can redistribute it and/or modify
  9. * it under the terms of the GNU General Public License as published by
  10. * the Free Software Foundation; either version 2 of the License, or
  11. * (at your option) any later version.
  12. *
  13. * This program is distributed in the hope that it will be useful,
  14. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  15. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  16. * GNU General Public License for more details.
  17. *
  18. * You should have received a copy of the GNU General Public License
  19. * along with this program; if not, write to the Free Software
  20. * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
  21. */
  22. package password.pwm.util.localdb;
  23. import jetbrains.exodus.ArrayByteIterable;
  24. import jetbrains.exodus.ByteIterable;
  25. import jetbrains.exodus.InvalidSettingException;
  26. import jetbrains.exodus.bindings.StringBinding;
  27. import jetbrains.exodus.env.Cursor;
  28. import jetbrains.exodus.env.Environment;
  29. import jetbrains.exodus.env.EnvironmentConfig;
  30. import jetbrains.exodus.env.EnvironmentStatistics;
  31. import jetbrains.exodus.env.Environments;
  32. import jetbrains.exodus.env.Store;
  33. import jetbrains.exodus.env.StoreConfig;
  34. import jetbrains.exodus.env.Transaction;
  35. import jetbrains.exodus.management.Statistics;
  36. import jetbrains.exodus.management.StatisticsItem;
  37. import password.pwm.error.ErrorInformation;
  38. import password.pwm.error.PwmError;
  39. import password.pwm.util.java.ConditionalTaskExecutor;
  40. import password.pwm.util.java.JsonUtil;
  41. import password.pwm.util.java.StringUtil;
  42. import password.pwm.util.java.TimeDuration;
  43. import password.pwm.util.logging.PwmLogger;
  44. import java.io.ByteArrayOutputStream;
  45. import java.io.File;
  46. import java.io.IOException;
  47. import java.io.Serializable;
  48. import java.time.Instant;
  49. import java.util.Collection;
  50. import java.util.Collections;
  51. import java.util.Date;
  52. import java.util.HashMap;
  53. import java.util.LinkedHashMap;
  54. import java.util.Map;
  55. import java.util.Set;
  56. import java.util.concurrent.TimeUnit;
  57. import java.util.zip.Deflater;
  58. import java.util.zip.DeflaterOutputStream;
  59. import java.util.zip.Inflater;
  60. import java.util.zip.InflaterOutputStream;
  61. public class Xodus_LocalDB implements LocalDBProvider {
  62. private static final PwmLogger LOGGER = PwmLogger.forClass(Xodus_LocalDB.class);
  63. private static final TimeDuration STATS_OUTPUT_INTERVAL = new TimeDuration(24, TimeUnit.HOURS);
  64. private Environment environment;
  65. private File fileLocation;
  66. private boolean readOnly;
  67. private enum Property {
  68. Compression_Enabled("xodus.compression.enabled"),
  69. Compression_MinLength("xodus.compression.minLength"),
  70. ;
  71. private final String keyName;
  72. Property(final String keyName) {
  73. this.keyName = keyName;
  74. }
  75. public String getKeyName() {
  76. return keyName;
  77. }
  78. }
  79. private LocalDB.Status status = LocalDB.Status.NEW;
  80. private final Map<LocalDB.DB,Store> cachedStoreObjects = new HashMap<>();
  81. private final ConditionalTaskExecutor outputLogExecutor = new ConditionalTaskExecutor(
  82. () -> outputStats(),new ConditionalTaskExecutor.TimeDurationPredicate(STATS_OUTPUT_INTERVAL).setNextTimeFromNow(1, TimeUnit.MINUTES)
  83. );
  84. private BindMachine bindMachine = new BindMachine(BindMachine.DEFAULT_ENABLE_COMPRESSION, BindMachine.DEFAULT_MIN_COMPRESSION_LENGTH);
  85. @Override
  86. public void init(
  87. final File dbDirectory,
  88. final Map<String, String> initParameters,
  89. final Map<Parameter,String> parameters
  90. )
  91. throws LocalDBException
  92. {
  93. this.fileLocation = dbDirectory;
  94. LOGGER.trace("begin environment open");
  95. final Instant startTime = Instant.now();
  96. final EnvironmentConfig environmentConfig = makeEnvironmentConfig(initParameters);
  97. {
  98. final boolean compressionEnabled = initParameters.containsKey(Property.Compression_Enabled.getKeyName())
  99. ? Boolean.parseBoolean(initParameters.get(Property.Compression_Enabled.getKeyName()))
  100. : BindMachine.DEFAULT_ENABLE_COMPRESSION;
  101. final int compressionMinLength = initParameters.containsKey(Property.Compression_MinLength.getKeyName())
  102. ? Integer.parseInt(initParameters.get(Property.Compression_MinLength.getKeyName()))
  103. : BindMachine.DEFAULT_MIN_COMPRESSION_LENGTH;
  104. bindMachine = new BindMachine(compressionEnabled, compressionMinLength);
  105. }
  106. readOnly = parameters.containsKey(Parameter.readOnly) && Boolean.parseBoolean(parameters.get(Parameter.readOnly));
  107. LOGGER.trace("preparing to open with configuration " + JsonUtil.serializeMap(environmentConfig.getSettings()));
  108. environment = Environments.newInstance(dbDirectory.getAbsolutePath() + File.separator + "xodus", environmentConfig);
  109. LOGGER.trace("environment open (" + TimeDuration.fromCurrent(startTime).asCompactString() + ")");
  110. environment.executeInTransaction(txn -> {
  111. for (final LocalDB.DB db : LocalDB.DB.values()) {
  112. final Store store = initStore(db, txn);
  113. cachedStoreObjects.put(db,store);
  114. }
  115. });
  116. status = LocalDB.Status.OPEN;
  117. for (final LocalDB.DB db : LocalDB.DB.values()) {
  118. LOGGER.trace("opened " + db + " with " + this.size(db) + " records");
  119. }
  120. }
  121. @Override
  122. public void close() throws LocalDBException {
  123. if (environment != null && environment.isOpen()) {
  124. environment.close();
  125. }
  126. status = LocalDB.Status.CLOSED;
  127. LOGGER.debug("closed");
  128. }
  129. private EnvironmentConfig makeEnvironmentConfig(final Map<String, String> initParameters) {
  130. final EnvironmentConfig environmentConfig = new EnvironmentConfig();
  131. environmentConfig.setEnvCloseForcedly(true);
  132. environmentConfig.setMemoryUsage(50 * 1024 * 1024);
  133. for (final String key : initParameters.keySet()) {
  134. final String value = initParameters.get(key);
  135. final Map<String,String> singleMap = Collections.singletonMap(key,value);
  136. try {
  137. environmentConfig.setSettings(singleMap);
  138. LOGGER.trace("set env setting from appProperty: " + key + "=" + value);
  139. } catch (InvalidSettingException e) {
  140. LOGGER.warn("problem setting configured env settings: " + e.getMessage());
  141. }
  142. }
  143. return environmentConfig;
  144. }
  145. @Override
  146. public int size(final LocalDB.DB db) throws LocalDBException {
  147. checkStatus(false);
  148. return environment.computeInReadonlyTransaction(transaction -> {
  149. final Store store = getStore(db);
  150. return (int) store.count(transaction);
  151. });
  152. }
  153. @Override
  154. public boolean contains(final LocalDB.DB db, final String key) throws LocalDBException {
  155. checkStatus(false);
  156. return get(db, key) != null;
  157. }
  158. @Override
  159. public String get(final LocalDB.DB db, final String key) throws LocalDBException {
  160. checkStatus(false);
  161. return environment.computeInReadonlyTransaction(transaction -> {
  162. final Store store = getStore(db);
  163. final ByteIterable returnValue = store.get(transaction, bindMachine.keyToEntry(key));
  164. if (returnValue != null) {
  165. return bindMachine.entryToValue(returnValue);
  166. }
  167. return null;
  168. });
  169. }
  170. @Override
  171. public LocalDB.LocalDBIterator<String> iterator(final LocalDB.DB db) throws LocalDBException {
  172. return new InnerIterator(db);
  173. }
  174. private class InnerIterator implements LocalDB.LocalDBIterator<String> {
  175. private final Transaction transaction;
  176. private final Cursor cursor;
  177. private boolean closed;
  178. private String nextValue = "";
  179. InnerIterator(final LocalDB.DB db) {
  180. this.transaction = environment.beginReadonlyTransaction();
  181. this.cursor = getStore(db).openCursor(transaction);
  182. doNext();
  183. }
  184. private void doNext() {
  185. try {
  186. checkStatus(false);
  187. } catch (LocalDBException e) {
  188. throw new IllegalStateException(e);
  189. }
  190. try {
  191. if (closed) {
  192. return;
  193. }
  194. if (!cursor.getNext()) {
  195. close();
  196. return;
  197. }
  198. final ByteIterable nextKey = cursor.getKey();
  199. if (nextKey == null || nextKey.getLength() == 0) {
  200. close();
  201. return;
  202. }
  203. final String decodedValue = bindMachine.entryToKey(nextKey);
  204. if (decodedValue == null) {
  205. close();
  206. return;
  207. }
  208. nextValue = decodedValue;
  209. } catch (Exception e) {
  210. e.printStackTrace();
  211. throw e;
  212. }
  213. }
  214. @Override
  215. public void close() {
  216. if (closed) {
  217. return;
  218. }
  219. cursor.close();
  220. transaction.abort();
  221. nextValue = null;
  222. closed = true;
  223. }
  224. @Override
  225. public boolean hasNext() {
  226. return !closed && nextValue != null;
  227. }
  228. @Override
  229. public String next() {
  230. if (closed) {
  231. return null;
  232. }
  233. final String value = nextValue;
  234. doNext();
  235. return value;
  236. }
  237. @Override
  238. public void remove() {
  239. throw new UnsupportedOperationException("remove not supported");
  240. }
  241. }
  242. @Override
  243. public void putAll(final LocalDB.DB db, final Map<String, String> keyValueMap) throws LocalDBException {
  244. checkStatus(true);
  245. environment.executeInTransaction(transaction -> {
  246. final Store store = getStore(db);
  247. for (final String key : keyValueMap.keySet()) {
  248. final String value = keyValueMap.get(key);
  249. final ByteIterable k = bindMachine.keyToEntry(key);
  250. final ByteIterable v = bindMachine.valueToEntry(value);
  251. store.put(transaction,k,v);
  252. }
  253. });
  254. outputLogExecutor.conditionallyExecuteTask();
  255. }
  256. @Override
  257. public boolean put(final LocalDB.DB db, final String key, final String value) throws LocalDBException {
  258. checkStatus(true);
  259. return environment.computeInTransaction(transaction -> {
  260. final ByteIterable k = bindMachine.keyToEntry(key);
  261. final ByteIterable v = bindMachine.valueToEntry(value);
  262. final Store store = getStore(db);
  263. return store.put(transaction,k,v);
  264. });
  265. }
  266. @Override
  267. public boolean remove(final LocalDB.DB db, final String key) throws LocalDBException {
  268. checkStatus(true);
  269. return environment.computeInTransaction(transaction -> {
  270. final Store store = getStore(db);
  271. return store.delete(transaction, bindMachine.keyToEntry(key));
  272. });
  273. }
  274. @Override
  275. public void removeAll(final LocalDB.DB db, final Collection<String> keys) throws LocalDBException {
  276. checkStatus(true);
  277. environment.executeInTransaction(transaction -> {
  278. final Store store = getStore(db);
  279. for (final String key : keys) {
  280. store.delete(transaction, bindMachine.keyToEntry(key));
  281. }
  282. });
  283. }
  284. @Override
  285. public void truncate(final LocalDB.DB db) throws LocalDBException {
  286. checkStatus(true);
  287. LOGGER.trace("begin truncate of " + db.toString() + ", size=" + this.size(db));
  288. final Date startDate = new Date();
  289. environment.executeInTransaction(transaction -> {
  290. environment.truncateStore(db.toString(), transaction);
  291. final Store newStoreReference = environment.openStore(db.toString(), StoreConfig.USE_EXISTING, transaction);
  292. cachedStoreObjects.put(db, newStoreReference);
  293. });
  294. LOGGER.trace("completed truncate of " + db.toString()
  295. + " (" + TimeDuration.fromCurrent(startDate).asCompactString() + ")"
  296. + ", size=" + this.size(db));
  297. }
  298. @Override
  299. public File getFileLocation() {
  300. return fileLocation;
  301. }
  302. @Override
  303. public LocalDB.Status getStatus() {
  304. return status;
  305. }
  306. private Store getStore(final LocalDB.DB db) {
  307. return cachedStoreObjects.get(db);
  308. }
  309. private Store initStore(final LocalDB.DB db, final Transaction txn) {
  310. return environment.openStore(db.toString(), StoreConfig.WITHOUT_DUPLICATES, txn);
  311. }
  312. private void checkStatus(final boolean writeOperation) throws LocalDBException {
  313. if (status != LocalDB.Status.OPEN) {
  314. throw new LocalDBException(new ErrorInformation(PwmError.ERROR_LOCALDB_UNAVAILABLE, "cannot perform operation, localdb instance is not open"));
  315. }
  316. if (writeOperation && readOnly) {
  317. throw new LocalDBException(new ErrorInformation(PwmError.ERROR_LOCALDB_UNAVAILABLE, "cannot perform operation, localdb is in read-only mode"));
  318. }
  319. outputLogExecutor.conditionallyExecuteTask();
  320. }
  321. private void outputStats() {
  322. LOGGER.trace("xodus environment stats: " + StringUtil.mapToString(debugInfo()));
  323. }
  324. @Override
  325. public Map<String, Serializable> debugInfo() {
  326. final Statistics statistics = environment.getStatistics();
  327. final Map<String,Serializable> outputStats = new LinkedHashMap<>();
  328. for (final EnvironmentStatistics.Type type : EnvironmentStatistics.Type.values()) {
  329. final String name = type.name();
  330. final StatisticsItem item = statistics.getStatisticsItem(name);
  331. if (item != null) {
  332. outputStats.put(name, String.valueOf(item.getTotal()));
  333. }
  334. }
  335. return outputStats;
  336. }
  337. private static class BindMachine {
  338. private static final byte COMPRESSED_PREFIX = 98;
  339. private static final byte UNCOMPRESSED_PREFIX = 99;
  340. private static final int DEFAULT_MIN_COMPRESSION_LENGTH = 16;
  341. private static final boolean DEFAULT_ENABLE_COMPRESSION = false;
  342. private final int minCompressionLength;
  343. private final boolean enableCompression;
  344. BindMachine(final boolean enableCompression, final int minCompressionLength) {
  345. this.enableCompression = enableCompression;
  346. this.minCompressionLength = minCompressionLength;
  347. }
  348. ByteIterable keyToEntry(final String key) {
  349. return StringBinding.stringToEntry(key);
  350. }
  351. String entryToKey(final ByteIterable entry) {
  352. return StringBinding.entryToString(entry);
  353. }
  354. ByteIterable valueToEntry(final String value) {
  355. if (!enableCompression || value == null || value.length() < minCompressionLength) {
  356. final ByteIterable byteIterable = StringBinding.stringToEntry(value);
  357. return new ArrayByteIterable(UNCOMPRESSED_PREFIX, byteIterable);
  358. }
  359. final ByteIterable byteIterable = StringBinding.stringToEntry(value);
  360. final byte[] rawArray = byteIterable.getBytesUnsafe();
  361. final byte[] compressedArray = compressData(rawArray);
  362. if (compressedArray.length < rawArray.length) {
  363. return new ArrayByteIterable(COMPRESSED_PREFIX, new ArrayByteIterable(compressedArray));
  364. } else {
  365. return new ArrayByteIterable(UNCOMPRESSED_PREFIX, byteIterable);
  366. }
  367. }
  368. String entryToValue(final ByteIterable value) {
  369. final byte[] rawValue = value.getBytesUnsafe();
  370. final byte[] strippedArray = new byte[rawValue.length -1];
  371. System.arraycopy(rawValue,1,strippedArray,0,rawValue.length -1);
  372. if (rawValue[0] == UNCOMPRESSED_PREFIX) {
  373. return StringBinding.entryToString(new ArrayByteIterable(strippedArray));
  374. } else if (rawValue[0] == COMPRESSED_PREFIX) {
  375. final byte[] decompressedValue = decompressData(strippedArray);
  376. return StringBinding.entryToString(new ArrayByteIterable(decompressedValue));
  377. }
  378. throw new IllegalStateException("unknown value prefix " + Byte.toString(rawValue[0]));
  379. }
  380. static byte[] compressData(final byte[] data) {
  381. final ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
  382. final DeflaterOutputStream deflaterOutputStream = new DeflaterOutputStream(byteArrayOutputStream, new Deflater());
  383. try {
  384. deflaterOutputStream.write(data);
  385. deflaterOutputStream.close();
  386. } catch (IOException e) {
  387. throw new IllegalStateException("unexpected exception compressing data stream: " + e.getMessage(), e);
  388. }
  389. return byteArrayOutputStream.toByteArray();
  390. }
  391. static byte[] decompressData(final byte[] data) {
  392. final ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
  393. final InflaterOutputStream inflaterOutputStream = new InflaterOutputStream(byteArrayOutputStream, new Inflater());
  394. try {
  395. inflaterOutputStream.write(data);
  396. inflaterOutputStream.close();
  397. } catch (IOException e) {
  398. throw new IllegalStateException("unexpected exception decompressing data stream: " + e.getMessage(), e);
  399. }
  400. return byteArrayOutputStream.toByteArray();
  401. }
  402. }
  403. @Override
  404. public Set<Flag> flags() {
  405. return Collections.emptySet();
  406. }
  407. }