diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..f999431 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,7 @@ +root = true + +[*] +charset = utf-8 +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..7ccba66 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,12 @@ +* text=auto eol=lf + +# Keep common binary artifacts untouched. +*.jar binary +*.zip binary +*.gz binary +*.ser binary +*.png binary +*.jpg binary +*.jpeg binary +*.gif binary +*.ico binary diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ff9da23..253cec6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -17,6 +17,13 @@ jobs: with: java-version: 17 distribution: "adopt" + - name: Check LF line endings + shell: bash + run: | + if git grep -I -l $'\r' -- .; then + echo "CR characters found in tracked text files. Use LF line endings." >&2 + exit 1 + fi - name: Setup settings.xml run: cp settings.xml ~/.m2/settings.xml - name: Test diff --git a/LICENSE b/LICENSE index 3083a41..a32e1af 100644 --- a/LICENSE +++ b/LICENSE @@ -1,20 +1,20 @@ -The MIT License (MIT) - -Copyright (c) 2013 Codeforces - -Permission is hereby granted, free of charge, to any person obtaining a copy of -this software and associated documentation files (the "Software"), to deal in -the Software without restriction, including without limitation the rights to -use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of -the Software, and to permit persons to whom the Software is furnished to do so, -subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS -FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR -COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER -IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN -CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +The MIT License (MIT) + +Copyright (c) 2013 Codeforces + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/README.md b/README.md index 3016841..341b9a4 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -inmemo -====== - -In-memory storage to avoid requests to database, support hashmap-based indices, Nocturne class-reloading magic. Built upon Jucuzzi. +inmemo +====== + +In-memory storage to avoid requests to database, support hashmap-based indices, Nocturne class-reloading magic. Built upon Jucuzzi. diff --git a/bundle-skip-tests.bat b/bundle-skip-tests.bat index 10fcefc..dfc2e39 100644 --- a/bundle-skip-tests.bat +++ b/bundle-skip-tests.bat @@ -1 +1 @@ -bundle.bat -DskipTests +bundle.bat -DskipTests diff --git a/bundle.bat b/bundle.bat index 9de9bd7..ff2e965 100644 --- a/bundle.bat +++ b/bundle.bat @@ -1,9 +1,9 @@ -setlocal enabledelayedexpansion - -if not "!JAVA8_64_HOME!"=="" ( - set PATH=!JAVA8_64_HOME!\bin;!PATH! - set JAVA_HOME=!JAVA8_64_HOME! -) - -call mvn.cmd validate --batch-mode -call mvn.cmd -Dfile.encoding=UTF-8 -DcreateChecksum=true --batch-mode clean source:jar javadoc:jar repository:bundle-create install %* +setlocal enabledelayedexpansion + +if not "!JAVA8_64_HOME!"=="" ( + set PATH=!JAVA8_64_HOME!\bin;!PATH! + set JAVA_HOME=!JAVA8_64_HOME! +) + +call mvn.cmd validate --batch-mode +call mvn.cmd -Dfile.encoding=UTF-8 -DcreateChecksum=true --batch-mode clean source:jar javadoc:jar repository:bundle-create install %* diff --git a/pom.xml b/pom.xml index 9cea58c..375cdf2 100644 --- a/pom.xml +++ b/pom.xml @@ -1,129 +1,129 @@ - - In-memory storage to avoid requests to database, support hashmap-based indices, Nocturne class-reloading magic. Built upon Jucuzzi. - 2014 - 4.0.0 - com.codeforces.inmemo - inmemo - 2.0.2-SNAPSHOT - jar - inmemo - - - github - GitHub Packages - https://maven.pkg.github.com/Codeforces/inmemo - - - https://github.com/Codeforces/inmemo - - - MikeMirzayanov - Mike Mirzayanov - mirzayanovmr@gmail.com - - owner, author - - +3 - - - - - GitHub - https://github.com/Codeforces/inmemo/issues - - - - The MIT License (MIT) - https://github.com/Codeforces/inmemo/blob/master/LICENSE - repo - - - - scm:git:https://github.com/Codeforces/inmemo.git - scm:git:https://github.com/Codeforces/inmemo.git - https://github.com/Codeforces/inmemo.git - - - UTF-8 - - - - - org.apache.maven.plugins - maven-repository-plugin - 2.4 - - - org.codehaus.plexus - plexus-archiver - 4.2.5 - - - - - org.apache.maven.plugins - maven-compiler-plugin - 3.8.1 - - 1.8 - 1.8 - UTF-8 - - - - org.apache.maven.plugins - maven-surefire-plugin - 2.22.2 - - -Dfile.encoding=UTF-8 - -Xmx1200M - - - - - - - - - org.xerial.snappy - snappy-java - 1.1.9.1 - - - jacuzzi - jacuzzi - 0.1.0-SNAPSHOT - - - commons-beanutils - commons-beanutils - 1.9.4 - - - net.sf.trove4j - trove4j - 3.0.3 - - - org.hsqldb - hsqldb - 2.5.2 - test - - - junit - junit - 4.13.2 - test - - - com.mchange - c3p0 - 0.9.5.5 - test - - - + + In-memory storage to avoid requests to database, support hashmap-based indices, Nocturne class-reloading magic. Built upon Jucuzzi. + 2014 + 4.0.0 + com.codeforces.inmemo + inmemo + 2.0.2-SNAPSHOT + jar + inmemo + + + github + GitHub Packages + https://maven.pkg.github.com/Codeforces/inmemo + + + https://github.com/Codeforces/inmemo + + + MikeMirzayanov + Mike Mirzayanov + mirzayanovmr@gmail.com + + owner, author + + +3 + + + + + GitHub + https://github.com/Codeforces/inmemo/issues + + + + The MIT License (MIT) + https://github.com/Codeforces/inmemo/blob/master/LICENSE + repo + + + + scm:git:https://github.com/Codeforces/inmemo.git + scm:git:https://github.com/Codeforces/inmemo.git + https://github.com/Codeforces/inmemo.git + + + UTF-8 + + + + + org.apache.maven.plugins + maven-repository-plugin + 2.4 + + + org.codehaus.plexus + plexus-archiver + 4.2.5 + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.8.1 + + 1.8 + 1.8 + UTF-8 + + + + org.apache.maven.plugins + maven-surefire-plugin + 2.22.2 + + -Dfile.encoding=UTF-8 + -Xmx1200M + + + + + + + + + org.xerial.snappy + snappy-java + 1.1.9.1 + + + jacuzzi + jacuzzi + 0.1.0-SNAPSHOT + + + commons-beanutils + commons-beanutils + 1.9.4 + + + net.sf.trove4j + trove4j + 3.0.3 + + + org.hsqldb + hsqldb + 2.5.2 + test + + + junit + junit + 4.13.2 + test + + + com.mchange + c3p0 + 0.9.5.5 + test + + + diff --git a/src/main/java/com/codeforces/inmemo/HasId.java b/src/main/java/com/codeforces/inmemo/HasId.java index 8cc9ff1..2414537 100644 --- a/src/main/java/com/codeforces/inmemo/HasId.java +++ b/src/main/java/com/codeforces/inmemo/HasId.java @@ -1,10 +1,10 @@ -package com.codeforces.inmemo; - -import java.io.Serializable; - -/** - * @author Mike Mirzayanov (mirzayanovmr@gmail.com) - */ -public interface HasId extends Serializable { - long getId(); -} +package com.codeforces.inmemo; + +import java.io.Serializable; + +/** + * @author Mike Mirzayanov (mirzayanovmr@gmail.com) + */ +public interface HasId extends Serializable { + long getId(); +} diff --git a/src/main/java/com/codeforces/inmemo/Index.java b/src/main/java/com/codeforces/inmemo/Index.java index 9a279c0..20a3e4a 100644 --- a/src/main/java/com/codeforces/inmemo/Index.java +++ b/src/main/java/com/codeforces/inmemo/Index.java @@ -1,336 +1,336 @@ -package com.codeforces.inmemo; - -import gnu.trove.iterator.TLongObjectIterator; -import gnu.trove.map.TLongObjectMap; -import gnu.trove.map.hash.TLongObjectHashMap; -import org.apache.log4j.Logger; - -import javax.annotation.Nonnull; -import javax.annotation.Nullable; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Collections; -import java.util.List; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.ConcurrentMap; - -/** - * @author MikeMirzayanov (mirzayanovmr@gmail.com) - */ -@SuppressWarnings("WeakerAccess") -public class Index { - private static final Logger logger = Logger.getLogger(Index.class); - - private static final Object NULL = new Object(); - - private final String name; - private final IndexGetter indexGetter; - - // Actually, it has type ConcurrentMap> but can't be used because of non-null keys in ConcurrentHashMap. - private final ConcurrentMap> map; - - // Actually, it has type ConcurrentMap> but can't be used because of non-null keys in ConcurrentHashMap. - private final ConcurrentMap uniqueMap; - - private final EmergencyDatabaseHelper emergencyDatabaseHelper; - - // {@code true} iff each index value corresponds to at most one item. - private final boolean unique; - - private Table table; - - private final Class indexClass; - - private Index( - String name, - Class indexClass, - IndexGetter indexGetter, - boolean unique, - EmergencyDatabaseHelper emergencyDatabaseHelper) { - this.name = name; - this.indexClass = indexClass; - this.indexGetter = indexGetter; - this.unique = unique; - this.emergencyDatabaseHelper = emergencyDatabaseHelper; - - if (unique) { - uniqueMap = new ConcurrentHashMap<>(); - map = null; - } else { - uniqueMap = null; - map = new ConcurrentHashMap<>(); - } - } - - @SuppressWarnings("UnusedDeclaration") - public static Index create(String name, - Class indexClass, - IndexGetter indexGetter) { - return create(name, indexClass, indexGetter, null); - } - - @SuppressWarnings("UnusedDeclaration") - public static Index createUnique(String name, - Class indexClass, - IndexGetter indexGetter) { - return createUnique(name, indexClass, indexGetter, null); - } - - @SuppressWarnings("UnusedDeclaration") - public static Index create(String name, - Class indexClass, - IndexGetter indexGetter, - EmergencyDatabaseHelper emergencyDatabaseHelper) { - return new Index<>(name, indexClass, indexGetter, false, emergencyDatabaseHelper); - } - - @SuppressWarnings("UnusedDeclaration") - public static Index createUnique(String name, - Class indexClass, - IndexGetter indexGetter, - EmergencyDatabaseHelper emergencyDatabaseHelper) { - return new Index<>(name, indexClass, indexGetter, true, emergencyDatabaseHelper); - } - - void setTable(Table table) { - this.table = table; - } - - private Object wrapValue(V value) { - return value == null ? NULL : value; - } - - void insertOrUpdate(@Nonnull T tableItem) { - Object value = wrapValue(indexGetter.get(tableItem)); - - if (value != NULL && value.getClass() != indexClass) { - logger.info("Item of " + tableItem.getClass() + " is invalid for index '" - + table.getClazz().getName() + '#' + name + "'."); - } - - if (unique) { - assert uniqueMap != null; - T previousTableItem = uniqueMap.get(value); - if (previousTableItem != null - && previousTableItem.getId() != tableItem.getId()) { - throw new InmemoException("Index `" + name - + "` expected to be unique but it has multiple items for value=" - + value + " [previousTableItem=" + previousTableItem + ", newTableItem=" + tableItem + "]."); - } - - uniqueMap.put(value, tableItem); - } else { - assert map != null; - if (!map.containsKey(value)) { - //map.putIfAbsent(value, new ConcurrentHashMap<>()); - map.putIfAbsent(value, new TLongObjectHashMap<>(1)); - } - - map.get(value).put(tableItem.getId(), tableItem); - } - } - - private List internalFind(V value, Matcher matcher) { - if (unique) { - T tableItem = internalFindOnly(true, value, matcher); - if (tableItem == null) { - return Collections.emptyList(); - } else { - return Collections.singletonList(tableItem); - } - } - - if (value != null && value.getClass() != indexClass) { - logger.info("Value of " + value.getClass() + " is invalid for index '" - + table.getClazz().getName() + '#' + name + "'."); - } - - Object wrappedValue = wrapValue(value); - - assert map != null; - TLongObjectMap valueMap = map.get(wrappedValue); - - if ((valueMap == null || valueMap.isEmpty()) && emergencyDatabaseHelper == null) { - return Collections.emptyList(); - } - - Collection tableItems = (valueMap == null || valueMap.isEmpty()) - ? table.findAndUpdateByEmergencyQueryFields(emergencyDatabaseHelper.getEmergencyQueryFields(value)) - : values(valueMap); - - List result = new ArrayList<>(tableItems.size()); - - for (T tableItem : tableItems) { - if (matcher.match(tableItem)) { - result.add(tableItem); - } - } - - return result; - } - - private Collection values(TLongObjectMap map) { - List result = new ArrayList<>(map.size()); - for (TLongObjectIterator i = map.iterator(); i.hasNext(); ) { - i.advance(); - result.add(i.value()); - } - return result; - } - - private T internalFindOnly(boolean throwOnNotUnique, V value, Matcher matcher) { - if (value != null && value.getClass() != indexClass) { - logger.info("Value of " + value.getClass() + " is invalid for index '" - + table.getClazz().getName() + '#' + name + "'."); - } - - Object wrappedValue = wrapValue(value); - - if (unique) { - assert uniqueMap != null; - T tableItem = uniqueMap.get(wrappedValue); - - if (tableItem == null && emergencyDatabaseHelper != null) { - List items = table.findAndUpdateByEmergencyQueryFields( - emergencyDatabaseHelper.getEmergencyQueryFields(value) - ); - - if (!items.isEmpty()) { - tableItem = items.get(0); - } - - if (throwOnNotUnique && items.size() >= 2) { - throw new InmemoException("Expected at most one item of " + table.getClazz() - + " matching index " + name - + " with value=" + value + '.'); - } - } - - if (tableItem == null || !matcher.match(tableItem)) { - return null; - } else { - return tableItem; - } - } else { - assert map != null; - TLongObjectMap valueMap = map.get(wrappedValue); - - if ((valueMap == null || valueMap.isEmpty()) && emergencyDatabaseHelper == null) { - return null; - } - - Collection tableItems = (valueMap == null || valueMap.isEmpty()) - ? table.findAndUpdateByEmergencyQueryFields(emergencyDatabaseHelper.getEmergencyQueryFields(value)) - : values(valueMap); - - List result = new ArrayList<>(2); - - for (T tableItem : tableItems) { - if (matcher.match(tableItem)) { - result.add(tableItem); - if (throwOnNotUnique) { - if (result.size() >= 2) { - throw new InmemoException("Expected at most one item of " + table.getClazz() - + " matching index " + name - + " with value=" + value + '.'); - } - } else { - break; - } - } - } - - return result.isEmpty() ? null : result.get(0); - } - } - - long internalFindCount(V value, Matcher matcher) { - return internalFind(value, matcher).size(); - } - - public String getName() { - return name; - } - - @SuppressWarnings("unchecked") - List find(Object value, Matcher predicate) { - return internalFind((V) value, predicate); - } - - @SuppressWarnings("unchecked") - public T findOnly(boolean throwOnNotUnique, Object value, Matcher predicate) { - return internalFindOnly(throwOnNotUnique, (V) value, predicate); - } - - @SuppressWarnings("unchecked") - long findCount(Object value, Matcher predicate) { - return internalFindCount((V) value, predicate); - } - - @Nullable - BucketStats getBucketStats() { - if (unique) { - return null; - } - - assert map != null; - - long bucketCount = 0; - long totalBucketSize = 0; - int maxBucketSize = 0; - - for (TLongObjectMap bucket : map.values()) { - int bucketSize = bucket.size(); - bucketCount++; - totalBucketSize += bucketSize; - if (bucketSize > maxBucketSize) { - maxBucketSize = bucketSize; - } - } - - return new BucketStats(bucketCount, totalBucketSize, maxBucketSize); - } - - /** - * Helper interface to emergency query database if object is not found in memory. - */ - public interface EmergencyDatabaseHelper { - /** - * @param indexValue index value - * @return mixed array of pairs: (database column name, value). So the length of array is always even. - */ - Object[] getEmergencyQueryFields(@Nullable V indexValue); - } - - static final class BucketStats { - private final long bucketCount; - private final long totalBucketSize; - private final int maxBucketSize; - - private BucketStats(long bucketCount, long totalBucketSize, int maxBucketSize) { - this.bucketCount = bucketCount; - this.totalBucketSize = totalBucketSize; - this.maxBucketSize = maxBucketSize; - } - - long getBucketCount() { - return bucketCount; - } - - long getTotalBucketSize() { - return totalBucketSize; - } - - int getMaxBucketSize() { - return maxBucketSize; - } - - double getAverageBucketSize() { - if (bucketCount == 0) { - return 0.0; - } - - return (double) totalBucketSize / bucketCount; - } - } -} +package com.codeforces.inmemo; + +import gnu.trove.iterator.TLongObjectIterator; +import gnu.trove.map.TLongObjectMap; +import gnu.trove.map.hash.TLongObjectHashMap; +import org.apache.log4j.Logger; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; + +/** + * @author MikeMirzayanov (mirzayanovmr@gmail.com) + */ +@SuppressWarnings("WeakerAccess") +public class Index { + private static final Logger logger = Logger.getLogger(Index.class); + + private static final Object NULL = new Object(); + + private final String name; + private final IndexGetter indexGetter; + + // Actually, it has type ConcurrentMap> but can't be used because of non-null keys in ConcurrentHashMap. + private final ConcurrentMap> map; + + // Actually, it has type ConcurrentMap> but can't be used because of non-null keys in ConcurrentHashMap. + private final ConcurrentMap uniqueMap; + + private final EmergencyDatabaseHelper emergencyDatabaseHelper; + + // {@code true} iff each index value corresponds to at most one item. + private final boolean unique; + + private Table table; + + private final Class indexClass; + + private Index( + String name, + Class indexClass, + IndexGetter indexGetter, + boolean unique, + EmergencyDatabaseHelper emergencyDatabaseHelper) { + this.name = name; + this.indexClass = indexClass; + this.indexGetter = indexGetter; + this.unique = unique; + this.emergencyDatabaseHelper = emergencyDatabaseHelper; + + if (unique) { + uniqueMap = new ConcurrentHashMap<>(); + map = null; + } else { + uniqueMap = null; + map = new ConcurrentHashMap<>(); + } + } + + @SuppressWarnings("UnusedDeclaration") + public static Index create(String name, + Class indexClass, + IndexGetter indexGetter) { + return create(name, indexClass, indexGetter, null); + } + + @SuppressWarnings("UnusedDeclaration") + public static Index createUnique(String name, + Class indexClass, + IndexGetter indexGetter) { + return createUnique(name, indexClass, indexGetter, null); + } + + @SuppressWarnings("UnusedDeclaration") + public static Index create(String name, + Class indexClass, + IndexGetter indexGetter, + EmergencyDatabaseHelper emergencyDatabaseHelper) { + return new Index<>(name, indexClass, indexGetter, false, emergencyDatabaseHelper); + } + + @SuppressWarnings("UnusedDeclaration") + public static Index createUnique(String name, + Class indexClass, + IndexGetter indexGetter, + EmergencyDatabaseHelper emergencyDatabaseHelper) { + return new Index<>(name, indexClass, indexGetter, true, emergencyDatabaseHelper); + } + + void setTable(Table table) { + this.table = table; + } + + private Object wrapValue(V value) { + return value == null ? NULL : value; + } + + void insertOrUpdate(@Nonnull T tableItem) { + Object value = wrapValue(indexGetter.get(tableItem)); + + if (value != NULL && value.getClass() != indexClass) { + logger.info("Item of " + tableItem.getClass() + " is invalid for index '" + + table.getClazz().getName() + '#' + name + "'."); + } + + if (unique) { + assert uniqueMap != null; + T previousTableItem = uniqueMap.get(value); + if (previousTableItem != null + && previousTableItem.getId() != tableItem.getId()) { + throw new InmemoException("Index `" + name + + "` expected to be unique but it has multiple items for value=" + + value + " [previousTableItem=" + previousTableItem + ", newTableItem=" + tableItem + "]."); + } + + uniqueMap.put(value, tableItem); + } else { + assert map != null; + if (!map.containsKey(value)) { + //map.putIfAbsent(value, new ConcurrentHashMap<>()); + map.putIfAbsent(value, new TLongObjectHashMap<>(1)); + } + + map.get(value).put(tableItem.getId(), tableItem); + } + } + + private List internalFind(V value, Matcher matcher) { + if (unique) { + T tableItem = internalFindOnly(true, value, matcher); + if (tableItem == null) { + return Collections.emptyList(); + } else { + return Collections.singletonList(tableItem); + } + } + + if (value != null && value.getClass() != indexClass) { + logger.info("Value of " + value.getClass() + " is invalid for index '" + + table.getClazz().getName() + '#' + name + "'."); + } + + Object wrappedValue = wrapValue(value); + + assert map != null; + TLongObjectMap valueMap = map.get(wrappedValue); + + if ((valueMap == null || valueMap.isEmpty()) && emergencyDatabaseHelper == null) { + return Collections.emptyList(); + } + + Collection tableItems = (valueMap == null || valueMap.isEmpty()) + ? table.findAndUpdateByEmergencyQueryFields(emergencyDatabaseHelper.getEmergencyQueryFields(value)) + : values(valueMap); + + List result = new ArrayList<>(tableItems.size()); + + for (T tableItem : tableItems) { + if (matcher.match(tableItem)) { + result.add(tableItem); + } + } + + return result; + } + + private Collection values(TLongObjectMap map) { + List result = new ArrayList<>(map.size()); + for (TLongObjectIterator i = map.iterator(); i.hasNext(); ) { + i.advance(); + result.add(i.value()); + } + return result; + } + + private T internalFindOnly(boolean throwOnNotUnique, V value, Matcher matcher) { + if (value != null && value.getClass() != indexClass) { + logger.info("Value of " + value.getClass() + " is invalid for index '" + + table.getClazz().getName() + '#' + name + "'."); + } + + Object wrappedValue = wrapValue(value); + + if (unique) { + assert uniqueMap != null; + T tableItem = uniqueMap.get(wrappedValue); + + if (tableItem == null && emergencyDatabaseHelper != null) { + List items = table.findAndUpdateByEmergencyQueryFields( + emergencyDatabaseHelper.getEmergencyQueryFields(value) + ); + + if (!items.isEmpty()) { + tableItem = items.get(0); + } + + if (throwOnNotUnique && items.size() >= 2) { + throw new InmemoException("Expected at most one item of " + table.getClazz() + + " matching index " + name + + " with value=" + value + '.'); + } + } + + if (tableItem == null || !matcher.match(tableItem)) { + return null; + } else { + return tableItem; + } + } else { + assert map != null; + TLongObjectMap valueMap = map.get(wrappedValue); + + if ((valueMap == null || valueMap.isEmpty()) && emergencyDatabaseHelper == null) { + return null; + } + + Collection tableItems = (valueMap == null || valueMap.isEmpty()) + ? table.findAndUpdateByEmergencyQueryFields(emergencyDatabaseHelper.getEmergencyQueryFields(value)) + : values(valueMap); + + List result = new ArrayList<>(2); + + for (T tableItem : tableItems) { + if (matcher.match(tableItem)) { + result.add(tableItem); + if (throwOnNotUnique) { + if (result.size() >= 2) { + throw new InmemoException("Expected at most one item of " + table.getClazz() + + " matching index " + name + + " with value=" + value + '.'); + } + } else { + break; + } + } + } + + return result.isEmpty() ? null : result.get(0); + } + } + + long internalFindCount(V value, Matcher matcher) { + return internalFind(value, matcher).size(); + } + + public String getName() { + return name; + } + + @SuppressWarnings("unchecked") + List find(Object value, Matcher predicate) { + return internalFind((V) value, predicate); + } + + @SuppressWarnings("unchecked") + public T findOnly(boolean throwOnNotUnique, Object value, Matcher predicate) { + return internalFindOnly(throwOnNotUnique, (V) value, predicate); + } + + @SuppressWarnings("unchecked") + long findCount(Object value, Matcher predicate) { + return internalFindCount((V) value, predicate); + } + + @Nullable + BucketStats getBucketStats() { + if (unique) { + return null; + } + + assert map != null; + + long bucketCount = 0; + long totalBucketSize = 0; + int maxBucketSize = 0; + + for (TLongObjectMap bucket : map.values()) { + int bucketSize = bucket.size(); + bucketCount++; + totalBucketSize += bucketSize; + if (bucketSize > maxBucketSize) { + maxBucketSize = bucketSize; + } + } + + return new BucketStats(bucketCount, totalBucketSize, maxBucketSize); + } + + /** + * Helper interface to emergency query database if object is not found in memory. + */ + public interface EmergencyDatabaseHelper { + /** + * @param indexValue index value + * @return mixed array of pairs: (database column name, value). So the length of array is always even. + */ + Object[] getEmergencyQueryFields(@Nullable V indexValue); + } + + static final class BucketStats { + private final long bucketCount; + private final long totalBucketSize; + private final int maxBucketSize; + + private BucketStats(long bucketCount, long totalBucketSize, int maxBucketSize) { + this.bucketCount = bucketCount; + this.totalBucketSize = totalBucketSize; + this.maxBucketSize = maxBucketSize; + } + + long getBucketCount() { + return bucketCount; + } + + long getTotalBucketSize() { + return totalBucketSize; + } + + int getMaxBucketSize() { + return maxBucketSize; + } + + double getAverageBucketSize() { + if (bucketCount == 0) { + return 0.0; + } + + return (double) totalBucketSize / bucketCount; + } + } +} diff --git a/src/main/java/com/codeforces/inmemo/IndexConstraint.java b/src/main/java/com/codeforces/inmemo/IndexConstraint.java index 7372743..ad55c38 100644 --- a/src/main/java/com/codeforces/inmemo/IndexConstraint.java +++ b/src/main/java/com/codeforces/inmemo/IndexConstraint.java @@ -1,23 +1,23 @@ -package com.codeforces.inmemo; - -/** - * @author MikeMirzayanov (mirzayanovmr@gmail.com) - */ -@SuppressWarnings("WeakerAccess") -public class IndexConstraint { - private final String indexName; - private final V value; - - public IndexConstraint(String indexName, V value) { - this.indexName = indexName; - this.value = value; - } - - public V getValue() { - return value; - } - - public String getIndexName() { - return indexName; - } -} +package com.codeforces.inmemo; + +/** + * @author MikeMirzayanov (mirzayanovmr@gmail.com) + */ +@SuppressWarnings("WeakerAccess") +public class IndexConstraint { + private final String indexName; + private final V value; + + public IndexConstraint(String indexName, V value) { + this.indexName = indexName; + this.value = value; + } + + public V getValue() { + return value; + } + + public String getIndexName() { + return indexName; + } +} diff --git a/src/main/java/com/codeforces/inmemo/IndexGetter.java b/src/main/java/com/codeforces/inmemo/IndexGetter.java index 84b956a..37dc016 100644 --- a/src/main/java/com/codeforces/inmemo/IndexGetter.java +++ b/src/main/java/com/codeforces/inmemo/IndexGetter.java @@ -1,8 +1,8 @@ -package com.codeforces.inmemo; - -/** - * @author MikeMirzayanov (mirzayanovmr@gmail.com) - */ -public interface IndexGetter { - V get(T tableItem); -} +package com.codeforces.inmemo; + +/** + * @author MikeMirzayanov (mirzayanovmr@gmail.com) + */ +public interface IndexGetter { + V get(T tableItem); +} diff --git a/src/main/java/com/codeforces/inmemo/Indices.java b/src/main/java/com/codeforces/inmemo/Indices.java index e3d618b..caa3e2e 100644 --- a/src/main/java/com/codeforces/inmemo/Indices.java +++ b/src/main/java/com/codeforces/inmemo/Indices.java @@ -1,77 +1,77 @@ -package com.codeforces.inmemo; - -import java.util.ArrayList; -import java.util.Collection; -import java.util.Collections; -import java.util.List; - -/** - * @author MikeMirzayanov (mirzayanovmr@gmail.com) - */ -@SuppressWarnings("WeakerAccess") -public class Indices { - private final List> indices = new ArrayList<>(); - private final List rowListeners = new ArrayList<>(); - private final List> itemListeners = new ArrayList<>(); - - private void add(Index index) { - indices.add(index); - } - - private void add(RowListener rowListener) { - rowListeners.add(rowListener); - } - - private void add(ItemListener itemListener) { - itemListeners.add(itemListener); - } - - List> getIndices() { - return Collections.unmodifiableList(indices); - } - - List getRowListeners() { - return Collections.unmodifiableList(rowListeners); - } - - List> getItemListeners() { - return Collections.unmodifiableList(itemListeners); - } - - @SuppressWarnings("unused") - public static class Builder { - private final Collection> indices = new ArrayList<>(); - private final Collection rowListeners = new ArrayList<>(); - private final Collection> itemListeners = new ArrayList<>(); - - public void add(Index index) { - indices.add(index); - } - - public void add(RowListener rowListener) { - rowListeners.add(rowListener); - } - - public void add(ItemListener itemListener) { - itemListeners.add(itemListener); - } - - public Indices build() { - Indices result = new Indices<>(); - - for (Index index : indices) { - result.add(index); - } - - for (RowListener rowListener : rowListeners) { - result.add(rowListener); - } - - for (ItemListener itemListener : itemListeners) { - result.add(itemListener); - } - - return result; - } - } -} +package com.codeforces.inmemo; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; + +/** + * @author MikeMirzayanov (mirzayanovmr@gmail.com) + */ +@SuppressWarnings("WeakerAccess") +public class Indices { + private final List> indices = new ArrayList<>(); + private final List rowListeners = new ArrayList<>(); + private final List> itemListeners = new ArrayList<>(); + + private void add(Index index) { + indices.add(index); + } + + private void add(RowListener rowListener) { + rowListeners.add(rowListener); + } + + private void add(ItemListener itemListener) { + itemListeners.add(itemListener); + } + + List> getIndices() { + return Collections.unmodifiableList(indices); + } + + List getRowListeners() { + return Collections.unmodifiableList(rowListeners); + } + + List> getItemListeners() { + return Collections.unmodifiableList(itemListeners); + } + + @SuppressWarnings("unused") + public static class Builder { + private final Collection> indices = new ArrayList<>(); + private final Collection rowListeners = new ArrayList<>(); + private final Collection> itemListeners = new ArrayList<>(); + + public void add(Index index) { + indices.add(index); + } + + public void add(RowListener rowListener) { + rowListeners.add(rowListener); + } + + public void add(ItemListener itemListener) { + itemListeners.add(itemListener); + } + + public Indices build() { + Indices result = new Indices<>(); + + for (Index index : indices) { + result.add(index); + } + + for (RowListener rowListener : rowListeners) { + result.add(rowListener); + } + + for (ItemListener itemListener : itemListeners) { + result.add(itemListener); + } + + return result; + } + } +} diff --git a/src/main/java/com/codeforces/inmemo/Inmemo.java b/src/main/java/com/codeforces/inmemo/Inmemo.java index 39ec00d..7daa37a 100644 --- a/src/main/java/com/codeforces/inmemo/Inmemo.java +++ b/src/main/java/com/codeforces/inmemo/Inmemo.java @@ -1,608 +1,608 @@ -package com.codeforces.inmemo; - -import net.sf.cglib.beans.BeanCopier; -import org.apache.log4j.Logger; -import org.jacuzzi.core.Row; - -import javax.annotation.Nonnull; -import javax.annotation.Nullable; -import javax.sql.DataSource; -import java.io.IOException; -import java.util.*; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.ConcurrentMap; -import java.util.concurrent.locks.Lock; -import java.util.concurrent.locks.ReentrantLock; - -/** - *

- * Inmemo is a framework to store entities in-memory and update their state in memory automatically on each update - * in database. Each entity should have `indicatorField` which updates to greater value on each change. The best choice - * is `updateTime` field of MySQL type `TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP`. It - * supports indices (based on hashmaps) and queries to find list of entities or counts. Refer to examples for - * clarifications. - *

- *

- * The implementation is friendly for class-reloading magic like in Nocturne - * framework. - *

- */ -@SuppressWarnings({"WeakerAccess", "unused"}) -public final class Inmemo { - private static final Logger logger = Logger.getLogger(Inmemo.class); - private static final Map> tables = new ConcurrentHashMap<>(); - private static final Lock tablesLock = new ReentrantLock(); - private static final ConcurrentMap beanCopiers = new ConcurrentHashMap<>(); - private static volatile boolean debug; - private static final Set> noSizeSupportClasses = new HashSet<>(); - private static final Set noJournalSupportTableClassNames - = Collections.newSetFromMap(new ConcurrentHashMap<>()); - - private Inmemo() { - // No operations. - } - - static boolean isDebug() { - return debug; - } - - public static void setDebug(boolean debug) { - Inmemo.debug = debug; - } - - private static BeanCopier getBeanCopier(Class sourceClass, Class targetClass) { - ClassPair classPair = new ClassPair(sourceClass, targetClass); - BeanCopier beanCopier = beanCopiers.get(classPair); - if (beanCopier == null) { - BeanCopier copier = BeanCopier.create(sourceClass, targetClass, false); - logger.info("Created BeanCopier(" + sourceClass + ',' + targetClass + ")."); - beanCopiers.putIfAbsent(classPair, copier); - return beanCopiers.get(classPair); - } else { - return beanCopier; - } - } - - @SuppressWarnings("UnusedDeclaration") - public static Matcher acceptAnyMatcher() { - return tableItem -> true; - } - - @SuppressWarnings("UnusedDeclaration") - public static boolean isPreloaded() { - tablesLock.lock(); - try { - for (Table table : tables.values()) { - if (!table.isPreloaded()) { - return false; - } - } - } finally { - tablesLock.unlock(); - } - - return true; - } - - /** - * Table maintains ids set to support size() operation. Use this function - * to refuse support of size() to reduce memory footprint. - * - * @param clazz Table item class. - */ - public static void unsetSizeSupport(@Nonnull Class clazz) { - noSizeSupportClasses.add(clazz); - } - - /** - * Disables journal support for a table class. The first call for a class - * must be made before createTable; repeated calls are always no-op. - * - * @param clazz Table item class. - */ - public static void unsetJournalSupport(@Nonnull Class clazz) { - String tableClassName = ReflectionUtil.getTableClassName(clazz); - tablesLock.lock(); - try { - if (noJournalSupportTableClassNames.contains(tableClassName)) { - return; - } - if (tables.containsKey(tableClassName)) { - throw new IllegalStateException("Inmemo.unsetJournalSupport(clazz) must be called" - + " before Inmemo.createTable [clazz=" + tableClassName + "]."); - } - noJournalSupportTableClassNames.add(tableClassName); - } finally { - tablesLock.unlock(); - } - } - - static Set> getNoSizeSupportClasses() { - return noSizeSupportClasses; - } - - static boolean isJournalSupportUnset(@Nonnull Class clazz) { - return noJournalSupportTableClassNames.contains(ReflectionUtil.getTableClassName(clazz)); - } - - static void putTableForUnsetJournalSupportTestOnly(Class clazz) { - tablesLock.lock(); - try { - tables.put(ReflectionUtil.getTableClassName(clazz), new Table<>(clazz, "id", null)); - } finally { - tablesLock.unlock(); - } - } - - static void removeTableForUnsetJournalSupportTestOnly(Class clazz) { - tablesLock.lock(); - try { - tables.remove(ReflectionUtil.getTableClassName(clazz)); - } finally { - tablesLock.unlock(); - } - } - - /** - * Creates new table, if there is already table for compatible class then doing nothing. - * - * @param clazz Table item class. - * @param indicatorField Item field which will be monitored to increase on each change. Good idea to make it - * 'TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP'. - * @param initialIndicatorValue Initial value of indicator, will be loaded only items with at least - * {@code initialIndicatorValue}. Use {@code null} to load all the items. - * @param indices Indices built with Indices.Builder. - * @param Item class. - */ - @SuppressWarnings("UnusedDeclaration") - public static void createTable( - @Nonnull Class clazz, - @Nonnull String indicatorField, - @Nullable Object initialIndicatorValue, - @Nonnull Indices indices, - boolean waitForPreload) { - createTable(clazz, indicatorField, initialIndicatorValue, indices, null, waitForPreload); - } - - /** - * Creates new table, if there is already table for compatible class then doing nothing. - * - * @param clazz Table item class. - * @param indicatorField Item field which will be monitored to increase on each change. Good idea to make it - * 'TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP'. - * @param initialIndicatorValue Initial value of indicator, will be loaded only items with at least - * {@code initialIndicatorValue}. Use {@code null} to load all the items. - * @param indices Indices built with Indices.Builder. - * @param rowFilter Filter predicate for rows to be processed from DB - * @param Item class. - */ - @SuppressWarnings("UnusedDeclaration") - public static void createTable( - @Nonnull Class clazz, - @Nonnull String indicatorField, - @Nullable Object initialIndicatorValue, - @Nonnull Indices indices, - @Nullable Filter rowFilter, - boolean waitForPreload) { - tablesLock.lock(); - - try { - String tableClassName = ReflectionUtil.getTableClassName(clazz); - - Table table = tables.get(tableClassName); - if (table == null) { - renewTable(clazz, indicatorField, initialIndicatorValue, indices, rowFilter); - - table = tables.get(tableClassName); - if (waitForPreload) { - while (!table.isPreloaded()) { - try { - Thread.sleep(100L); - } catch (InterruptedException ignored) { - break; - } - } - logger.info("Inmemo completed to wait for preload " + tableClassName + '.'); - } - } else { - // Exactly the same class? - if (table.getClazz().equals(clazz)) { - // logger.info("Exactly the same class [class=" + clazz + "]."); - return; - } - - String clazzSpec = ReflectionUtil.getTableClassSpec(clazz); - // Compatible classes? - if (table.getClazzSpec().equals(clazzSpec)) { - logger.info("Compatible classes " + tableClassName + " [class=" + clazz + "]."); - return; - } - - renewTable(clazz, indicatorField, initialIndicatorValue, indices, rowFilter); - - table = tables.get(tableClassName); - if (waitForPreload) { - while (!table.isPreloaded()) { - try { - Thread.sleep(100L); - } catch (InterruptedException ignored) { - break; - } - } - logger.info("Inmemo completed to wait for preload " + tableClassName + '.'); - } - } - } finally { - tablesLock.unlock(); - } - } - - /** - * Drops table or doing nothing. - * - * @param clazz Table class. - * @param Entity class. - */ - public static void dropTableIfExists(@Nonnull Class clazz) { - String tableClassName = ReflectionUtil.getTableClassName(clazz); - Table table = tables.get(tableClassName); - - if (table != null) { - tablesLock.lock(); - try { - tables.remove(tableClassName); - } finally { - tablesLock.unlock(); - } - } - } - - public static int size(@Nonnull Class clazz) { - String tableClassName = ReflectionUtil.getTableClassName(clazz); - Table table = tables.get(tableClassName); - - if (table != null) { - return table.size(); - } else { - throw new InmemoException("Unable to find table for class name `" + tableClassName + "`."); - } - } - - @SuppressWarnings("UnusedDeclaration") - public static void waitForPreload(@Nonnull Class clazz) { - String tableClassName = ReflectionUtil.getTableClassName(clazz); - Table table = tables.get(tableClassName); - - if (table != null) { - boolean preloadedNow = false; - while (!table.isPreloaded()) { - preloadedNow = true; - try { - Thread.sleep(100L); - } catch (InterruptedException ignored) { - break; - } - } - if (preloadedNow) { - logger.info("Inmemo completed to wait for preload " + tableClassName + '.'); - } - } else { - throw new InmemoException("Unable to find table for class name `" + tableClassName + "`."); - } - } - - private static void renewTable(Class clazz, - String indicatorField, Object initialIndicatorValue, - Indices indices, - Filter rowFilter) { - Table table = new Table<>(clazz, indicatorField, rowFilter); - table.createUpdater(initialIndicatorValue); - - for (Index index : indices.getIndices()) { - table.add(index); - } - - for (RowListener rowListener : indices.getRowListeners()) { - table.add(rowListener); - } - - for (ItemListener itemListener : indices.getItemListeners()) { - table.add(itemListener); - } - - table.runUpdater(); - tables.put(ReflectionUtil.getTableClassName(clazz), table); - } - - /** - * @param clazz Table item class. - * @param indexConstraint Index to use in search, index value. - * @param Items class. - * @return List of _copies_ of satisfying indexConstraint items. - */ - @SuppressWarnings("UnusedDeclaration") - public static List find( - @Nonnull Class clazz, - @Nonnull IndexConstraint indexConstraint) { - return find(clazz, indexConstraint, acceptAnyMatcher()); - } - - /** - * @param clazz Table item class. - * @param indexConstraint Index to use in search, index value. - * @param matcher Predicate to choose items. - * @param Items class. - * @return List of _copies_ of matched items. - */ - @SuppressWarnings("UnusedDeclaration") - public static List find( - @Nonnull Class clazz, - @Nonnull IndexConstraint indexConstraint, - @Nonnull Matcher matcher) { - String tableClassName = ReflectionUtil.getTableClassName(clazz); - - Table table = tables.get(tableClassName); - if (table == null) { - throw new InmemoException("Unable to find table for class name `" + tableClassName + "`."); - } - - if (!table.isCompatibleItemClass(clazz)) { - throw new InmemoException("Table class is incompatible with the given [tableClass=" + table.getClazz() - + ", clazz=" + clazz + "]."); - } - - //noinspection rawtypes - Matcher tableMatcher = table.convertMatcher(clazz, matcher); - - //noinspection unchecked - List result = table.find(indexConstraint, tableMatcher); - - if (result.isEmpty()) { - return Collections.emptyList(); - } else { - boolean sameClass = true; - - for (HasId hasId : result) { - if (hasId != null) { - if (hasId.getClass() != clazz) { - sameClass = false; - } - break; - } - } - - if (false && sameClass) { - //This case does not work with multiple classloaders. - BeanCopier beanCopier = getBeanCopier(clazz, clazz); - List tableClassResult = new ArrayList<>(result.size()); - - for (HasId tableItem : result) { - T item = ReflectionUtil.newInstance(clazz); - beanCopier.copy(tableItem, item, null); - tableClassResult.add(item); - } - - return Collections.unmodifiableList(tableClassResult); - } else { - List tableClassResult = new ArrayList<>(result.size()); - - for (HasId tableItem : result) { - T item = ReflectionUtil.newInstance(clazz); - ReflectionUtil.copyProperties(tableItem, item); - tableClassResult.add(item); - } - - return Collections.unmodifiableList(tableClassResult); - } - } - } - - /** - * @param throwOnNotUnique Throw exception if resulting item is not unique. - * @param clazz Table item class. - * @param indexConstraint Index to use in search, index value. - * @param Items class. - * @return List of _copies_ of satisfying indexConstraint items. - */ - @SuppressWarnings("UnusedDeclaration") - public static T findOnly( - boolean throwOnNotUnique, - @Nonnull Class clazz, - @Nonnull IndexConstraint indexConstraint) { - return findOnly(throwOnNotUnique, clazz, indexConstraint, acceptAnyMatcher()); - } - - /** - * @param throwOnNotUnique Throw exception if resulting item is not unique. - * @param clazz Table item class. - * @param indexConstraint Index to use in search, index value. - * @param matcher Predicate to choose items. - * @param Items class. - * @return List of _copies_ of matched items. - */ - @SuppressWarnings("UnusedDeclaration") - public static T findOnly( - boolean throwOnNotUnique, - @Nonnull Class clazz, - @Nonnull IndexConstraint indexConstraint, - @Nonnull Matcher matcher) { - String tableClassName = ReflectionUtil.getTableClassName(clazz); - - Table table = tables.get(tableClassName); - if (table == null) { - throw new InmemoException("Unable to find table for class name `" + tableClassName + "`."); - } - - if (!table.isCompatibleItemClass(clazz)) { - throw new InmemoException("Table class is incompatible with the given [tableClass=" + table.getClazz() - + ", clazz=" + clazz + "]."); - } - - //noinspection rawtypes - Matcher tableMatcher = table.convertMatcher(clazz, matcher); - - //noinspection unchecked - HasId result = table.findOnly(throwOnNotUnique, indexConstraint, tableMatcher); - - if (result == null) { - return null; - } - - if (false && result.getClass() == clazz) { - //This case does not work with multiple classloaders. - T item = ReflectionUtil.newInstance(clazz); - getBeanCopier(clazz, clazz).copy(result, item, null); - return item; - } else { - T item = ReflectionUtil.newInstance(clazz); - ReflectionUtil.copyProperties(result, item); - return item; - } - } - - /** - * @param clazz Table item class. - * @param indexConstraint Index to use in search, index value. - * @param Items class. - * @return Number of satisfying indexConstraint items. - */ - @SuppressWarnings("UnusedDeclaration") - public static long findCount( - @Nonnull Class clazz, - @Nonnull IndexConstraint indexConstraint) { - return findCount(clazz, indexConstraint, acceptAnyMatcher()); - } - - /** - * @param clazz Table item class. - * @param indexConstraint Index to use in search, index value. - * @param matcher Predicate to choose items. - * @param Items class. - * @return Number of matched items. - */ - @SuppressWarnings("UnusedDeclaration") - public static long findCount( - @Nonnull Class clazz, - @Nonnull IndexConstraint indexConstraint, - @Nonnull Matcher matcher) { - String tableClassName = ReflectionUtil.getTableClassName(clazz); - - Table table = tables.get(tableClassName); - if (table == null) { - throw new InmemoException("Unable to find table for class name `" + tableClassName + "`."); - } - - if (!table.isCompatibleItemClass(clazz)) { - throw new InmemoException("Table class is incompatible with the given [tableClass=" + table.getClazz() - + ", clazz=" + clazz + "]."); - } - - //noinspection rawtypes - Matcher tableMatcher = table.convertMatcher(clazz, matcher); - - //noinspection unchecked - return table.findCount(indexConstraint, tableMatcher); - } - - private static Table getTableByClass(Class clazz) { - String tableClassName = ReflectionUtil.getTableClassName(clazz); - Table table = tables.get(tableClassName); - if (table == null) { - throw new InmemoException("Unable to find table for class name `" + tableClassName + "`."); - } - return table; - } - - @SuppressWarnings("UnusedDeclaration") - public static void insertOrUpdate(@Nullable T object) { - if (object == null) { - return; - } - - getTableByClass(object.getClass()).insertOrUpdate(object, null); - } - - public static void update(Class clazz) { - if (clazz == null) { - throw new IllegalArgumentException("Illegal arguments for Inmemo#update: clazz = "); - } - - getTableByClass(clazz).update(); - } - - public static void insertOrUpdateByIds(Class clazz, Long... ids) { - if (clazz == null) { - throw new IllegalArgumentException("Illegal arguments for Inmemo#insertOrUpdateByIds: clazz = "); - } - if (ids == null) { - throw new IllegalArgumentException("Illegal arguments for Inmemo#insertOrUpdateByIds: ids = "); - } - - if (ids.length == 0) { - return; - } - - getTableByClass(clazz).insertOrUpdateByIds(ids); - } - - /** - * Sends shutdown signal to all Inmemo background threads. There is no guarantee, that all threads are completely - * stopped, when this method finishes. - */ - public static void stopAllThreads() { - TableUpdater.stop(); - } - - @SuppressWarnings("UnusedDeclaration") - public static void setDataSource(@Nonnull DataSource dataSource) { - TableUpdater.setDataSource(dataSource); - } - - public static void setSpecificDataSource(Class clazz, @Nonnull DataSource dataSource) { - TableUpdater.setSpecificDataSource(clazz, dataSource); - } - - public static void deleteJournal(Class clazz) throws IOException { - if (clazz == null) { - throw new IllegalArgumentException("Illegal arguments for Inmemo#deleteJournal: clazz = "); - } - - getTableByClass(clazz).deleteJournal(); - } - - public interface Filter { - boolean testRow(Row row); - boolean testItem(T item); - } - - private static final class ClassPair { - private final Class classA; - private final Class classB; - - private ClassPair(Class classA, Class classB) { - this.classA = classA; - this.classB = classB; - } - - @SuppressWarnings("RedundantIfStatement") - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - - ClassPair classPair = (ClassPair) o; - - if (!classA.equals(classPair.classA)) return false; - if (!classB.equals(classPair.classB)) return false; - - return true; - } - - @Override - public int hashCode() { - int result = classA.hashCode(); - result = 31 * result + classB.hashCode(); - return result; - } - } -} +package com.codeforces.inmemo; + +import net.sf.cglib.beans.BeanCopier; +import org.apache.log4j.Logger; +import org.jacuzzi.core.Row; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import javax.sql.DataSource; +import java.io.IOException; +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; + +/** + *

+ * Inmemo is a framework to store entities in-memory and update their state in memory automatically on each update + * in database. Each entity should have `indicatorField` which updates to greater value on each change. The best choice + * is `updateTime` field of MySQL type `TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP`. It + * supports indices (based on hashmaps) and queries to find list of entities or counts. Refer to examples for + * clarifications. + *

+ *

+ * The implementation is friendly for class-reloading magic like in Nocturne + * framework. + *

+ */ +@SuppressWarnings({"WeakerAccess", "unused"}) +public final class Inmemo { + private static final Logger logger = Logger.getLogger(Inmemo.class); + private static final Map> tables = new ConcurrentHashMap<>(); + private static final Lock tablesLock = new ReentrantLock(); + private static final ConcurrentMap beanCopiers = new ConcurrentHashMap<>(); + private static volatile boolean debug; + private static final Set> noSizeSupportClasses = new HashSet<>(); + private static final Set noJournalSupportTableClassNames + = Collections.newSetFromMap(new ConcurrentHashMap<>()); + + private Inmemo() { + // No operations. + } + + static boolean isDebug() { + return debug; + } + + public static void setDebug(boolean debug) { + Inmemo.debug = debug; + } + + private static BeanCopier getBeanCopier(Class sourceClass, Class targetClass) { + ClassPair classPair = new ClassPair(sourceClass, targetClass); + BeanCopier beanCopier = beanCopiers.get(classPair); + if (beanCopier == null) { + BeanCopier copier = BeanCopier.create(sourceClass, targetClass, false); + logger.info("Created BeanCopier(" + sourceClass + ',' + targetClass + ")."); + beanCopiers.putIfAbsent(classPair, copier); + return beanCopiers.get(classPair); + } else { + return beanCopier; + } + } + + @SuppressWarnings("UnusedDeclaration") + public static Matcher acceptAnyMatcher() { + return tableItem -> true; + } + + @SuppressWarnings("UnusedDeclaration") + public static boolean isPreloaded() { + tablesLock.lock(); + try { + for (Table table : tables.values()) { + if (!table.isPreloaded()) { + return false; + } + } + } finally { + tablesLock.unlock(); + } + + return true; + } + + /** + * Table maintains ids set to support size() operation. Use this function + * to refuse support of size() to reduce memory footprint. + * + * @param clazz Table item class. + */ + public static void unsetSizeSupport(@Nonnull Class clazz) { + noSizeSupportClasses.add(clazz); + } + + /** + * Disables journal support for a table class. The first call for a class + * must be made before createTable; repeated calls are always no-op. + * + * @param clazz Table item class. + */ + public static void unsetJournalSupport(@Nonnull Class clazz) { + String tableClassName = ReflectionUtil.getTableClassName(clazz); + tablesLock.lock(); + try { + if (noJournalSupportTableClassNames.contains(tableClassName)) { + return; + } + if (tables.containsKey(tableClassName)) { + throw new IllegalStateException("Inmemo.unsetJournalSupport(clazz) must be called" + + " before Inmemo.createTable [clazz=" + tableClassName + "]."); + } + noJournalSupportTableClassNames.add(tableClassName); + } finally { + tablesLock.unlock(); + } + } + + static Set> getNoSizeSupportClasses() { + return noSizeSupportClasses; + } + + static boolean isJournalSupportUnset(@Nonnull Class clazz) { + return noJournalSupportTableClassNames.contains(ReflectionUtil.getTableClassName(clazz)); + } + + static void putTableForUnsetJournalSupportTestOnly(Class clazz) { + tablesLock.lock(); + try { + tables.put(ReflectionUtil.getTableClassName(clazz), new Table<>(clazz, "id", null)); + } finally { + tablesLock.unlock(); + } + } + + static void removeTableForUnsetJournalSupportTestOnly(Class clazz) { + tablesLock.lock(); + try { + tables.remove(ReflectionUtil.getTableClassName(clazz)); + } finally { + tablesLock.unlock(); + } + } + + /** + * Creates new table, if there is already table for compatible class then doing nothing. + * + * @param clazz Table item class. + * @param indicatorField Item field which will be monitored to increase on each change. Good idea to make it + * 'TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP'. + * @param initialIndicatorValue Initial value of indicator, will be loaded only items with at least + * {@code initialIndicatorValue}. Use {@code null} to load all the items. + * @param indices Indices built with Indices.Builder. + * @param Item class. + */ + @SuppressWarnings("UnusedDeclaration") + public static void createTable( + @Nonnull Class clazz, + @Nonnull String indicatorField, + @Nullable Object initialIndicatorValue, + @Nonnull Indices indices, + boolean waitForPreload) { + createTable(clazz, indicatorField, initialIndicatorValue, indices, null, waitForPreload); + } + + /** + * Creates new table, if there is already table for compatible class then doing nothing. + * + * @param clazz Table item class. + * @param indicatorField Item field which will be monitored to increase on each change. Good idea to make it + * 'TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP'. + * @param initialIndicatorValue Initial value of indicator, will be loaded only items with at least + * {@code initialIndicatorValue}. Use {@code null} to load all the items. + * @param indices Indices built with Indices.Builder. + * @param rowFilter Filter predicate for rows to be processed from DB + * @param Item class. + */ + @SuppressWarnings("UnusedDeclaration") + public static void createTable( + @Nonnull Class clazz, + @Nonnull String indicatorField, + @Nullable Object initialIndicatorValue, + @Nonnull Indices indices, + @Nullable Filter rowFilter, + boolean waitForPreload) { + tablesLock.lock(); + + try { + String tableClassName = ReflectionUtil.getTableClassName(clazz); + + Table table = tables.get(tableClassName); + if (table == null) { + renewTable(clazz, indicatorField, initialIndicatorValue, indices, rowFilter); + + table = tables.get(tableClassName); + if (waitForPreload) { + while (!table.isPreloaded()) { + try { + Thread.sleep(100L); + } catch (InterruptedException ignored) { + break; + } + } + logger.info("Inmemo completed to wait for preload " + tableClassName + '.'); + } + } else { + // Exactly the same class? + if (table.getClazz().equals(clazz)) { + // logger.info("Exactly the same class [class=" + clazz + "]."); + return; + } + + String clazzSpec = ReflectionUtil.getTableClassSpec(clazz); + // Compatible classes? + if (table.getClazzSpec().equals(clazzSpec)) { + logger.info("Compatible classes " + tableClassName + " [class=" + clazz + "]."); + return; + } + + renewTable(clazz, indicatorField, initialIndicatorValue, indices, rowFilter); + + table = tables.get(tableClassName); + if (waitForPreload) { + while (!table.isPreloaded()) { + try { + Thread.sleep(100L); + } catch (InterruptedException ignored) { + break; + } + } + logger.info("Inmemo completed to wait for preload " + tableClassName + '.'); + } + } + } finally { + tablesLock.unlock(); + } + } + + /** + * Drops table or doing nothing. + * + * @param clazz Table class. + * @param Entity class. + */ + public static void dropTableIfExists(@Nonnull Class clazz) { + String tableClassName = ReflectionUtil.getTableClassName(clazz); + Table table = tables.get(tableClassName); + + if (table != null) { + tablesLock.lock(); + try { + tables.remove(tableClassName); + } finally { + tablesLock.unlock(); + } + } + } + + public static int size(@Nonnull Class clazz) { + String tableClassName = ReflectionUtil.getTableClassName(clazz); + Table table = tables.get(tableClassName); + + if (table != null) { + return table.size(); + } else { + throw new InmemoException("Unable to find table for class name `" + tableClassName + "`."); + } + } + + @SuppressWarnings("UnusedDeclaration") + public static void waitForPreload(@Nonnull Class clazz) { + String tableClassName = ReflectionUtil.getTableClassName(clazz); + Table table = tables.get(tableClassName); + + if (table != null) { + boolean preloadedNow = false; + while (!table.isPreloaded()) { + preloadedNow = true; + try { + Thread.sleep(100L); + } catch (InterruptedException ignored) { + break; + } + } + if (preloadedNow) { + logger.info("Inmemo completed to wait for preload " + tableClassName + '.'); + } + } else { + throw new InmemoException("Unable to find table for class name `" + tableClassName + "`."); + } + } + + private static void renewTable(Class clazz, + String indicatorField, Object initialIndicatorValue, + Indices indices, + Filter rowFilter) { + Table table = new Table<>(clazz, indicatorField, rowFilter); + table.createUpdater(initialIndicatorValue); + + for (Index index : indices.getIndices()) { + table.add(index); + } + + for (RowListener rowListener : indices.getRowListeners()) { + table.add(rowListener); + } + + for (ItemListener itemListener : indices.getItemListeners()) { + table.add(itemListener); + } + + table.runUpdater(); + tables.put(ReflectionUtil.getTableClassName(clazz), table); + } + + /** + * @param clazz Table item class. + * @param indexConstraint Index to use in search, index value. + * @param Items class. + * @return List of _copies_ of satisfying indexConstraint items. + */ + @SuppressWarnings("UnusedDeclaration") + public static List find( + @Nonnull Class clazz, + @Nonnull IndexConstraint indexConstraint) { + return find(clazz, indexConstraint, acceptAnyMatcher()); + } + + /** + * @param clazz Table item class. + * @param indexConstraint Index to use in search, index value. + * @param matcher Predicate to choose items. + * @param Items class. + * @return List of _copies_ of matched items. + */ + @SuppressWarnings("UnusedDeclaration") + public static List find( + @Nonnull Class clazz, + @Nonnull IndexConstraint indexConstraint, + @Nonnull Matcher matcher) { + String tableClassName = ReflectionUtil.getTableClassName(clazz); + + Table table = tables.get(tableClassName); + if (table == null) { + throw new InmemoException("Unable to find table for class name `" + tableClassName + "`."); + } + + if (!table.isCompatibleItemClass(clazz)) { + throw new InmemoException("Table class is incompatible with the given [tableClass=" + table.getClazz() + + ", clazz=" + clazz + "]."); + } + + //noinspection rawtypes + Matcher tableMatcher = table.convertMatcher(clazz, matcher); + + //noinspection unchecked + List result = table.find(indexConstraint, tableMatcher); + + if (result.isEmpty()) { + return Collections.emptyList(); + } else { + boolean sameClass = true; + + for (HasId hasId : result) { + if (hasId != null) { + if (hasId.getClass() != clazz) { + sameClass = false; + } + break; + } + } + + if (false && sameClass) { + //This case does not work with multiple classloaders. + BeanCopier beanCopier = getBeanCopier(clazz, clazz); + List tableClassResult = new ArrayList<>(result.size()); + + for (HasId tableItem : result) { + T item = ReflectionUtil.newInstance(clazz); + beanCopier.copy(tableItem, item, null); + tableClassResult.add(item); + } + + return Collections.unmodifiableList(tableClassResult); + } else { + List tableClassResult = new ArrayList<>(result.size()); + + for (HasId tableItem : result) { + T item = ReflectionUtil.newInstance(clazz); + ReflectionUtil.copyProperties(tableItem, item); + tableClassResult.add(item); + } + + return Collections.unmodifiableList(tableClassResult); + } + } + } + + /** + * @param throwOnNotUnique Throw exception if resulting item is not unique. + * @param clazz Table item class. + * @param indexConstraint Index to use in search, index value. + * @param Items class. + * @return List of _copies_ of satisfying indexConstraint items. + */ + @SuppressWarnings("UnusedDeclaration") + public static T findOnly( + boolean throwOnNotUnique, + @Nonnull Class clazz, + @Nonnull IndexConstraint indexConstraint) { + return findOnly(throwOnNotUnique, clazz, indexConstraint, acceptAnyMatcher()); + } + + /** + * @param throwOnNotUnique Throw exception if resulting item is not unique. + * @param clazz Table item class. + * @param indexConstraint Index to use in search, index value. + * @param matcher Predicate to choose items. + * @param Items class. + * @return List of _copies_ of matched items. + */ + @SuppressWarnings("UnusedDeclaration") + public static T findOnly( + boolean throwOnNotUnique, + @Nonnull Class clazz, + @Nonnull IndexConstraint indexConstraint, + @Nonnull Matcher matcher) { + String tableClassName = ReflectionUtil.getTableClassName(clazz); + + Table table = tables.get(tableClassName); + if (table == null) { + throw new InmemoException("Unable to find table for class name `" + tableClassName + "`."); + } + + if (!table.isCompatibleItemClass(clazz)) { + throw new InmemoException("Table class is incompatible with the given [tableClass=" + table.getClazz() + + ", clazz=" + clazz + "]."); + } + + //noinspection rawtypes + Matcher tableMatcher = table.convertMatcher(clazz, matcher); + + //noinspection unchecked + HasId result = table.findOnly(throwOnNotUnique, indexConstraint, tableMatcher); + + if (result == null) { + return null; + } + + if (false && result.getClass() == clazz) { + //This case does not work with multiple classloaders. + T item = ReflectionUtil.newInstance(clazz); + getBeanCopier(clazz, clazz).copy(result, item, null); + return item; + } else { + T item = ReflectionUtil.newInstance(clazz); + ReflectionUtil.copyProperties(result, item); + return item; + } + } + + /** + * @param clazz Table item class. + * @param indexConstraint Index to use in search, index value. + * @param Items class. + * @return Number of satisfying indexConstraint items. + */ + @SuppressWarnings("UnusedDeclaration") + public static long findCount( + @Nonnull Class clazz, + @Nonnull IndexConstraint indexConstraint) { + return findCount(clazz, indexConstraint, acceptAnyMatcher()); + } + + /** + * @param clazz Table item class. + * @param indexConstraint Index to use in search, index value. + * @param matcher Predicate to choose items. + * @param Items class. + * @return Number of matched items. + */ + @SuppressWarnings("UnusedDeclaration") + public static long findCount( + @Nonnull Class clazz, + @Nonnull IndexConstraint indexConstraint, + @Nonnull Matcher matcher) { + String tableClassName = ReflectionUtil.getTableClassName(clazz); + + Table table = tables.get(tableClassName); + if (table == null) { + throw new InmemoException("Unable to find table for class name `" + tableClassName + "`."); + } + + if (!table.isCompatibleItemClass(clazz)) { + throw new InmemoException("Table class is incompatible with the given [tableClass=" + table.getClazz() + + ", clazz=" + clazz + "]."); + } + + //noinspection rawtypes + Matcher tableMatcher = table.convertMatcher(clazz, matcher); + + //noinspection unchecked + return table.findCount(indexConstraint, tableMatcher); + } + + private static Table getTableByClass(Class clazz) { + String tableClassName = ReflectionUtil.getTableClassName(clazz); + Table table = tables.get(tableClassName); + if (table == null) { + throw new InmemoException("Unable to find table for class name `" + tableClassName + "`."); + } + return table; + } + + @SuppressWarnings("UnusedDeclaration") + public static void insertOrUpdate(@Nullable T object) { + if (object == null) { + return; + } + + getTableByClass(object.getClass()).insertOrUpdate(object, null); + } + + public static void update(Class clazz) { + if (clazz == null) { + throw new IllegalArgumentException("Illegal arguments for Inmemo#update: clazz = "); + } + + getTableByClass(clazz).update(); + } + + public static void insertOrUpdateByIds(Class clazz, Long... ids) { + if (clazz == null) { + throw new IllegalArgumentException("Illegal arguments for Inmemo#insertOrUpdateByIds: clazz = "); + } + if (ids == null) { + throw new IllegalArgumentException("Illegal arguments for Inmemo#insertOrUpdateByIds: ids = "); + } + + if (ids.length == 0) { + return; + } + + getTableByClass(clazz).insertOrUpdateByIds(ids); + } + + /** + * Sends shutdown signal to all Inmemo background threads. There is no guarantee, that all threads are completely + * stopped, when this method finishes. + */ + public static void stopAllThreads() { + TableUpdater.stop(); + } + + @SuppressWarnings("UnusedDeclaration") + public static void setDataSource(@Nonnull DataSource dataSource) { + TableUpdater.setDataSource(dataSource); + } + + public static void setSpecificDataSource(Class clazz, @Nonnull DataSource dataSource) { + TableUpdater.setSpecificDataSource(clazz, dataSource); + } + + public static void deleteJournal(Class clazz) throws IOException { + if (clazz == null) { + throw new IllegalArgumentException("Illegal arguments for Inmemo#deleteJournal: clazz = "); + } + + getTableByClass(clazz).deleteJournal(); + } + + public interface Filter { + boolean testRow(Row row); + boolean testItem(T item); + } + + private static final class ClassPair { + private final Class classA; + private final Class classB; + + private ClassPair(Class classA, Class classB) { + this.classA = classA; + this.classB = classB; + } + + @SuppressWarnings("RedundantIfStatement") + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + ClassPair classPair = (ClassPair) o; + + if (!classA.equals(classPair.classA)) return false; + if (!classB.equals(classPair.classB)) return false; + + return true; + } + + @Override + public int hashCode() { + int result = classA.hashCode(); + result = 31 * result + classB.hashCode(); + return result; + } + } +} diff --git a/src/main/java/com/codeforces/inmemo/InmemoException.java b/src/main/java/com/codeforces/inmemo/InmemoException.java index 72c3f15..3554b95 100644 --- a/src/main/java/com/codeforces/inmemo/InmemoException.java +++ b/src/main/java/com/codeforces/inmemo/InmemoException.java @@ -1,15 +1,15 @@ -package com.codeforces.inmemo; - -/** - * @author Mike Mirzayanov (mirzayanovmr@gmail.com) - */ -@SuppressWarnings("WeakerAccess") -public class InmemoException extends RuntimeException { - public InmemoException(String message, Throwable throwable) { - super(message, throwable); - } - - public InmemoException(String message) { - super(message); - } -} +package com.codeforces.inmemo; + +/** + * @author Mike Mirzayanov (mirzayanovmr@gmail.com) + */ +@SuppressWarnings("WeakerAccess") +public class InmemoException extends RuntimeException { + public InmemoException(String message, Throwable throwable) { + super(message, throwable); + } + + public InmemoException(String message) { + super(message); + } +} diff --git a/src/main/java/com/codeforces/inmemo/ItemListener.java b/src/main/java/com/codeforces/inmemo/ItemListener.java index 91f2f76..a20f264 100644 --- a/src/main/java/com/codeforces/inmemo/ItemListener.java +++ b/src/main/java/com/codeforces/inmemo/ItemListener.java @@ -1,29 +1,29 @@ -package com.codeforces.inmemo; - -import javax.annotation.Nonnull; - -/** - * @author Mike Mirzayanov (mirzayanovmr@gmail.com) - */ -@SuppressWarnings({"WeakerAccess", "unused"}) -public class ItemListener { - private final String name; - private final Listener listener; - - public ItemListener(@Nonnull String name, @Nonnull Listener listener) { - this.name = name; - this.listener = listener; - } - - public void insertOrUpdate(@Nonnull T item) { - listener.onInsertOrUpdate(item); - } - - public String getName() { - return name; - } - - public interface Listener { - void onInsertOrUpdate(T item); - } -} +package com.codeforces.inmemo; + +import javax.annotation.Nonnull; + +/** + * @author Mike Mirzayanov (mirzayanovmr@gmail.com) + */ +@SuppressWarnings({"WeakerAccess", "unused"}) +public class ItemListener { + private final String name; + private final Listener listener; + + public ItemListener(@Nonnull String name, @Nonnull Listener listener) { + this.name = name; + this.listener = listener; + } + + public void insertOrUpdate(@Nonnull T item) { + listener.onInsertOrUpdate(item); + } + + public String getName() { + return name; + } + + public interface Listener { + void onInsertOrUpdate(T item); + } +} diff --git a/src/main/java/com/codeforces/inmemo/Matcher.java b/src/main/java/com/codeforces/inmemo/Matcher.java index 9c22164..6666020 100644 --- a/src/main/java/com/codeforces/inmemo/Matcher.java +++ b/src/main/java/com/codeforces/inmemo/Matcher.java @@ -1,8 +1,8 @@ -package com.codeforces.inmemo; - -/** - * @author MikeMirzayanov (mirzayanovmr@gmail.com) - */ -public interface Matcher { - boolean match(T tableItem); -} +package com.codeforces.inmemo; + +/** + * @author MikeMirzayanov (mirzayanovmr@gmail.com) + */ +public interface Matcher { + boolean match(T tableItem); +} diff --git a/src/main/java/com/codeforces/inmemo/ReflectionUtil.java b/src/main/java/com/codeforces/inmemo/ReflectionUtil.java index 463ff2f..3db4ee7 100644 --- a/src/main/java/com/codeforces/inmemo/ReflectionUtil.java +++ b/src/main/java/com/codeforces/inmemo/ReflectionUtil.java @@ -1,281 +1,281 @@ -package com.codeforces.inmemo; - -import net.sf.cglib.reflect.FastClass; -import net.sf.cglib.reflect.FastMethod; - -import javax.annotation.Nonnull; -import java.beans.BeanInfo; -import java.beans.IntrospectionException; -import java.beans.Introspector; -import java.beans.PropertyDescriptor; -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; -import java.lang.reflect.Modifier; -import java.util.HashMap; -import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.ConcurrentMap; - -/** - * @author MikeMirzayanov (mirzayanovmr@gmail.com) - */ -@SuppressWarnings("WeakerAccess") -final class ReflectionUtil { - private static final ConcurrentMap, FastClass> fastClassCache = new ConcurrentHashMap<>(); - private static final ConcurrentMap, Map> gettersCache = new ConcurrentHashMap<>(); - private static final ConcurrentMap, Map> settersCache = new ConcurrentHashMap<>(); - private static final ConcurrentMap, String> tableClassNamesCache = new ConcurrentHashMap<>(); - - private ReflectionUtil() { - // No operations. - } - - public static T newInstance(Class clazz) { - FastClass fastClass = getFastClass(clazz); - - try { - //noinspection unchecked - return (T) fastClass.newInstance(); - } catch (InvocationTargetException e) { - throw new InmemoException("Can't create new instance [class=" + clazz + "].", e); - } - } - - @Nonnull - public static String getTableClassSpec(Class clazz) { - try { - BeanInfo beanInfo = Introspector.getBeanInfo(clazz); - - StringBuilder result = new StringBuilder(); - for (PropertyDescriptor propertyDescriptor : beanInfo.getPropertyDescriptors()) { - Class propertyType = propertyDescriptor.getPropertyType(); - if (propertyType != null) { - result.append(propertyDescriptor.getName()) - .append(':') - .append(propertyType.getName()) - .append(','); - } - } - - return result.toString(); - } catch (IntrospectionException e) { - throw new InmemoException("Can't get class specification [class=" + clazz + "].", e); - } - } - - public static String getTableClassName(Class clazz) { - String cachedResult = tableClassNamesCache.get(clazz); - if (cachedResult != null) { - return cachedResult; - } - - Class currentClass = clazz; - - while (currentClass != null) { - String currentClassName = currentClass.getName(); - if (currentClassName.contains("$")) { - currentClass = currentClass.getSuperclass(); - } else { - tableClassNamesCache.putIfAbsent(clazz, currentClassName); - return currentClassName; - } - } - - throw new InmemoException("Can't get table class name [class=" + clazz + "]."); - } - - private static FastClass getFastClass(Class clazz) { - FastClass result = fastClassCache.get(clazz); - - while (result == null) { - FastClass fastClass = FastClass.create(clazz); - fastClassCache.putIfAbsent(clazz, fastClass); - result = fastClassCache.get(clazz); - } - - return result; - } - - private static Map getGettersMap(Class clazz) { - Map result = gettersCache.get(clazz); - - while (result == null) { - result = buildGettersMap(clazz); - gettersCache.putIfAbsent(clazz, result); - result = gettersCache.get(clazz); - } - - return result; - } - - private static Map buildGettersMap(Class clazz) { - Map result = new HashMap<>(); - - FastClass fastClass = getFastClass(clazz); - Method[] methods = clazz.getMethods(); - - for (Method method : methods) { - if (!Modifier.isStatic(method.getModifiers())) { - String property = getGetterProperty(method); - if (property != null) { - result.put(property, fastClass.getMethod(method)); - } - } - } - - return result; - } - - private static Map getSettersMap(Class clazz) { - Map result = settersCache.get(clazz); - - while (result == null) { - result = buildSettersMap(clazz); - settersCache.putIfAbsent(clazz, result); - result = settersCache.get(clazz); - } - - return result; - } - - private static Map buildSettersMap(Class clazz) { - Map result = new HashMap<>(); - - FastClass fastClass = getFastClass(clazz); - Method[] methods = clazz.getMethods(); - - for (Method method : methods) { - if (!Modifier.isStatic(method.getModifiers())) { - String property = getSetterProperty(method); - if (property != null) { - result.put(property, fastClass.getMethod(method)); - } - } - } - - return result; - } - - private static String getGetterProperty(Method method) { - if (method.getParameterTypes().length > 0 || method.getDeclaringClass() == Object.class) { - return null; - } - - String name = method.getName(); - if (name.length() > 3 && name.startsWith("get") && Character.isUpperCase(name.charAt(3))) { - return Character.toLowerCase(name.charAt(3)) + name.substring(4); - } - if (name.length() > 2 && name.startsWith("is") && Character.isUpperCase(name.charAt(2))) { - return Character.toLowerCase(name.charAt(2)) + name.substring(3); - } - - return null; - } - - private static String getSetterProperty(Method method) { - if (method.getParameterTypes().length != 1 || method.getDeclaringClass() == Object.class) { - return null; - } - - String name = method.getName(); - if (name.length() > 3 && name.startsWith("set") && Character.isUpperCase(name.charAt(3))) { - return Character.toLowerCase(name.charAt(3)) + name.substring(4); - } - - return null; - } - - public static void copyProperties(Object source, Object target) { - if (source == target) { - return; - } - - if (source == null) { - throw new NullPointerException("Argument source can't be null (if target is not null)."); - } - - if (target == null) { - throw new NullPointerException("Argument target can't be null (if source is not null)."); - } - - Map sourceGetters = getGettersMap(source.getClass()); - Map targetSetters = getSettersMap(target.getClass()); - - for (Map.Entry getterEntry : sourceGetters.entrySet()) { - FastMethod getter = getterEntry.getValue(); - FastMethod setter = targetSetters.get(getterEntry.getKey()); - - if (setter != null) { - Class getterReturnsClass = getter.getReturnType(); - Class setterExpectsClass = setter.getParameterTypes()[0]; - - Object value; - try { - value = getter.invoke(source, new Object[]{}); - } catch (InvocationTargetException e) { - throw new RuntimeException("Can't get property '" + getterEntry.getKey() - + "' from " + source.getClass() + ".", e); - } - - if (setterExpectsClass.isAssignableFrom(getterReturnsClass)) { - try { - setter.invoke(target, new Object[]{value}); - } catch (InvocationTargetException e) { - throw new RuntimeException("Can't copy assignable property '" + getterEntry.getKey() - + "' from " + source.getClass() + " to " + target.getClass() + ".", e); - } - continue; - } - - if (setterExpectsClass.isAssignableFrom(String.class)) { - try { - if (value == null) { - setter.invoke(target, new Object[]{null}); - } else { - setter.invoke(target, new Object[]{value.toString()}); - } - } catch (InvocationTargetException e) { - throw new RuntimeException("Can't copy assignable property '" + getterEntry.getKey() - + "' from " + source.getClass() - + " to string assignable property of " + target.getClass() + ".", e); - } - continue; - } - - if (setterExpectsClass.isEnum()) { - try { - if (value == null) { - setter.invoke(target, new Object[]{null}); - } else { - String valueString = value.toString(); - Object[] constants = setterExpectsClass.getEnumConstants(); - for (Object constant : constants) { - if (constant.toString().equals(valueString)) { - setter.invoke(target, new Object[]{constant}); - break; - } - } - } - } catch (InvocationTargetException e) { - throw new RuntimeException("Can't copy enum property '" + getterEntry.getKey() - + "' from " + source.getClass() - + " to " + target.getClass() + ".", e); - } - continue; - } - - if (value != null) { - try { - Object valueCopy = setterExpectsClass.newInstance(); - copyProperties(value, valueCopy); - setter.invoke(target, new Object[]{valueCopy}); - } catch (Exception e) { - throw new RuntimeException("Can't copy object property '" + getterEntry.getKey() - + "' from " + source.getClass() - + " to " + target.getClass() + ".", e); - } - } - } - } - } -} +package com.codeforces.inmemo; + +import net.sf.cglib.reflect.FastClass; +import net.sf.cglib.reflect.FastMethod; + +import javax.annotation.Nonnull; +import java.beans.BeanInfo; +import java.beans.IntrospectionException; +import java.beans.Introspector; +import java.beans.PropertyDescriptor; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; + +/** + * @author MikeMirzayanov (mirzayanovmr@gmail.com) + */ +@SuppressWarnings("WeakerAccess") +final class ReflectionUtil { + private static final ConcurrentMap, FastClass> fastClassCache = new ConcurrentHashMap<>(); + private static final ConcurrentMap, Map> gettersCache = new ConcurrentHashMap<>(); + private static final ConcurrentMap, Map> settersCache = new ConcurrentHashMap<>(); + private static final ConcurrentMap, String> tableClassNamesCache = new ConcurrentHashMap<>(); + + private ReflectionUtil() { + // No operations. + } + + public static T newInstance(Class clazz) { + FastClass fastClass = getFastClass(clazz); + + try { + //noinspection unchecked + return (T) fastClass.newInstance(); + } catch (InvocationTargetException e) { + throw new InmemoException("Can't create new instance [class=" + clazz + "].", e); + } + } + + @Nonnull + public static String getTableClassSpec(Class clazz) { + try { + BeanInfo beanInfo = Introspector.getBeanInfo(clazz); + + StringBuilder result = new StringBuilder(); + for (PropertyDescriptor propertyDescriptor : beanInfo.getPropertyDescriptors()) { + Class propertyType = propertyDescriptor.getPropertyType(); + if (propertyType != null) { + result.append(propertyDescriptor.getName()) + .append(':') + .append(propertyType.getName()) + .append(','); + } + } + + return result.toString(); + } catch (IntrospectionException e) { + throw new InmemoException("Can't get class specification [class=" + clazz + "].", e); + } + } + + public static String getTableClassName(Class clazz) { + String cachedResult = tableClassNamesCache.get(clazz); + if (cachedResult != null) { + return cachedResult; + } + + Class currentClass = clazz; + + while (currentClass != null) { + String currentClassName = currentClass.getName(); + if (currentClassName.contains("$")) { + currentClass = currentClass.getSuperclass(); + } else { + tableClassNamesCache.putIfAbsent(clazz, currentClassName); + return currentClassName; + } + } + + throw new InmemoException("Can't get table class name [class=" + clazz + "]."); + } + + private static FastClass getFastClass(Class clazz) { + FastClass result = fastClassCache.get(clazz); + + while (result == null) { + FastClass fastClass = FastClass.create(clazz); + fastClassCache.putIfAbsent(clazz, fastClass); + result = fastClassCache.get(clazz); + } + + return result; + } + + private static Map getGettersMap(Class clazz) { + Map result = gettersCache.get(clazz); + + while (result == null) { + result = buildGettersMap(clazz); + gettersCache.putIfAbsent(clazz, result); + result = gettersCache.get(clazz); + } + + return result; + } + + private static Map buildGettersMap(Class clazz) { + Map result = new HashMap<>(); + + FastClass fastClass = getFastClass(clazz); + Method[] methods = clazz.getMethods(); + + for (Method method : methods) { + if (!Modifier.isStatic(method.getModifiers())) { + String property = getGetterProperty(method); + if (property != null) { + result.put(property, fastClass.getMethod(method)); + } + } + } + + return result; + } + + private static Map getSettersMap(Class clazz) { + Map result = settersCache.get(clazz); + + while (result == null) { + result = buildSettersMap(clazz); + settersCache.putIfAbsent(clazz, result); + result = settersCache.get(clazz); + } + + return result; + } + + private static Map buildSettersMap(Class clazz) { + Map result = new HashMap<>(); + + FastClass fastClass = getFastClass(clazz); + Method[] methods = clazz.getMethods(); + + for (Method method : methods) { + if (!Modifier.isStatic(method.getModifiers())) { + String property = getSetterProperty(method); + if (property != null) { + result.put(property, fastClass.getMethod(method)); + } + } + } + + return result; + } + + private static String getGetterProperty(Method method) { + if (method.getParameterTypes().length > 0 || method.getDeclaringClass() == Object.class) { + return null; + } + + String name = method.getName(); + if (name.length() > 3 && name.startsWith("get") && Character.isUpperCase(name.charAt(3))) { + return Character.toLowerCase(name.charAt(3)) + name.substring(4); + } + if (name.length() > 2 && name.startsWith("is") && Character.isUpperCase(name.charAt(2))) { + return Character.toLowerCase(name.charAt(2)) + name.substring(3); + } + + return null; + } + + private static String getSetterProperty(Method method) { + if (method.getParameterTypes().length != 1 || method.getDeclaringClass() == Object.class) { + return null; + } + + String name = method.getName(); + if (name.length() > 3 && name.startsWith("set") && Character.isUpperCase(name.charAt(3))) { + return Character.toLowerCase(name.charAt(3)) + name.substring(4); + } + + return null; + } + + public static void copyProperties(Object source, Object target) { + if (source == target) { + return; + } + + if (source == null) { + throw new NullPointerException("Argument source can't be null (if target is not null)."); + } + + if (target == null) { + throw new NullPointerException("Argument target can't be null (if source is not null)."); + } + + Map sourceGetters = getGettersMap(source.getClass()); + Map targetSetters = getSettersMap(target.getClass()); + + for (Map.Entry getterEntry : sourceGetters.entrySet()) { + FastMethod getter = getterEntry.getValue(); + FastMethod setter = targetSetters.get(getterEntry.getKey()); + + if (setter != null) { + Class getterReturnsClass = getter.getReturnType(); + Class setterExpectsClass = setter.getParameterTypes()[0]; + + Object value; + try { + value = getter.invoke(source, new Object[]{}); + } catch (InvocationTargetException e) { + throw new RuntimeException("Can't get property '" + getterEntry.getKey() + + "' from " + source.getClass() + ".", e); + } + + if (setterExpectsClass.isAssignableFrom(getterReturnsClass)) { + try { + setter.invoke(target, new Object[]{value}); + } catch (InvocationTargetException e) { + throw new RuntimeException("Can't copy assignable property '" + getterEntry.getKey() + + "' from " + source.getClass() + " to " + target.getClass() + ".", e); + } + continue; + } + + if (setterExpectsClass.isAssignableFrom(String.class)) { + try { + if (value == null) { + setter.invoke(target, new Object[]{null}); + } else { + setter.invoke(target, new Object[]{value.toString()}); + } + } catch (InvocationTargetException e) { + throw new RuntimeException("Can't copy assignable property '" + getterEntry.getKey() + + "' from " + source.getClass() + + " to string assignable property of " + target.getClass() + ".", e); + } + continue; + } + + if (setterExpectsClass.isEnum()) { + try { + if (value == null) { + setter.invoke(target, new Object[]{null}); + } else { + String valueString = value.toString(); + Object[] constants = setterExpectsClass.getEnumConstants(); + for (Object constant : constants) { + if (constant.toString().equals(valueString)) { + setter.invoke(target, new Object[]{constant}); + break; + } + } + } + } catch (InvocationTargetException e) { + throw new RuntimeException("Can't copy enum property '" + getterEntry.getKey() + + "' from " + source.getClass() + + " to " + target.getClass() + ".", e); + } + continue; + } + + if (value != null) { + try { + Object valueCopy = setterExpectsClass.newInstance(); + copyProperties(value, valueCopy); + setter.invoke(target, new Object[]{valueCopy}); + } catch (Exception e) { + throw new RuntimeException("Can't copy object property '" + getterEntry.getKey() + + "' from " + source.getClass() + + " to " + target.getClass() + ".", e); + } + } + } + } + } +} diff --git a/src/main/java/com/codeforces/inmemo/RowListener.java b/src/main/java/com/codeforces/inmemo/RowListener.java index 6368212..76396b5 100644 --- a/src/main/java/com/codeforces/inmemo/RowListener.java +++ b/src/main/java/com/codeforces/inmemo/RowListener.java @@ -1,31 +1,31 @@ -package com.codeforces.inmemo; - -import org.jacuzzi.core.Row; - -import javax.annotation.Nonnull; - -/** - * @author Mike Mirzayanov (mirzayanovmr@gmail.com) - */ -@SuppressWarnings({"WeakerAccess", "unused"}) -public class RowListener { - private final String name; - private final Listener listener; - - public RowListener(@Nonnull String name, @Nonnull Listener listener) { - this.name = name; - this.listener = listener; - } - - public void insertOrUpdate(@Nonnull Row row) { - listener.onInsertOrUpdate(row); - } - - public String getName() { - return name; - } - - public interface Listener { - void onInsertOrUpdate(@Nonnull Row row); - } -} +package com.codeforces.inmemo; + +import org.jacuzzi.core.Row; + +import javax.annotation.Nonnull; + +/** + * @author Mike Mirzayanov (mirzayanovmr@gmail.com) + */ +@SuppressWarnings({"WeakerAccess", "unused"}) +public class RowListener { + private final String name; + private final Listener listener; + + public RowListener(@Nonnull String name, @Nonnull Listener listener) { + this.name = name; + this.listener = listener; + } + + public void insertOrUpdate(@Nonnull Row row) { + listener.onInsertOrUpdate(row); + } + + public String getName() { + return name; + } + + public interface Listener { + void onInsertOrUpdate(@Nonnull Row row); + } +} diff --git a/src/main/java/com/codeforces/inmemo/Table.java b/src/main/java/com/codeforces/inmemo/Table.java index 63280f2..5b9da16 100644 --- a/src/main/java/com/codeforces/inmemo/Table.java +++ b/src/main/java/com/codeforces/inmemo/Table.java @@ -1,442 +1,442 @@ -package com.codeforces.inmemo; - -import gnu.trove.set.TLongSet; -import gnu.trove.set.hash.TLongHashSet; -import org.apache.log4j.Logger; -import org.jacuzzi.core.ArrayMap; -import org.jacuzzi.core.Row; -import org.jacuzzi.core.RowRoll; -import org.xerial.snappy.SnappyInputStream; -import org.xerial.snappy.SnappyOutputStream; - -import javax.annotation.Nonnull; -import javax.annotation.Nullable; -import java.io.*; -import java.nio.file.Files; -import java.util.ArrayList; -import java.util.Date; -import java.util.List; -import java.util.Locale; -import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.atomic.AtomicInteger; -import java.util.concurrent.locks.Lock; -import java.util.concurrent.locks.ReentrantLock; -import java.util.regex.Pattern; - -/** - * @author Mike Mirzayanov (mirzayanovmr@gmail.com) - */ -@SuppressWarnings("unused") -public class Table { - private static final Logger logger = Logger.getLogger(Table.class); - private static final Pattern INDICATOR_FIELD_SPLIT_PATTERN = Pattern.compile("@"); - private static final int JOURNAL_STREAM_BUFFER_SIZE = 4 * 1024 * 1024; - - private final Lock lock = new ReentrantLock(); - - private final Map> indices = new ConcurrentHashMap<>(); - private final List rowListeners = new ArrayList<>(); - private final List> itemListeners = new ArrayList<>(); - - private final Class clazz; - private final String clazzSpec; - private final String indicatorField; - private final String databaseIndex; - private final Inmemo.Filter rowFilter; - - private TableUpdater tableUpdater; - private volatile boolean preloaded; - private final TLongSet ids; - - private RowRoll journal = new RowRoll(); - private boolean useJournal = true; - private static File journalsDir = new File("."); - private final AtomicInteger insertOrUpdateCount = new AtomicInteger(); - - public static void setJournalsDir(File journalsDir) { - if (!journalsDir.isDirectory()) { - throw new RuntimeException("Journals directory '" - + journalsDir.getAbsolutePath() + "' expected to be a directory."); - } - - try { - PrintWriter statusWriter = new PrintWriter(Files.newOutputStream(new File(journalsDir, "status").toPath())); - statusWriter.println(new Date()); - statusWriter.close(); - } catch (Exception e) { - throw new RuntimeException("Journals directory '" - + journalsDir.getAbsolutePath() + "' expected to be writeable.", e); - } - - Table.journalsDir = journalsDir; - } - - static File getJournalsDirForTesting() { - return journalsDir; - } - - static void setJournalsDirForTesting(File journalsDir) { - Table.journalsDir = journalsDir; - } - - Table(Class clazz, String indicatorField, Inmemo.Filter rowFilter) { - this.clazz = clazz; - if (indicatorField.contains("@")) { - String[] tokens = INDICATOR_FIELD_SPLIT_PATTERN.split(indicatorField); - this.indicatorField = tokens[0]; - this.databaseIndex = tokens[1]; - } else { - this.indicatorField = indicatorField; - this.databaseIndex = null; - } - clazzSpec = ReflectionUtil.getTableClassSpec(clazz); - ids = Inmemo.getNoSizeSupportClasses().contains(clazz) ? null : new TLongHashSet(); - this.rowFilter = rowFilter; - if (Inmemo.isJournalSupportUnset(clazz)) { - journal = null; - useJournal = false; - deleteStaleJournalFileQuietly(); - } - } - - void createUpdater(Object initialIndicatorValue) { - tableUpdater = new TableUpdater<>(this, initialIndicatorValue); - } - - void runUpdater() { - tableUpdater.start(); - } - - Class getClazz() { - return clazz; - } - - String getIndicatorField() { - return indicatorField; - } - - String getDatabaseIndex() { - return databaseIndex; - } - - String getClazzSpec() { - return clazzSpec; - } - - boolean isPreloaded() { - return preloaded; - } - - @SuppressWarnings("SameParameterValue") - void setPreloaded(boolean preloaded) { - this.preloaded = preloaded; - } - - @SuppressWarnings("BooleanMethodIsAlwaysInverted") - boolean isCompatibleItemClass(Class otherItemClass) { - return clazz == otherItemClass - || clazzSpec.equals(ReflectionUtil.getTableClassSpec(otherItemClass)); - } - - @SuppressWarnings("unchecked") - Matcher convertMatcher(final Class otherClass, final Matcher otherMatcher) { - if (clazz == otherClass) { - return (Matcher) otherMatcher; - } - - String otherClassSpec = ReflectionUtil.getTableClassSpec(otherClass); - if (clazzSpec.equals(otherClassSpec)) { - return tableItem -> { - U otherItem = ReflectionUtil.newInstance(otherClass); - ReflectionUtil.copyProperties(tableItem, otherItem); - return otherMatcher.match(otherItem); - }; - } - - throw new InmemoException(String.format( - "Can't convert matchers because the are incompatible [class=%s, otherClass=%s].", - clazz, otherClass - )); - } - - void add(Index index) { - indices.put(index.getName(), index); - index.setTable(this); - } - - void add(RowListener rowListener) { - rowListeners.add(rowListener); - } - - void add(ItemListener itemListener) { - itemListeners.add(itemListener); - } - - boolean hasInsertOrUpdateByRow() { - return !rowListeners.isEmpty(); - } - - boolean hasSize() { - return ids != null; - } - - void insertOrUpdate(@Nonnull U item, @Nullable Row row) { - Class itemClass = item.getClass(); - String itemClassSpec = ReflectionUtil.getTableClassSpec(itemClass); - - if (clazzSpec.equals(itemClassSpec)) { - T tableItem = ReflectionUtil.newInstance(clazz); - ReflectionUtil.copyProperties(item, tableItem); - if (rowFilter == null || (rowFilter.testItem(tableItem) && (row == null || rowFilter.testRow(row)))) { - internalInsertOrUpdate(tableItem, row); - } - } else { - throw new InmemoException("Table class is incompatible with the class of object [tableClass=" + clazz - + ", clazz=" + itemClass + "]."); - } - } - - void update() { - if (tableUpdater != null) { - tableUpdater.update(); - } - } - - void insertOrUpdate(Row row) { - if (rowFilter == null || rowFilter.testRow(row)) { - for (RowListener rowListener : rowListeners) { - rowListener.insertOrUpdate(row); - } - } - } - - int size() { - if (ids == null) { - throw new UnsupportedOperationException("The operation is unsupported due Inmemo.unsetSizeSupport(clazz)."); - } - - lock.lock(); - try { - return ids.size(); - } finally { - lock.unlock(); - } - } - - private void internalInsertOrUpdate(@Nonnull T item, @Nullable Row row) { - lock.lock(); - try { - if (journal != null && row != null) { - journal.addRow(row); - } - - if (ids != null) { - ids.add(item.getId()); - } - - for (Index index : indices.values()) { - index.insertOrUpdate(item); - } - for (ItemListener itemListener : itemListeners) { - itemListener.insertOrUpdate(item); - } - } finally { - lock.unlock(); - } - - int count = insertOrUpdateCount.incrementAndGet(); - if (count % 100000 == 0) { - logger.info("Inmemo: table " + ReflectionUtil.getTableClassName(getClazz()) - + " insertOrUpdateCount=" + count + "."); - } - } - - List find(IndexConstraint indexConstraint, Matcher predicate) { - if (indexConstraint == null) { - throw new InmemoException("Nonnul IndexConstraint is required [tableClass=" - + ReflectionUtil.getTableClassName(clazz) + "]."); - } - - Index index = indices.get(indexConstraint.getIndexName()); - if (index == null) { - throw new IllegalArgumentException("Unexpected index name `" + indexConstraint.getIndexName() + "`."); - } - - return index.find(indexConstraint.getValue(), predicate); - } - - T findOnly(boolean throwOnNotUnique, IndexConstraint indexConstraint, Matcher predicate) { - if (indexConstraint == null) { - throw new InmemoException("Nonnul IndexConstraint is required [tableClass=" - + ReflectionUtil.getTableClassName(clazz) + "]."); - } - - Index index = indices.get(indexConstraint.getIndexName()); - if (index == null) { - throw new IllegalArgumentException("Unexpected index name `" + indexConstraint.getIndexName() + "`."); - } - - return index.findOnly(throwOnNotUnique, indexConstraint.getValue(), predicate); - } - - long findCount(IndexConstraint indexConstraint, Matcher predicate) { - if (indexConstraint == null) { - throw new InmemoException("Nonnul IndexConstraint is required [tableClass=" - + ReflectionUtil.getTableClassName(clazz) + "]."); - } - - Index index = indices.get(indexConstraint.getIndexName()); - if (index == null) { - throw new IllegalArgumentException("Unexpected index name `" + indexConstraint.getIndexName() + "`."); - } - - return index.findCount(indexConstraint.getValue(), predicate); - } - - void insertOrUpdateByIds(Long[] ids) { - for (Long id : ids) { - if (id == null) { - continue; - } - - tableUpdater.insertOrUpdateById(id); - } - } - - List findAndUpdateByEmergencyQueryFields(Object[] fields) { - return tableUpdater.findAndUpdateByEmergencyQueryFields(fields); - } - - void logBucketStats() { - lock.lock(); - try { - for (Index index : indices.values()) { - Index.BucketStats bucketStats = index.getBucketStats(); - if (bucketStats == null) { - continue; - } - - logger.info("Inmemo bucket stats [table=" - + ReflectionUtil.getTableClassName(clazz) - + ", index=" - + index.getName() - + ", buckets=" - + bucketStats.getBucketCount() - + ", totalBucketSize=" - + bucketStats.getTotalBucketSize() - + ", avgBucketSize=" - + String.format(Locale.US, "%.2f", bucketStats.getAverageBucketSize()) - + ", maxBucketSize=" - + bucketStats.getMaxBucketSize() - + "]."); - } - } finally { - lock.unlock(); - } - } - - void deleteJournal() throws IOException { - File journalFile = new File(journalsDir, getInmemoFilename()); - if (journalFile.isFile()) { - if (!journalFile.delete()) { - String message = "Journal has not been deleted [table='" + clazz.getSimpleName() + "']."; - logger.error(message); - throw new IOException(message); - } - } - } - - private void deleteStaleJournalFileQuietly() { - try { - deleteJournal(); - logger.info("Journal support is disabled for table '" + clazz.getSimpleName() - + "', stale journal file (if any) has been deleted."); - } catch (IOException | RuntimeException e) { - logger.error("Journal support is disabled for table '" + clazz.getSimpleName() - + "', but failed to delete stale journal file.", e); - } - } - - private String getInmemoFilename() { - return clazz.getSimpleName() + ".inmemo"; - } - - void writeJournal() throws IOException { - if (useJournal && journal != null) { - if (journal.isEmpty()) { - logger.warn("Journal has not been dumped because of empty journal [table='" + clazz.getSimpleName() + "']."); - return; - } - - File journalFile = new File(journalsDir, getInmemoFilename()); - lock.lock(); - try { - long startTimeMillis = System.currentTimeMillis(); - try (OutputStream outputStream = new SnappyOutputStream(new BufferedOutputStream( - Files.newOutputStream(journalFile.toPath()), JOURNAL_STREAM_BUFFER_SIZE))) { - ArrayMap.writeRowRoll(outputStream, journal); - } - long writeRowRollTimeMillis = System.currentTimeMillis() - startTimeMillis; - logger.info("Journal binary data has been prepared and written in " - + writeRowRollTimeMillis + " ms [table='" + clazz.getSimpleName() - + "', size=" + journal.size() + ", bytes=" + journalFile.length() + "]."); - journal = null; - } finally { - lock.unlock(); - } - } - } - - RowRoll readJournal() { - if (!useJournal) { - return null; - } - - File journalFile = new File(journalsDir, getInmemoFilename()); - if (journalFile.isFile()) { - long startTimeMillis = System.currentTimeMillis(); - InputStream inputStream = null; - try { - inputStream = new SnappyInputStream(new BufferedInputStream( - Files.newInputStream(journalFile.toPath()), JOURNAL_STREAM_BUFFER_SIZE)); - RowRoll rowRoll = ArrayMap.readRowRoll(inputStream); - long durationTimeMillis = System.currentTimeMillis() - startTimeMillis; - logger.info("Journal binary data has been read and parsed in " - + durationTimeMillis + " ms [table='" + clazz.getSimpleName() - + "', bytes=" + journalFile.length() + "]."); - return rowRoll; - } catch (ClassCastException e) { - logger.error("ClassCastException: Unable to read journal [table='" + clazz.getSimpleName() + "'].", e); - return null; - } catch (IOException e) { - logger.error("IOException: Unable to read journal [table='" + clazz.getSimpleName() + "'].", e); - return null; - } finally { - if (inputStream != null) { - try { - inputStream.close(); - } catch (IOException e) { - logger.error("Unable to close journal after reading [table='" + clazz.getSimpleName() + "'].", e); - // No operations. - } - } - } - } else { - logger.info("Can't read journal because of no file '" + journalFile + "'."); - } - - return null; - } - - boolean isUseJournal() { - return useJournal; - } - - /* init. */ { - if (!"true".equals(System.getProperty("Inmemo.UseJournal"))) { - journal = null; - useJournal = false; - } - } -} +package com.codeforces.inmemo; + +import gnu.trove.set.TLongSet; +import gnu.trove.set.hash.TLongHashSet; +import org.apache.log4j.Logger; +import org.jacuzzi.core.ArrayMap; +import org.jacuzzi.core.Row; +import org.jacuzzi.core.RowRoll; +import org.xerial.snappy.SnappyInputStream; +import org.xerial.snappy.SnappyOutputStream; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import java.io.*; +import java.nio.file.Files; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; +import java.util.regex.Pattern; + +/** + * @author Mike Mirzayanov (mirzayanovmr@gmail.com) + */ +@SuppressWarnings("unused") +public class Table { + private static final Logger logger = Logger.getLogger(Table.class); + private static final Pattern INDICATOR_FIELD_SPLIT_PATTERN = Pattern.compile("@"); + private static final int JOURNAL_STREAM_BUFFER_SIZE = 4 * 1024 * 1024; + + private final Lock lock = new ReentrantLock(); + + private final Map> indices = new ConcurrentHashMap<>(); + private final List rowListeners = new ArrayList<>(); + private final List> itemListeners = new ArrayList<>(); + + private final Class clazz; + private final String clazzSpec; + private final String indicatorField; + private final String databaseIndex; + private final Inmemo.Filter rowFilter; + + private TableUpdater tableUpdater; + private volatile boolean preloaded; + private final TLongSet ids; + + private RowRoll journal = new RowRoll(); + private boolean useJournal = true; + private static File journalsDir = new File("."); + private final AtomicInteger insertOrUpdateCount = new AtomicInteger(); + + public static void setJournalsDir(File journalsDir) { + if (!journalsDir.isDirectory()) { + throw new RuntimeException("Journals directory '" + + journalsDir.getAbsolutePath() + "' expected to be a directory."); + } + + try { + PrintWriter statusWriter = new PrintWriter(Files.newOutputStream(new File(journalsDir, "status").toPath())); + statusWriter.println(new Date()); + statusWriter.close(); + } catch (Exception e) { + throw new RuntimeException("Journals directory '" + + journalsDir.getAbsolutePath() + "' expected to be writeable.", e); + } + + Table.journalsDir = journalsDir; + } + + static File getJournalsDirForTesting() { + return journalsDir; + } + + static void setJournalsDirForTesting(File journalsDir) { + Table.journalsDir = journalsDir; + } + + Table(Class clazz, String indicatorField, Inmemo.Filter rowFilter) { + this.clazz = clazz; + if (indicatorField.contains("@")) { + String[] tokens = INDICATOR_FIELD_SPLIT_PATTERN.split(indicatorField); + this.indicatorField = tokens[0]; + this.databaseIndex = tokens[1]; + } else { + this.indicatorField = indicatorField; + this.databaseIndex = null; + } + clazzSpec = ReflectionUtil.getTableClassSpec(clazz); + ids = Inmemo.getNoSizeSupportClasses().contains(clazz) ? null : new TLongHashSet(); + this.rowFilter = rowFilter; + if (Inmemo.isJournalSupportUnset(clazz)) { + journal = null; + useJournal = false; + deleteStaleJournalFileQuietly(); + } + } + + void createUpdater(Object initialIndicatorValue) { + tableUpdater = new TableUpdater<>(this, initialIndicatorValue); + } + + void runUpdater() { + tableUpdater.start(); + } + + Class getClazz() { + return clazz; + } + + String getIndicatorField() { + return indicatorField; + } + + String getDatabaseIndex() { + return databaseIndex; + } + + String getClazzSpec() { + return clazzSpec; + } + + boolean isPreloaded() { + return preloaded; + } + + @SuppressWarnings("SameParameterValue") + void setPreloaded(boolean preloaded) { + this.preloaded = preloaded; + } + + @SuppressWarnings("BooleanMethodIsAlwaysInverted") + boolean isCompatibleItemClass(Class otherItemClass) { + return clazz == otherItemClass + || clazzSpec.equals(ReflectionUtil.getTableClassSpec(otherItemClass)); + } + + @SuppressWarnings("unchecked") + Matcher convertMatcher(final Class otherClass, final Matcher otherMatcher) { + if (clazz == otherClass) { + return (Matcher) otherMatcher; + } + + String otherClassSpec = ReflectionUtil.getTableClassSpec(otherClass); + if (clazzSpec.equals(otherClassSpec)) { + return tableItem -> { + U otherItem = ReflectionUtil.newInstance(otherClass); + ReflectionUtil.copyProperties(tableItem, otherItem); + return otherMatcher.match(otherItem); + }; + } + + throw new InmemoException(String.format( + "Can't convert matchers because the are incompatible [class=%s, otherClass=%s].", + clazz, otherClass + )); + } + + void add(Index index) { + indices.put(index.getName(), index); + index.setTable(this); + } + + void add(RowListener rowListener) { + rowListeners.add(rowListener); + } + + void add(ItemListener itemListener) { + itemListeners.add(itemListener); + } + + boolean hasInsertOrUpdateByRow() { + return !rowListeners.isEmpty(); + } + + boolean hasSize() { + return ids != null; + } + + void insertOrUpdate(@Nonnull U item, @Nullable Row row) { + Class itemClass = item.getClass(); + String itemClassSpec = ReflectionUtil.getTableClassSpec(itemClass); + + if (clazzSpec.equals(itemClassSpec)) { + T tableItem = ReflectionUtil.newInstance(clazz); + ReflectionUtil.copyProperties(item, tableItem); + if (rowFilter == null || (rowFilter.testItem(tableItem) && (row == null || rowFilter.testRow(row)))) { + internalInsertOrUpdate(tableItem, row); + } + } else { + throw new InmemoException("Table class is incompatible with the class of object [tableClass=" + clazz + + ", clazz=" + itemClass + "]."); + } + } + + void update() { + if (tableUpdater != null) { + tableUpdater.update(); + } + } + + void insertOrUpdate(Row row) { + if (rowFilter == null || rowFilter.testRow(row)) { + for (RowListener rowListener : rowListeners) { + rowListener.insertOrUpdate(row); + } + } + } + + int size() { + if (ids == null) { + throw new UnsupportedOperationException("The operation is unsupported due Inmemo.unsetSizeSupport(clazz)."); + } + + lock.lock(); + try { + return ids.size(); + } finally { + lock.unlock(); + } + } + + private void internalInsertOrUpdate(@Nonnull T item, @Nullable Row row) { + lock.lock(); + try { + if (journal != null && row != null) { + journal.addRow(row); + } + + if (ids != null) { + ids.add(item.getId()); + } + + for (Index index : indices.values()) { + index.insertOrUpdate(item); + } + for (ItemListener itemListener : itemListeners) { + itemListener.insertOrUpdate(item); + } + } finally { + lock.unlock(); + } + + int count = insertOrUpdateCount.incrementAndGet(); + if (count % 100000 == 0) { + logger.info("Inmemo: table " + ReflectionUtil.getTableClassName(getClazz()) + + " insertOrUpdateCount=" + count + "."); + } + } + + List find(IndexConstraint indexConstraint, Matcher predicate) { + if (indexConstraint == null) { + throw new InmemoException("Nonnul IndexConstraint is required [tableClass=" + + ReflectionUtil.getTableClassName(clazz) + "]."); + } + + Index index = indices.get(indexConstraint.getIndexName()); + if (index == null) { + throw new IllegalArgumentException("Unexpected index name `" + indexConstraint.getIndexName() + "`."); + } + + return index.find(indexConstraint.getValue(), predicate); + } + + T findOnly(boolean throwOnNotUnique, IndexConstraint indexConstraint, Matcher predicate) { + if (indexConstraint == null) { + throw new InmemoException("Nonnul IndexConstraint is required [tableClass=" + + ReflectionUtil.getTableClassName(clazz) + "]."); + } + + Index index = indices.get(indexConstraint.getIndexName()); + if (index == null) { + throw new IllegalArgumentException("Unexpected index name `" + indexConstraint.getIndexName() + "`."); + } + + return index.findOnly(throwOnNotUnique, indexConstraint.getValue(), predicate); + } + + long findCount(IndexConstraint indexConstraint, Matcher predicate) { + if (indexConstraint == null) { + throw new InmemoException("Nonnul IndexConstraint is required [tableClass=" + + ReflectionUtil.getTableClassName(clazz) + "]."); + } + + Index index = indices.get(indexConstraint.getIndexName()); + if (index == null) { + throw new IllegalArgumentException("Unexpected index name `" + indexConstraint.getIndexName() + "`."); + } + + return index.findCount(indexConstraint.getValue(), predicate); + } + + void insertOrUpdateByIds(Long[] ids) { + for (Long id : ids) { + if (id == null) { + continue; + } + + tableUpdater.insertOrUpdateById(id); + } + } + + List findAndUpdateByEmergencyQueryFields(Object[] fields) { + return tableUpdater.findAndUpdateByEmergencyQueryFields(fields); + } + + void logBucketStats() { + lock.lock(); + try { + for (Index index : indices.values()) { + Index.BucketStats bucketStats = index.getBucketStats(); + if (bucketStats == null) { + continue; + } + + logger.info("Inmemo bucket stats [table=" + + ReflectionUtil.getTableClassName(clazz) + + ", index=" + + index.getName() + + ", buckets=" + + bucketStats.getBucketCount() + + ", totalBucketSize=" + + bucketStats.getTotalBucketSize() + + ", avgBucketSize=" + + String.format(Locale.US, "%.2f", bucketStats.getAverageBucketSize()) + + ", maxBucketSize=" + + bucketStats.getMaxBucketSize() + + "]."); + } + } finally { + lock.unlock(); + } + } + + void deleteJournal() throws IOException { + File journalFile = new File(journalsDir, getInmemoFilename()); + if (journalFile.isFile()) { + if (!journalFile.delete()) { + String message = "Journal has not been deleted [table='" + clazz.getSimpleName() + "']."; + logger.error(message); + throw new IOException(message); + } + } + } + + private void deleteStaleJournalFileQuietly() { + try { + deleteJournal(); + logger.info("Journal support is disabled for table '" + clazz.getSimpleName() + + "', stale journal file (if any) has been deleted."); + } catch (IOException | RuntimeException e) { + logger.error("Journal support is disabled for table '" + clazz.getSimpleName() + + "', but failed to delete stale journal file.", e); + } + } + + private String getInmemoFilename() { + return clazz.getSimpleName() + ".inmemo"; + } + + void writeJournal() throws IOException { + if (useJournal && journal != null) { + if (journal.isEmpty()) { + logger.warn("Journal has not been dumped because of empty journal [table='" + clazz.getSimpleName() + "']."); + return; + } + + File journalFile = new File(journalsDir, getInmemoFilename()); + lock.lock(); + try { + long startTimeMillis = System.currentTimeMillis(); + try (OutputStream outputStream = new SnappyOutputStream(new BufferedOutputStream( + Files.newOutputStream(journalFile.toPath()), JOURNAL_STREAM_BUFFER_SIZE))) { + ArrayMap.writeRowRoll(outputStream, journal); + } + long writeRowRollTimeMillis = System.currentTimeMillis() - startTimeMillis; + logger.info("Journal binary data has been prepared and written in " + + writeRowRollTimeMillis + " ms [table='" + clazz.getSimpleName() + + "', size=" + journal.size() + ", bytes=" + journalFile.length() + "]."); + journal = null; + } finally { + lock.unlock(); + } + } + } + + RowRoll readJournal() { + if (!useJournal) { + return null; + } + + File journalFile = new File(journalsDir, getInmemoFilename()); + if (journalFile.isFile()) { + long startTimeMillis = System.currentTimeMillis(); + InputStream inputStream = null; + try { + inputStream = new SnappyInputStream(new BufferedInputStream( + Files.newInputStream(journalFile.toPath()), JOURNAL_STREAM_BUFFER_SIZE)); + RowRoll rowRoll = ArrayMap.readRowRoll(inputStream); + long durationTimeMillis = System.currentTimeMillis() - startTimeMillis; + logger.info("Journal binary data has been read and parsed in " + + durationTimeMillis + " ms [table='" + clazz.getSimpleName() + + "', bytes=" + journalFile.length() + "]."); + return rowRoll; + } catch (ClassCastException e) { + logger.error("ClassCastException: Unable to read journal [table='" + clazz.getSimpleName() + "'].", e); + return null; + } catch (IOException e) { + logger.error("IOException: Unable to read journal [table='" + clazz.getSimpleName() + "'].", e); + return null; + } finally { + if (inputStream != null) { + try { + inputStream.close(); + } catch (IOException e) { + logger.error("Unable to close journal after reading [table='" + clazz.getSimpleName() + "'].", e); + // No operations. + } + } + } + } else { + logger.info("Can't read journal because of no file '" + journalFile + "'."); + } + + return null; + } + + boolean isUseJournal() { + return useJournal; + } + + /* init. */ { + if (!"true".equals(System.getProperty("Inmemo.UseJournal"))) { + journal = null; + useJournal = false; + } + } +} diff --git a/src/main/java/com/codeforces/inmemo/TableUpdater.java b/src/main/java/com/codeforces/inmemo/TableUpdater.java index 41b70d6..e41e0db 100644 --- a/src/main/java/com/codeforces/inmemo/TableUpdater.java +++ b/src/main/java/com/codeforces/inmemo/TableUpdater.java @@ -1,568 +1,568 @@ -package com.codeforces.inmemo; - -import org.apache.log4j.Level; -import org.apache.log4j.Logger; -import org.jacuzzi.core.Jacuzzi; -import org.jacuzzi.core.Row; -import org.jacuzzi.core.RowRoll; -import org.jacuzzi.core.TypeOracle; - -import javax.sql.DataSource; -import java.io.IOException; -import java.util.*; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.ThreadLocalRandom; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicInteger; -import java.util.concurrent.atomic.AtomicReference; -import java.util.concurrent.locks.Lock; -import java.util.concurrent.locks.ReentrantLock; - -/** - * @author MikeMirzayanov (mirzayanovmr@gmail.com) - */ -class TableUpdater { - private static final Logger logger = Logger.getLogger(TableUpdater.class); - - private static final List tableUpdaterThreads - = Collections.synchronizedList(new ArrayList<>()); - - private static final AtomicInteger tableUpdaterThreadCount - = new AtomicInteger(0); - - /** - * Delay after meaningless update try. - */ - private static final long RESCAN_TIME_MILLIS = 750; - - private static final int MAX_ROWS_IN_SINGLE_SQL_STATEMENT = 2_000_000; - private static final int MAX_UPDATE_SAME_INDICATOR_TIMES = 10; - - private final Lock updateLock = new ReentrantLock(); - - private static DataSource dataSource; - private static final Map dataSourceByClazzName = new ConcurrentHashMap<>(); - private static final Collection> instances - = Collections.synchronizedList(new ArrayList<>()); - - private final Table table; - private final Thread thread; - private final String threadName; - private volatile boolean running; - - private final Jacuzzi jacuzzi; - private final TypeOracle typeOracle; - - private final AtomicReference lastIndicatorValue = new AtomicReference<>(); - private final long startTimeMillis; - - private final Map lastEntityIdsUpdateCount = new ConcurrentHashMap<>(); - - TableUpdater(Table table, Object initialIndicatorValue) { - if (dataSource == null) { - logger.error("It should be called static Inmemo#setDataSource()" - + " before any instance of TableUpdater."); - throw new InmemoException("It should be called static Inmemo#setDataSource()" - + " before any instance of TableUpdater."); - } - - this.table = table; - this.lastIndicatorValue.set(initialIndicatorValue); - - DataSource clazzDataSource = dataSourceByClazzName.get(table.getClazz().getName()); - jacuzzi = Jacuzzi.getJacuzzi(clazzDataSource == null ? dataSource : clazzDataSource); - - typeOracle = TypeOracle.getTypeOracle(table.getClazz()); - - threadName = "InmemoUpdater#" + table.getClazz(); - thread = new Thread(new TableUpdaterRunnable(this), threadName); - thread.setDaemon(true); - tableUpdaterThreads.add(thread); - - logger.info("Started Inmemo table updater thread '" + threadName + "'."); - startTimeMillis = System.currentTimeMillis(); - - //noinspection ThisEscapedInObjectConstruction - instances.add(this); - } - - @SuppressWarnings("UnusedDeclaration") - static void setDataSource(DataSource dataSource) { - TableUpdater.dataSource = dataSource; - } - - @SuppressWarnings("UnusedDeclaration") - static void stop() { - synchronized (instances) { - for (TableUpdater instance : instances) { - instance.running = false; - } - } - } - - void start() { - running = true; - thread.start(); - } - - private int getMaxUpdateSameIndicatorTimes() { - if (table.isPreloaded()) { - return MAX_UPDATE_SAME_INDICATOR_TIMES; - } else { - return 1; - } - } - - void insertOrUpdateById(Long id) { - if (id == null) { - return; - } - - RowRoll rows = jacuzzi.findRowRoll("SELECT * FROM " - + typeOracle.getTableName() - + " WHERE " - + typeOracle.getIdColumn() - + " = " - + id); - - if (rows == null || rows.isEmpty()) { - return; - } - - if (rows.size() == 1) { - Row row = rows.getRow(0); - T entity = typeOracle.convertFromRow(row); - - table.insertOrUpdate(entity, row); - table.insertOrUpdate(row); - } else { - throw new InmemoException("Expected at most one item of " - + table.getClazz() + " with id = " + id + '.'); - } - } - - List findAndUpdateByEmergencyQueryFields(Object[] fields) { - validateFieldsArray(fields); - - String[] fieldNames = new String[fields.length / 2]; - Object[] fieldValues = new Object[fields.length / 2]; - - for (int index = 0; index < fields.length; index += 2) { - fieldNames[index / 2] = (String) fields[index]; - fieldValues[index / 2] = fields[index + 1]; - } - - String formattedFields = typeOracle.getQueryFindSql(fieldNames); - - RowRoll rows = jacuzzi.findRowRoll("SELECT * FROM " - + typeOracle.getTableName() - + " WHERE " - + formattedFields - + " ORDER BY " - + typeOracle.getIdColumn(), fieldValues); - - if (rows == null || rows.isEmpty()) { - return Collections.emptyList(); - } - - logger.warn("Emergency case: found " - + rows.size() - + " items of class " - + table.getClazz().getName() - + " [fields=" - + formattedFields - + "]."); - - List result = new ArrayList<>(rows.size()); - for (int i = 0; i < rows.size(); i++) { - Row row = rows.getRow(i); - - T entity = typeOracle.convertFromRow(row); - logger.warn("Emergency found: " - + table.getClazz().getName() - + " id=" - + entity.getId() - + " [fields=" - + formattedFields - + "]."); - - result.add(entity); - - table.insertOrUpdate(entity, row); - table.insertOrUpdate(row); - } - - return result; - } - - private static void validateFieldsArray(Object[] fields) { - if (fields.length % 2 != 0) { - throw new IllegalArgumentException("EmergencyQueryFields array should have" - + " even length. Found: " - + Arrays.toString(fields) + '.'); - } - - for (int index = 0; index < fields.length; index += 2) { - Object field = fields[index]; - if (!(field instanceof String) || ((String) field).isEmpty()) { - throw new IllegalArgumentException("EmergencyQueryFields array must contain" - + " non-empty strings on even positions. Found: " - + Arrays.toString(fields) + '.'); - } - } - } - - void update() { - if (running) { - internalUpdate(); - } - } - - private void randomizedUpdate() { - List updatedIds = internalUpdate(); - sleepBetweenRescans(updatedIds.size()); - } - - private List internalUpdate() { - updateLock.lock(); - - try { - long startTimeMillis = System.currentTimeMillis(); - Object prevLastIndicatorValue = lastIndicatorValue.get(); - RowRoll rows = getRecentlyChangedRows(prevLastIndicatorValue); - - long afterGetRecentlyChangedRowsMillis = System.currentTimeMillis(); - long getRecentlyChangedMillis = afterGetRecentlyChangedRowsMillis - startTimeMillis; - - if (rows.size() >= 100 - || getRecentlyChangedMillis >= TimeUnit.SECONDS.toMillis(1)) { - logger.error("Table '" - + table.getClazz().getSimpleName() - + "': getRecentlyChangedRows returns " - + rows.size() - + " rows [prevLastIndicatorValue=" - + prevLastIndicatorValue - + ", lastIndicatorValue=" - + lastIndicatorValue.get() - + ", thread=" - + threadName - + ", time=" + getRecentlyChangedMillis + " ms]."); - } - - boolean hasInsertOrUpdateByRow = table.hasInsertOrUpdateByRow(); - List updatedIds = new ArrayList<>(); - - int idColumn = getIdColumn(rows); - int indicatorFieldColumn = rows.getColumn(table.getIndicatorField()); - - for (int i = 0; i < rows.size(); i++) { - long id = (long) rows.getValue(i, idColumn); - - if (Objects.equals(rows.getValue(i, indicatorFieldColumn), prevLastIndicatorValue) - && lastEntityIdsUpdateCount.containsKey(id) - && lastEntityIdsUpdateCount.get(id) >= getMaxUpdateSameIndicatorTimes()) { - continue; - } - - // Insert or update entity. - Row row = rows.getRow(i); - T entity = typeOracle.convertFromRow(row); - table.insertOrUpdate(entity, row); - - updatedIds.add(id); - - // Insert or update row. - if (hasInsertOrUpdateByRow) { - table.insertOrUpdate(row); - } - - if (i > 0 && i % 100000 == 0) { - logger.warn("Inserted " - + i - + " rows in a batch [table=" - + table.getClazz().getSimpleName() - + "]."); - } - - if (table.hasSize()) { - int tableSize = table.size(); - if (tableSize > 0 && tableSize % 100000 == 0) { - logger.warn("Table " - + table.getClazz().getSimpleName() - + " contains now " - + tableSize - + " rows."); - } - } - - lastIndicatorValue.set(row.get(table.getIndicatorField())); - } - - if (updatedIds.size() >= 10) { - logger.info("Thread '" - + threadName - + "' has found " - + rows.size() - + "(" - + updatedIds.size() - + ") rows to update in " - + getRecentlyChangedMillis - + " ms [prevLastIndicatorValue=" - + prevLastIndicatorValue - + ", lastIndicatorValue=" - + lastIndicatorValue.get() - + "]."); - - if (updatedIds.size() <= 100) { - StringBuilder ids = new StringBuilder(); - for (long id : updatedIds) { - if (ids.length() > 0) { - ids.append(','); - } - ids.append(id); - } - logger.info("Updated entries have id=" + ids + '.'); - } - - logger.info("Thread '" - + threadName - + "' has updated " - + updatedIds.size() - + " items in " - + (System.currentTimeMillis() - startTimeMillis) - + " ms [prevLastIndicatorValue=" - + prevLastIndicatorValue - + ", lastIndicatorValue=" - + lastIndicatorValue.get() - + "]."); - } - - if (updatedIds.isEmpty() && !table.isPreloaded()) { - if (table.hasSize()) { - logger.info("Inmemo ready to dump journal of table " + ReflectionUtil.getTableClassName(table.getClazz()) - + " [items=" + table.size() + "]."); - } - long totalTimeMillis = System.currentTimeMillis() - this.startTimeMillis; - try { - table.writeJournal(); - } catch (IOException e) { - logger.error("Inmemo failed to dump journal of table " + ReflectionUtil.getTableClassName(table.getClazz()) - + " [items=" + table.size() + "] in " + totalTimeMillis + " ms.", e); - } - if (table.hasSize()) { - logger.log(totalTimeMillis < TimeUnit.SECONDS.toMillis(1) ? Level.INFO : Level.WARN, "Inmemo preloaded " + ReflectionUtil.getTableClassName(table.getClazz()) - + " [items=" + table.size() + "] in " + totalTimeMillis + " ms."); - } else { - logger.log(totalTimeMillis < TimeUnit.SECONDS.toMillis(1) ? Level.INFO : Level.WARN, "Inmemo preloaded " + ReflectionUtil.getTableClassName(table.getClazz()) - + " in " + totalTimeMillis + " ms."); - } - table.logBucketStats(); - table.setPreloaded(true); - } - - Object newLastIndicatorValue = lastIndicatorValue.get(); - if (!Objects.equals(prevLastIndicatorValue, newLastIndicatorValue)) { - lastEntityIdsUpdateCount.clear(); - } - List trulyUpdatedIds = new ArrayList<>(updatedIds.size()); - - for (int i = 0; i < rows.size(); i++) { - if (Objects.equals(rows.getValue(i, indicatorFieldColumn), newLastIndicatorValue)) { - long id = (long) rows.getValue(i, idColumn); - Integer updateCount = lastEntityIdsUpdateCount.get(id); - if (updateCount == null) { - trulyUpdatedIds.add(id); - updateCount = 1; - lastEntityIdsUpdateCount.put(id, updateCount); - } else if (updateCount < getMaxUpdateSameIndicatorTimes()) { - updateCount++; - lastEntityIdsUpdateCount.put(id, updateCount); - } - } - } - - return trulyUpdatedIds; - } finally { - updateLock.unlock(); - } - } - - private static int getIdColumn(RowRoll rowRoll) { - int column = rowRoll.getColumn("id"); - if (column == -1) { - column = rowRoll.getColumn("ID"); - } - return column; - } - - private void sleepBetweenRescans(int updatedCount) { - long rescanTimeMillis = getRescanTimeMillis(); - - if (updatedCount == 0) { - sleep((4 * rescanTimeMillis / 5) - + ThreadLocalRandom.current().nextInt((int) (rescanTimeMillis / 5))); - } else if (updatedCount * 2 > MAX_ROWS_IN_SINGLE_SQL_STATEMENT) { - logger.info("Thread '" - + threadName - + "' will not sleep because it updated near maximum row count."); - } else { - sleep(ThreadLocalRandom.current().nextInt((int) (rescanTimeMillis / 5))); - } - } - - private long getRescanTimeMillis() { - return RESCAN_TIME_MILLIS; - } - - private void sleep(long timeMillis) { - try { - Thread.sleep(timeMillis); - } catch (InterruptedException e) { - logger.error("Thread '" + threadName + "' has been stopped because of InterruptedException.", e); - running = false; - } - } - - private RowRoll getRecentlyChangedRows(Object indicatorLastValue) { - long startTimeMillis = System.currentTimeMillis(); - RowRoll rows = null; - - if (lastIndicatorValue.get() == null && table.isUseJournal()) { - RowRoll journalRows = table.readJournal(); - if (journalRows != null && !journalRows.isEmpty()) { - rows = journalRows; - } - } - - if (rows != null) { - logger.info("getRecentlyChangedRows loads data of using the journal in " - + (System.currentTimeMillis() - startTimeMillis) - + " ms [table=" + table.getClazz().getSimpleName() + "]."); - return rows; - } - - String forceIndexClause = table.getDatabaseIndex() == null ? "" : ("FORCE INDEX (" + table.getDatabaseIndex() + ')'); - - if (indicatorLastValue == null) { - rows = jacuzzi.findRowRoll("SELECT * FROM " - + typeOracle.getTableName() - + ' ' - + forceIndexClause - + " ORDER BY " - + table.getIndicatorField() - + ", " - + typeOracle.getIdColumn() - + " LIMIT " - + MAX_ROWS_IN_SINGLE_SQL_STATEMENT); - } else { - rows = jacuzzi.findRowRoll("SELECT * FROM " - + typeOracle.getTableName() - + ' ' - + forceIndexClause - + " WHERE " - + table.getIndicatorField() - + " >= ? ORDER BY " - + table.getIndicatorField() - + ", " - + typeOracle.getIdColumn() - + " LIMIT " - + MAX_ROWS_IN_SINGLE_SQL_STATEMENT, - indicatorLastValue - ); - } - - long queryTimeMillis = System.currentTimeMillis() - startTimeMillis; - if (queryTimeMillis * 10 > getRescanTimeMillis()) { - logger.warn("Rescanning query for entity `" - + table.getClazz().getName() - + "` took too long time " - + queryTimeMillis - + " ms."); - } - return rows; - } - - @SuppressWarnings("WeakerAccess") - public static void setSpecificDataSource(Class clazz, DataSource dataSource) { - String clazzName = clazz.getName(); - logger.info("Setting specific data source for [clazz=" + clazzName + "]."); - dataSourceByClazzName.put(clazzName, dataSource); - } - - private static final class TableUpdaterRunnable implements Runnable { - private final TableUpdater tableUpdater; - - private TableUpdaterRunnable(TableUpdater tableUpdater) { - this.tableUpdater = tableUpdater; - } - - @Override - public void run() { - tableUpdaterThreadCount.incrementAndGet(); - while (tableUpdater.running) { - try { - tableUpdater.randomizedUpdate(); - } catch (Exception e) { - logger.error("Unexpected " - + e.getClass().getName() - + " exception in TableUpdaterRunnable of " - + tableUpdater.threadName - + ": " + e, e); - } - } - - tableUpdaterThreadCount.decrementAndGet(); - - logger.warn("Inmemo update thread for " - + tableUpdater.table.getClazz().getName() - + " finished."); - } - } - - private static final class TableUpdaterThreadsPrinterRunnable implements Runnable { - @SuppressWarnings("BusyWait") - @Override - public void run() { - while (!Thread.currentThread().isInterrupted()) { - try { - Thread.sleep(TimeUnit.SECONDS.toMillis(300)); - } catch (InterruptedException e) { - logger.warn("TableUpdaterThreadsPrinterRunnable stopped because of" - + " InterruptedException."); - break; - } - - if (tableUpdaterThreadCount.get() == 0) { - logger.warn("TableUpdaterThreadsPrinterRunnable stopped because of" - + " `tableUpdaterThreadCount.get() == 0`."); - break; - } - - synchronized (tableUpdaterThreads) { - logger.info("tableUpdaterThreads.size()=" + tableUpdaterThreads.size() + "."); - for (Thread tableUpdaterRunnable : tableUpdaterThreads) { - printStackTrace(tableUpdaterRunnable); - } - } - } - } - - private void printStackTrace(Thread thread) { - StringBuilder result = new StringBuilder(); - StackTraceElement[] elements = thread.getStackTrace(); - for (StackTraceElement element : elements) { - result.append(thread.getName()).append(": ").append(element.toString()).append('\n'); - } - result.append("\n\n"); - logger.info(thread.getName() + " stack trace: " + result); - } - } - - static { - Thread tableUpdaterThreadsPrinterThread = new Thread(new TableUpdaterThreadsPrinterRunnable()); - tableUpdaterThreadsPrinterThread.setDaemon(true); - tableUpdaterThreadsPrinterThread.setUncaughtExceptionHandler((t, e) - -> logger.error("Uncaught exception [thread=" + t + ", exception=" + e + "].")); - tableUpdaterThreadsPrinterThread.start(); - } -} +package com.codeforces.inmemo; + +import org.apache.log4j.Level; +import org.apache.log4j.Logger; +import org.jacuzzi.core.Jacuzzi; +import org.jacuzzi.core.Row; +import org.jacuzzi.core.RowRoll; +import org.jacuzzi.core.TypeOracle; + +import javax.sql.DataSource; +import java.io.IOException; +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ThreadLocalRandom; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicReference; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; + +/** + * @author MikeMirzayanov (mirzayanovmr@gmail.com) + */ +class TableUpdater { + private static final Logger logger = Logger.getLogger(TableUpdater.class); + + private static final List tableUpdaterThreads + = Collections.synchronizedList(new ArrayList<>()); + + private static final AtomicInteger tableUpdaterThreadCount + = new AtomicInteger(0); + + /** + * Delay after meaningless update try. + */ + private static final long RESCAN_TIME_MILLIS = 750; + + private static final int MAX_ROWS_IN_SINGLE_SQL_STATEMENT = 2_000_000; + private static final int MAX_UPDATE_SAME_INDICATOR_TIMES = 10; + + private final Lock updateLock = new ReentrantLock(); + + private static DataSource dataSource; + private static final Map dataSourceByClazzName = new ConcurrentHashMap<>(); + private static final Collection> instances + = Collections.synchronizedList(new ArrayList<>()); + + private final Table table; + private final Thread thread; + private final String threadName; + private volatile boolean running; + + private final Jacuzzi jacuzzi; + private final TypeOracle typeOracle; + + private final AtomicReference lastIndicatorValue = new AtomicReference<>(); + private final long startTimeMillis; + + private final Map lastEntityIdsUpdateCount = new ConcurrentHashMap<>(); + + TableUpdater(Table table, Object initialIndicatorValue) { + if (dataSource == null) { + logger.error("It should be called static Inmemo#setDataSource()" + + " before any instance of TableUpdater."); + throw new InmemoException("It should be called static Inmemo#setDataSource()" + + " before any instance of TableUpdater."); + } + + this.table = table; + this.lastIndicatorValue.set(initialIndicatorValue); + + DataSource clazzDataSource = dataSourceByClazzName.get(table.getClazz().getName()); + jacuzzi = Jacuzzi.getJacuzzi(clazzDataSource == null ? dataSource : clazzDataSource); + + typeOracle = TypeOracle.getTypeOracle(table.getClazz()); + + threadName = "InmemoUpdater#" + table.getClazz(); + thread = new Thread(new TableUpdaterRunnable(this), threadName); + thread.setDaemon(true); + tableUpdaterThreads.add(thread); + + logger.info("Started Inmemo table updater thread '" + threadName + "'."); + startTimeMillis = System.currentTimeMillis(); + + //noinspection ThisEscapedInObjectConstruction + instances.add(this); + } + + @SuppressWarnings("UnusedDeclaration") + static void setDataSource(DataSource dataSource) { + TableUpdater.dataSource = dataSource; + } + + @SuppressWarnings("UnusedDeclaration") + static void stop() { + synchronized (instances) { + for (TableUpdater instance : instances) { + instance.running = false; + } + } + } + + void start() { + running = true; + thread.start(); + } + + private int getMaxUpdateSameIndicatorTimes() { + if (table.isPreloaded()) { + return MAX_UPDATE_SAME_INDICATOR_TIMES; + } else { + return 1; + } + } + + void insertOrUpdateById(Long id) { + if (id == null) { + return; + } + + RowRoll rows = jacuzzi.findRowRoll("SELECT * FROM " + + typeOracle.getTableName() + + " WHERE " + + typeOracle.getIdColumn() + + " = " + + id); + + if (rows == null || rows.isEmpty()) { + return; + } + + if (rows.size() == 1) { + Row row = rows.getRow(0); + T entity = typeOracle.convertFromRow(row); + + table.insertOrUpdate(entity, row); + table.insertOrUpdate(row); + } else { + throw new InmemoException("Expected at most one item of " + + table.getClazz() + " with id = " + id + '.'); + } + } + + List findAndUpdateByEmergencyQueryFields(Object[] fields) { + validateFieldsArray(fields); + + String[] fieldNames = new String[fields.length / 2]; + Object[] fieldValues = new Object[fields.length / 2]; + + for (int index = 0; index < fields.length; index += 2) { + fieldNames[index / 2] = (String) fields[index]; + fieldValues[index / 2] = fields[index + 1]; + } + + String formattedFields = typeOracle.getQueryFindSql(fieldNames); + + RowRoll rows = jacuzzi.findRowRoll("SELECT * FROM " + + typeOracle.getTableName() + + " WHERE " + + formattedFields + + " ORDER BY " + + typeOracle.getIdColumn(), fieldValues); + + if (rows == null || rows.isEmpty()) { + return Collections.emptyList(); + } + + logger.warn("Emergency case: found " + + rows.size() + + " items of class " + + table.getClazz().getName() + + " [fields=" + + formattedFields + + "]."); + + List result = new ArrayList<>(rows.size()); + for (int i = 0; i < rows.size(); i++) { + Row row = rows.getRow(i); + + T entity = typeOracle.convertFromRow(row); + logger.warn("Emergency found: " + + table.getClazz().getName() + + " id=" + + entity.getId() + + " [fields=" + + formattedFields + + "]."); + + result.add(entity); + + table.insertOrUpdate(entity, row); + table.insertOrUpdate(row); + } + + return result; + } + + private static void validateFieldsArray(Object[] fields) { + if (fields.length % 2 != 0) { + throw new IllegalArgumentException("EmergencyQueryFields array should have" + + " even length. Found: " + + Arrays.toString(fields) + '.'); + } + + for (int index = 0; index < fields.length; index += 2) { + Object field = fields[index]; + if (!(field instanceof String) || ((String) field).isEmpty()) { + throw new IllegalArgumentException("EmergencyQueryFields array must contain" + + " non-empty strings on even positions. Found: " + + Arrays.toString(fields) + '.'); + } + } + } + + void update() { + if (running) { + internalUpdate(); + } + } + + private void randomizedUpdate() { + List updatedIds = internalUpdate(); + sleepBetweenRescans(updatedIds.size()); + } + + private List internalUpdate() { + updateLock.lock(); + + try { + long startTimeMillis = System.currentTimeMillis(); + Object prevLastIndicatorValue = lastIndicatorValue.get(); + RowRoll rows = getRecentlyChangedRows(prevLastIndicatorValue); + + long afterGetRecentlyChangedRowsMillis = System.currentTimeMillis(); + long getRecentlyChangedMillis = afterGetRecentlyChangedRowsMillis - startTimeMillis; + + if (rows.size() >= 100 + || getRecentlyChangedMillis >= TimeUnit.SECONDS.toMillis(1)) { + logger.error("Table '" + + table.getClazz().getSimpleName() + + "': getRecentlyChangedRows returns " + + rows.size() + + " rows [prevLastIndicatorValue=" + + prevLastIndicatorValue + + ", lastIndicatorValue=" + + lastIndicatorValue.get() + + ", thread=" + + threadName + + ", time=" + getRecentlyChangedMillis + " ms]."); + } + + boolean hasInsertOrUpdateByRow = table.hasInsertOrUpdateByRow(); + List updatedIds = new ArrayList<>(); + + int idColumn = getIdColumn(rows); + int indicatorFieldColumn = rows.getColumn(table.getIndicatorField()); + + for (int i = 0; i < rows.size(); i++) { + long id = (long) rows.getValue(i, idColumn); + + if (Objects.equals(rows.getValue(i, indicatorFieldColumn), prevLastIndicatorValue) + && lastEntityIdsUpdateCount.containsKey(id) + && lastEntityIdsUpdateCount.get(id) >= getMaxUpdateSameIndicatorTimes()) { + continue; + } + + // Insert or update entity. + Row row = rows.getRow(i); + T entity = typeOracle.convertFromRow(row); + table.insertOrUpdate(entity, row); + + updatedIds.add(id); + + // Insert or update row. + if (hasInsertOrUpdateByRow) { + table.insertOrUpdate(row); + } + + if (i > 0 && i % 100000 == 0) { + logger.warn("Inserted " + + i + + " rows in a batch [table=" + + table.getClazz().getSimpleName() + + "]."); + } + + if (table.hasSize()) { + int tableSize = table.size(); + if (tableSize > 0 && tableSize % 100000 == 0) { + logger.warn("Table " + + table.getClazz().getSimpleName() + + " contains now " + + tableSize + + " rows."); + } + } + + lastIndicatorValue.set(row.get(table.getIndicatorField())); + } + + if (updatedIds.size() >= 10) { + logger.info("Thread '" + + threadName + + "' has found " + + rows.size() + + "(" + + updatedIds.size() + + ") rows to update in " + + getRecentlyChangedMillis + + " ms [prevLastIndicatorValue=" + + prevLastIndicatorValue + + ", lastIndicatorValue=" + + lastIndicatorValue.get() + + "]."); + + if (updatedIds.size() <= 100) { + StringBuilder ids = new StringBuilder(); + for (long id : updatedIds) { + if (ids.length() > 0) { + ids.append(','); + } + ids.append(id); + } + logger.info("Updated entries have id=" + ids + '.'); + } + + logger.info("Thread '" + + threadName + + "' has updated " + + updatedIds.size() + + " items in " + + (System.currentTimeMillis() - startTimeMillis) + + " ms [prevLastIndicatorValue=" + + prevLastIndicatorValue + + ", lastIndicatorValue=" + + lastIndicatorValue.get() + + "]."); + } + + if (updatedIds.isEmpty() && !table.isPreloaded()) { + if (table.hasSize()) { + logger.info("Inmemo ready to dump journal of table " + ReflectionUtil.getTableClassName(table.getClazz()) + + " [items=" + table.size() + "]."); + } + long totalTimeMillis = System.currentTimeMillis() - this.startTimeMillis; + try { + table.writeJournal(); + } catch (IOException e) { + logger.error("Inmemo failed to dump journal of table " + ReflectionUtil.getTableClassName(table.getClazz()) + + " [items=" + table.size() + "] in " + totalTimeMillis + " ms.", e); + } + if (table.hasSize()) { + logger.log(totalTimeMillis < TimeUnit.SECONDS.toMillis(1) ? Level.INFO : Level.WARN, "Inmemo preloaded " + ReflectionUtil.getTableClassName(table.getClazz()) + + " [items=" + table.size() + "] in " + totalTimeMillis + " ms."); + } else { + logger.log(totalTimeMillis < TimeUnit.SECONDS.toMillis(1) ? Level.INFO : Level.WARN, "Inmemo preloaded " + ReflectionUtil.getTableClassName(table.getClazz()) + + " in " + totalTimeMillis + " ms."); + } + table.logBucketStats(); + table.setPreloaded(true); + } + + Object newLastIndicatorValue = lastIndicatorValue.get(); + if (!Objects.equals(prevLastIndicatorValue, newLastIndicatorValue)) { + lastEntityIdsUpdateCount.clear(); + } + List trulyUpdatedIds = new ArrayList<>(updatedIds.size()); + + for (int i = 0; i < rows.size(); i++) { + if (Objects.equals(rows.getValue(i, indicatorFieldColumn), newLastIndicatorValue)) { + long id = (long) rows.getValue(i, idColumn); + Integer updateCount = lastEntityIdsUpdateCount.get(id); + if (updateCount == null) { + trulyUpdatedIds.add(id); + updateCount = 1; + lastEntityIdsUpdateCount.put(id, updateCount); + } else if (updateCount < getMaxUpdateSameIndicatorTimes()) { + updateCount++; + lastEntityIdsUpdateCount.put(id, updateCount); + } + } + } + + return trulyUpdatedIds; + } finally { + updateLock.unlock(); + } + } + + private static int getIdColumn(RowRoll rowRoll) { + int column = rowRoll.getColumn("id"); + if (column == -1) { + column = rowRoll.getColumn("ID"); + } + return column; + } + + private void sleepBetweenRescans(int updatedCount) { + long rescanTimeMillis = getRescanTimeMillis(); + + if (updatedCount == 0) { + sleep((4 * rescanTimeMillis / 5) + + ThreadLocalRandom.current().nextInt((int) (rescanTimeMillis / 5))); + } else if (updatedCount * 2 > MAX_ROWS_IN_SINGLE_SQL_STATEMENT) { + logger.info("Thread '" + + threadName + + "' will not sleep because it updated near maximum row count."); + } else { + sleep(ThreadLocalRandom.current().nextInt((int) (rescanTimeMillis / 5))); + } + } + + private long getRescanTimeMillis() { + return RESCAN_TIME_MILLIS; + } + + private void sleep(long timeMillis) { + try { + Thread.sleep(timeMillis); + } catch (InterruptedException e) { + logger.error("Thread '" + threadName + "' has been stopped because of InterruptedException.", e); + running = false; + } + } + + private RowRoll getRecentlyChangedRows(Object indicatorLastValue) { + long startTimeMillis = System.currentTimeMillis(); + RowRoll rows = null; + + if (lastIndicatorValue.get() == null && table.isUseJournal()) { + RowRoll journalRows = table.readJournal(); + if (journalRows != null && !journalRows.isEmpty()) { + rows = journalRows; + } + } + + if (rows != null) { + logger.info("getRecentlyChangedRows loads data of using the journal in " + + (System.currentTimeMillis() - startTimeMillis) + + " ms [table=" + table.getClazz().getSimpleName() + "]."); + return rows; + } + + String forceIndexClause = table.getDatabaseIndex() == null ? "" : ("FORCE INDEX (" + table.getDatabaseIndex() + ')'); + + if (indicatorLastValue == null) { + rows = jacuzzi.findRowRoll("SELECT * FROM " + + typeOracle.getTableName() + + ' ' + + forceIndexClause + + " ORDER BY " + + table.getIndicatorField() + + ", " + + typeOracle.getIdColumn() + + " LIMIT " + + MAX_ROWS_IN_SINGLE_SQL_STATEMENT); + } else { + rows = jacuzzi.findRowRoll("SELECT * FROM " + + typeOracle.getTableName() + + ' ' + + forceIndexClause + + " WHERE " + + table.getIndicatorField() + + " >= ? ORDER BY " + + table.getIndicatorField() + + ", " + + typeOracle.getIdColumn() + + " LIMIT " + + MAX_ROWS_IN_SINGLE_SQL_STATEMENT, + indicatorLastValue + ); + } + + long queryTimeMillis = System.currentTimeMillis() - startTimeMillis; + if (queryTimeMillis * 10 > getRescanTimeMillis()) { + logger.warn("Rescanning query for entity `" + + table.getClazz().getName() + + "` took too long time " + + queryTimeMillis + + " ms."); + } + return rows; + } + + @SuppressWarnings("WeakerAccess") + public static void setSpecificDataSource(Class clazz, DataSource dataSource) { + String clazzName = clazz.getName(); + logger.info("Setting specific data source for [clazz=" + clazzName + "]."); + dataSourceByClazzName.put(clazzName, dataSource); + } + + private static final class TableUpdaterRunnable implements Runnable { + private final TableUpdater tableUpdater; + + private TableUpdaterRunnable(TableUpdater tableUpdater) { + this.tableUpdater = tableUpdater; + } + + @Override + public void run() { + tableUpdaterThreadCount.incrementAndGet(); + while (tableUpdater.running) { + try { + tableUpdater.randomizedUpdate(); + } catch (Exception e) { + logger.error("Unexpected " + + e.getClass().getName() + + " exception in TableUpdaterRunnable of " + + tableUpdater.threadName + + ": " + e, e); + } + } + + tableUpdaterThreadCount.decrementAndGet(); + + logger.warn("Inmemo update thread for " + + tableUpdater.table.getClazz().getName() + + " finished."); + } + } + + private static final class TableUpdaterThreadsPrinterRunnable implements Runnable { + @SuppressWarnings("BusyWait") + @Override + public void run() { + while (!Thread.currentThread().isInterrupted()) { + try { + Thread.sleep(TimeUnit.SECONDS.toMillis(300)); + } catch (InterruptedException e) { + logger.warn("TableUpdaterThreadsPrinterRunnable stopped because of" + + " InterruptedException."); + break; + } + + if (tableUpdaterThreadCount.get() == 0) { + logger.warn("TableUpdaterThreadsPrinterRunnable stopped because of" + + " `tableUpdaterThreadCount.get() == 0`."); + break; + } + + synchronized (tableUpdaterThreads) { + logger.info("tableUpdaterThreads.size()=" + tableUpdaterThreads.size() + "."); + for (Thread tableUpdaterRunnable : tableUpdaterThreads) { + printStackTrace(tableUpdaterRunnable); + } + } + } + } + + private void printStackTrace(Thread thread) { + StringBuilder result = new StringBuilder(); + StackTraceElement[] elements = thread.getStackTrace(); + for (StackTraceElement element : elements) { + result.append(thread.getName()).append(": ").append(element.toString()).append('\n'); + } + result.append("\n\n"); + logger.info(thread.getName() + " stack trace: " + result); + } + } + + static { + Thread tableUpdaterThreadsPrinterThread = new Thread(new TableUpdaterThreadsPrinterRunnable()); + tableUpdaterThreadsPrinterThread.setDaemon(true); + tableUpdaterThreadsPrinterThread.setUncaughtExceptionHandler((t, e) + -> logger.error("Uncaught exception [thread=" + t + ", exception=" + e + "].")); + tableUpdaterThreadsPrinterThread.start(); + } +} diff --git a/src/test/java/com/codeforces/inmemo/InmemoTest.java b/src/test/java/com/codeforces/inmemo/InmemoTest.java index fa474d4..5453f1a 100644 --- a/src/test/java/com/codeforces/inmemo/InmemoTest.java +++ b/src/test/java/com/codeforces/inmemo/InmemoTest.java @@ -1,724 +1,724 @@ -package com.codeforces.inmemo; - -import com.codeforces.inmemo.dao.UserDao; -import com.codeforces.inmemo.dao.impl.UserDaoImpl; -import com.codeforces.inmemo.model.User; -import com.codeforces.inmemo.model.Wrapper; -import com.codeforces.inmemo.model.Wrapper.a; -import com.mchange.v2.c3p0.ComboPooledDataSource; -import org.apache.commons.lang3.RandomUtils; -import org.jacuzzi.core.Row; -import org.junit.Assert; -import org.junit.Before; -import org.junit.Test; - -import javax.sql.DataSource; -import java.io.File; -import java.io.IOException; -import java.sql.Connection; -import java.sql.SQLException; -import java.util.*; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import java.util.concurrent.TimeUnit; - -import static org.hamcrest.core.Is.is; -import static org.hamcrest.core.IsNot.not; - -/** - * @author Mike Mirzayanov (mirzayanovmr@gmail.com) - */ -public class InmemoTest { - private static final long USER_COUNT = 10000; - private static final long BASE_SLEEP_MS = 1500; - - private UserDao userDao; - - private static DataSource newDataSource() { - ComboPooledDataSource comboPooledDataSource = new ComboPooledDataSource(); - comboPooledDataSource.setJdbcUrl("jdbc:hsqldb:mem:inmemo"); - comboPooledDataSource.setUser("sa"); - comboPooledDataSource.setPassword(""); - return comboPooledDataSource; - } - - @Before - public void setup() throws SQLException { - DataSource dataSource = newDataSource(); - - userDao = new UserDaoImpl(dataSource); - - Connection connection = dataSource.getConnection(); - connection.createStatement().execute("DROP TABLE User IF EXISTS"); - connection.createStatement().execute("CREATE TABLE User (" + - "ID BIGINT GENERATED BY DEFAULT AS IDENTITY (START WITH 1), " + - "HANDLE VARCHAR(255), " + - "OPENID VARCHAR(255), " + - "EMAIL VARCHAR(255), " + - "PASSWORD VARCHAR(255), " + - "ADMIN BOOLEAN, " + - "CREATIONTIME TIMESTAMP, " + - "LASTONLINETIME TIMESTAMP, " + - "DISABLED BOOLEAN," + - "TSHIRTSIZE VARCHAR(255) " + - ')' - ); - - for (int i = 0; i < USER_COUNT; i++) { - userDao.insertRandom(); - } - - Inmemo.setDataSource(dataSource); - } - - @Test - public void testEnumField() throws InterruptedException { - Inmemo.dropTableIfExists(User.class); - - // Create table. - { - Inmemo.createTable(User.class, "ID", null, new Indices.Builder() {{ - add(Index.create("ID", Long.class, User::getId)); - }}.build(), true); - } - - Map users = new HashMap<>(); - for (User.TShirtSize tShirtSize : User.TShirtSize.values()) { - User user = userDao.newRandomUser(); - user.setTShirtSize(tShirtSize); - userDao.insert(user); - users.put(user.getId(), user); - } - - Thread.sleep(BASE_SLEEP_MS); - - for (Map.Entry entry : users.entrySet()) { - User inmemoUser = Inmemo.findOnly(true, User.class, new IndexConstraint<>("ID", entry.getKey())); - org.junit.Assert.assertEquals(entry.getValue().getTShirtSize(), inmemoUser.getTShirtSize()); - } - } - - @Test - public void testRowFilter() throws InterruptedException { - Inmemo.dropTableIfExists(User.class); - - Inmemo.Filter adminFilter = new Inmemo.Filter() { - @Override - public boolean testRow(Row row) { - return Boolean.TRUE.equals(row.get("ADMIN")); - } - - @Override - public boolean testItem(User user) { - return user.isAdmin(); - } - }; - Inmemo.createTable(User.class, "ID", null, new Indices.Builder() {{ - add(Index.create("ID", Long.class, User::getId)); - }}.build(), adminFilter, true); - int size = Inmemo.size(User.class); - org.junit.Assert.assertTrue(size >= USER_COUNT * 0.45 && size <= USER_COUNT * 0.55); - - for (User user : userDao.findAll()) { - Inmemo.insertOrUpdate(user); - } - org.junit.Assert.assertEquals(size, Inmemo.size(User.class)); - - List admins = new ArrayList<>(); - List nonAdmins = new ArrayList<>(); - for (User user : userDao.findAll()) { - if (user.isAdmin()) { - admins.add(user); - } else { - nonAdmins.add(user); - } - } - - User randomAdmin = admins.get(RandomUtils.nextInt(0, admins.size())); - randomAdmin.setHandle("???"); - userDao.update(randomAdmin); - Inmemo.insertOrUpdateByIds(User.class, randomAdmin.getId()); - User inmemoRandomAdmin = Inmemo.findOnly(true, User.class, new IndexConstraint<>("ID", randomAdmin.getId())); - org.junit.Assert.assertEquals(randomAdmin.getHandle(), inmemoRandomAdmin.getHandle()); - - User randomNonAdmin = nonAdmins.get(RandomUtils.nextInt(0, nonAdmins.size())); - randomNonAdmin.setHandle("???"); - userDao.update(randomNonAdmin); - Inmemo.insertOrUpdateByIds(User.class, randomNonAdmin.getId()); - User inmemoRandomNonAdmin = Inmemo.findOnly(true, User.class, new IndexConstraint<>("ID", randomNonAdmin.getId())); - org.junit.Assert.assertNull(inmemoRandomNonAdmin); - } - - @Test - public void testCommon() throws InterruptedException { - Inmemo.dropTableIfExists(User.class); - - // Test user count. - { - Assert.assertEquals(USER_COUNT, userDao.findAll().size()); - } - System.out.println(1); - - // Create table. - { - Inmemo.createTable(User.class, "ID", null, new Indices.Builder() {{ - add(Index.create("ID", Long.class, User::getId)); - - add(Index.create("handle", String.class, User::getHandle)); - - add(Index.create("FIRST_HANDLE_LETTER", String.class, user -> user.getHandle().substring(0, 1))); - }}.build(), true); - } - System.out.println(2); - - // Assert size. - { - Assert.assertEquals(USER_COUNT, Inmemo.size(User.class)); - } - System.out.println(3); - - // Exactly one user with id=123. - { - List users = Inmemo.find(User.class, new IndexConstraint<>("ID", 123L), user -> true); - Assert.assertEquals(1, users.size()); - Assert.assertEquals(123L, users.get(0).getId()); - } - System.out.println(4); - - // Tests that there is no user[id=USER_COUNT + 1], inserts it, waits, tests that the table contains it. - { - Assert.assertEquals(0, Inmemo.find(User.class, new IndexConstraint<>("ID", USER_COUNT + 1), user -> true).size()); - - userDao.insertRandom(); - Thread.sleep(BASE_SLEEP_MS); - - List users = Inmemo.find(User.class, new IndexConstraint<>("ID", USER_COUNT + 1), user -> true); - Assert.assertEquals(1, users.size()); - Assert.assertEquals(USER_COUNT + 1, users.get(0).getId()); - } - System.out.println(5); - - // Tests that there is no user[id=USER_COUNT + 2], inserts it, enforces update, tests that the table contains it. - { - Assert.assertEquals(0, Inmemo.find(User.class, new IndexConstraint<>("ID", USER_COUNT + 2), user -> true).size()); - - userDao.insertRandom(); - Inmemo.update(User.class); - - List users = Inmemo.find(User.class, new IndexConstraint<>("ID", USER_COUNT + 2), user -> true); - Assert.assertEquals(1, users.size()); - Assert.assertEquals(USER_COUNT + 2, users.get(0).getId()); - } - System.out.println(6); - - // Tests using second index that the number of users with first letter 'e' in handle is expected. - { - long eUsers1 = Inmemo.findCount(User.class, new IndexConstraint<>("FIRST_HANDLE_LETTER", "e"), user -> true); - - long eUsers2 = Inmemo.find(User.class, new IndexConstraint<>("FIRST_HANDLE_LETTER", "e"), user -> true).size(); - - int expectedCount = userDao.findCountByHandlePrefix("e"); - Assert.assertEquals(expectedCount, eUsers1); - Assert.assertEquals(expectedCount, eUsers2); - } - System.out.println(7); - - // Tests matcher. - { - long xyUsers = Inmemo.findCount(User.class, new IndexConstraint<>("FIRST_HANDLE_LETTER", "x"), user -> user.getHandle().charAt(1) == 'y'); - - Assert.assertEquals(userDao.findCountByHandlePrefix("xy"), xyUsers); - } - System.out.println(8); - - // Tests that if we change user state in memory, it will no affect table unless we use insertOrUpdate. - { - User user13 = Inmemo.find(User.class, new IndexConstraint<>("ID", 13L), user -> true).get(0); - - user13.setHandle("handle13"); - - org.junit.Assert.assertThat(user13.getHandle(), not(Inmemo.find(User.class, new IndexConstraint<>("ID", 13L), user -> true).get(0).getHandle())); - - Inmemo.insertOrUpdate(user13); - - org.junit.Assert.assertThat(user13.getHandle(), is(Inmemo.find(User.class, new IndexConstraint<>("ID", 13L), user -> true).get(0).getHandle())); - } - System.out.println(9); - - // Tests that findOnly throws exception if non-unique and throwOnNotUnique parameter is true; - { - boolean hasException = false; - User existingUser = null; - try { - existingUser = Inmemo.findOnly(true, User.class, new IndexConstraint<>("ID", USER_COUNT / 5)); - } catch (InmemoException e) { - hasException = true; - } - Assert.assertFalse(hasException); - - User user = userDao.newRandomUser(); - user.setHandle(existingUser.getHandle()); - Inmemo.insertOrUpdate(user); - - List possibleUsers = Arrays.asList(existingUser, user); - - hasException = false; - try { - User foundUser = Inmemo.findOnly(false, User.class, new IndexConstraint<>("handle", user.getHandle())); - Assert.assertNotNull(foundUser); - Assert.assertTrue(possibleUsers.contains(foundUser)); - } catch (InmemoException e) { - hasException = e.getMessage().toLowerCase().contains("unique"); - } - - Assert.assertFalse(hasException); - - hasException = false; - try { - User foundUser = Inmemo.findOnly(true, User.class, new IndexConstraint<>("handle", user.getHandle())); - Assert.assertNotNull(foundUser); - Assert.assertTrue(possibleUsers.contains(foundUser)); - } catch (InmemoException e) { - hasException = true; - } - - Assert.assertTrue(hasException); - } - System.out.println(10); - - // Tests insertOrUpdateByIds method - for (long i = 1; i <= USER_COUNT; i++) { - User user = Inmemo.findOnly(true, User.class, new IndexConstraint<>("ID", i)); - String newHandle = userDao.getRandomString(); - user.setHandle(newHandle); - - userDao.update(user); - Inmemo.insertOrUpdateByIds(User.class, user.getId()); - - User newUser = Inmemo.findOnly(true, User.class, new IndexConstraint<>("ID", user.getId())); - Assert.assertEquals(newHandle, newUser.getHandle()); - } - System.out.println(11); - } - - @Test - public void testCompatibleClasses() throws InterruptedException { - Inmemo.dropTableIfExists(Wrapper.a.class); - - // Test user count. - { - Assert.assertEquals(USER_COUNT, userDao.findAll().size()); - } - - // Create table. - { - Inmemo.createTable(User.class, "ID", null, new Indices.Builder() {{ - add(Index.create("ID", Long.class, User::getId)); - add(Index.create("FIRST_HANDLE_LETTER", String.class, user -> user.getHandle().substring(0, 1))); - }}.build(), true); - } - -// Assert.assertEquals(1, Inmemo.findCount(User2.class, new IndexConstraint("ID", 11), new Matcher() { -// @Override -// public boolean match(User2 tableItem) { -// return true; -// } -// })); - - Assert.assertEquals(1, Inmemo.findCount(Wrapper.a.class, new IndexConstraint("ID", 11L), user -> true)); - - // Tests that there is no user[id=USER_COUNT + 1], inserts it, waits, tests that the table contains it. - { - List users = Inmemo.find(a.class, - new IndexConstraint<>("ID", USER_COUNT + 1), user -> true); - if (users.size() != 0) { - System.out.println(users); - } - - Assert.assertEquals(0, Inmemo.find(Wrapper.a.class, - new IndexConstraint<>("ID", USER_COUNT + 1), user -> true).size()); - - userDao.insertRandom(); - Thread.sleep(BASE_SLEEP_MS); - Assert.assertEquals(1, Inmemo.find(Wrapper.a.class, - new IndexConstraint<>("ID", USER_COUNT + 1), user -> true).size()); - } - - // Tests that there is no user[id=USER_COUNT + 2], inserts it, waits, tests that the table contains it. - { - Assert.assertEquals(0, Inmemo.find(Wrapper.a.class, - new IndexConstraint<>("ID", USER_COUNT + 2), user -> true).size()); - - a x = new a(); - x.setId(USER_COUNT + 2); - x.setHandle("aa"); - Inmemo.insertOrUpdate(x); - - Assert.assertEquals(1, Inmemo.find(Wrapper.a.class, - new IndexConstraint<>("ID", USER_COUNT + 2), user -> true).size()); - - Assert.assertEquals(1, - Inmemo.find(User.class, new IndexConstraint<>("ID", USER_COUNT + 2), user -> user.getPassword() == null).size()); - Assert.assertEquals(0, - Inmemo.find(User.class, new IndexConstraint<>("ID", USER_COUNT + 2), user -> user.getPassword() != null).size()); - } - } - - @Test - public void testUnique() throws InterruptedException { - Inmemo.dropTableIfExists(User.class); - - // Test user count. - { - Assert.assertEquals(USER_COUNT, userDao.findAll().size()); - } - - // Create table. - { - Inmemo.createTable(User.class, "ID", null, new Indices.Builder() {{ - add(Index.createUnique("ID", Long.class, User::getId)); - add(Index.createUnique("handle", String.class, User::getHandle)); - add(Index.create("FIRST_HANDLE_LETTER", String.class, tableItem -> tableItem.getHandle().substring(0, 1))); - }}.build(), true); - } - - // Assert size. - { - Assert.assertEquals(USER_COUNT, Inmemo.size(User.class)); - } - - // Exactly one user with id=123. - { - Assert.assertEquals(1, Inmemo.find(User.class, new IndexConstraint<>("ID", 123L), user -> true).size()); - } - - // No one user with id=123 and false matcher. - { - Assert.assertEquals(0, Inmemo.find(User.class, new IndexConstraint<>("ID", 123L), user -> false).size()); - } - - // Exactly one user with id=123. - { - Assert.assertEquals(1, Inmemo.findCount(User.class, new IndexConstraint<>("ID", 123L))); - } - - // Exactly one user with id=123. - { - Assert.assertEquals(123L, Inmemo.findOnly(true, User.class, new IndexConstraint<>("ID", 123L)).getId()); - } - - // Tests that there is no user[id=USER_COUNT + 1], inserts it, waits, tests that the table contains it. - { - Assert.assertNull(Inmemo.findOnly(false, User.class, new IndexConstraint<>("ID", USER_COUNT + 1))); - - userDao.insertRandom(); - Thread.sleep(BASE_SLEEP_MS); - - Assert.assertEquals(USER_COUNT + 1, Inmemo.findOnly(false, User.class, new IndexConstraint<>("ID", USER_COUNT + 1)).getId()); - } - - // Tests using second index that the number of users with first letter 'e' in handle is expected. - { - long id = USER_COUNT / 2; - - User userById = Inmemo.findOnly(true, User.class, new IndexConstraint<>("ID", id)); - Assert.assertEquals(id, userById.getId()); - - User userByHandle = Inmemo.findOnly(true, User.class, new IndexConstraint<>("handle", userById.getHandle())); - - Assert.assertNotNull(userByHandle); - Assert.assertNotSame(userById, userByHandle); - Assert.assertEquals(userById, userByHandle); - - List usersByFirstHandleLetter = Inmemo.find(User.class, new IndexConstraint<>("FIRST_HANDLE_LETTER", userById.getHandle().substring(0, 1))); - Assert.assertTrue(usersByFirstHandleLetter.contains(userById)); - } - - // Tests that if non-unique then throw exception. - { - User existingUser = Inmemo.findOnly(true, User.class, new IndexConstraint<>("ID", USER_COUNT / 5)); - User user = userDao.newRandomUser(); - user.setHandle(existingUser.getHandle()); - - boolean hasException = false; - try { - Inmemo.insertOrUpdate(user); - } catch (InmemoException e) { - hasException = e.getMessage().toLowerCase().contains("unique"); - } - - Assert.assertTrue(hasException); - } - - // Tests matcher for unique indices - { - long id = USER_COUNT / 5; - User existingUser = Inmemo.findOnly(true, User.class, new IndexConstraint<>("ID", id)); - final String handle = existingUser.getHandle(); - User notFoundUser = Inmemo.findOnly(true, User.class, new IndexConstraint<>("ID", id), - user -> !user.getHandle().equals(handle) - ); - - Assert.assertNull(notFoundUser); - - List emptyUserList = Inmemo.find(User.class, new IndexConstraint<>("ID", id), - user -> !user.getHandle().equals(handle) - ); - - Assert.assertTrue(emptyUserList.isEmpty()); - } - } - - @Test - public void testEmergency() throws InterruptedException { - Inmemo.dropTableIfExists(User.class); - - // Test user count. - { - Assert.assertEquals(USER_COUNT, userDao.findAll().size()); - } - - // Create table. - { - Inmemo.createTable(User.class, "ID", null, new Indices.Builder() {{ - add(Index.createUnique("ID", Long.class, User::getId, indexValue -> new Object[]{"ID", indexValue} - )); - add(Index.createUnique("handle", String.class, User::getHandle)); - add(Index.createUnique("idAndHandle", String.class, user -> user.getId() + "," + user.getHandle(), indexValue -> { - String[] tokens = indexValue.split(","); - return new Object[]{"ID", Long.valueOf(tokens[0]), "HANDLE", tokens[1]}; - } - )); - add(Index.create("FIRST_HANDLE_LETTER", String.class, tableItem -> tableItem.getHandle().substring(0, 1))); - }}.build(), true); - } - - // Assert size. - { - Assert.assertEquals(USER_COUNT, Inmemo.size(User.class)); - } - - Map traces = Thread.getAllStackTraces(); - for (Thread thread : traces.keySet()) { - if ("InmemoUpdater#class com.codeforces.inmemo.model.User".equals(thread.getName())) { - thread.interrupt(); - try { - Thread.sleep(BASE_SLEEP_MS * 2); - } catch (InterruptedException e) { - // No operations. - } - } - } - - userDao.insertRandom(); - Assert.assertEquals(USER_COUNT + 1, userDao.findAll().size()); - Assert.assertEquals(USER_COUNT + 1, Inmemo.findOnly(true, User.class, new IndexConstraint<>("ID", USER_COUNT + 1)).getId()); - - userDao.insertRandom(); - Assert.assertEquals(1, Inmemo.findCount(User.class, new IndexConstraint<>("ID", USER_COUNT + 1))); - Assert.assertEquals(1, Inmemo.findCount(User.class, new IndexConstraint<>("ID", USER_COUNT + 2))); - Assert.assertEquals(0, Inmemo.findCount(User.class, new IndexConstraint<>("ID", USER_COUNT + 3))); - - userDao.insertRandom(); - Assert.assertEquals(1, Inmemo.findCount(User.class, new IndexConstraint<>("ID", USER_COUNT + 3))); - - Assert.assertEquals(0, Inmemo.find(User.class, new IndexConstraint<>("ID", USER_COUNT + 4)).size()); - userDao.insertRandom(); - Assert.assertEquals(1, Inmemo.find(User.class, new IndexConstraint<>("ID", USER_COUNT + 4)).size()); - - User newUser = userDao.newRandomUser(); - userDao.insert(newUser); - User foundUser = Inmemo.findOnly(true, User.class, new IndexConstraint<>("idAndHandle", newUser.getId() + "," + newUser.getHandle())); - newUser = userDao.find(newUser.getId()); - Assert.assertEquals(newUser, foundUser); - } - - @Test - public void testEmergencyDatabaseQuery() throws InterruptedException { - Inmemo.dropTableIfExists(User.class); - - // Test user count. - { - Assert.assertEquals(USER_COUNT, userDao.findAll().size()); - } - - // Create table. - { - Inmemo.createTable(User.class, "ID", null, new Indices.Builder() {{ - add(Index.createUnique("ID", Long.class, User::getId, id -> new Object[]{"id", id} - ) - ); - - add(Index.create("handle", String.class, User::getHandle, handle -> new Object[]{"handle", handle} - )); - - add(Index.create("handle2", String.class, User::getHandle)); - - add(Index.create("FIRST_HANDLE_LETTER", String.class, tableItem -> tableItem.getHandle().substring(0, 1))); - }}.build(), true); - } - - // Assert size. - { - Assert.assertEquals(USER_COUNT, Inmemo.size(User.class)); - } - - { - User newUser = userDao.newRandomUser(); - userDao.insert(newUser); - - List foundUsers = Inmemo.find(User.class, new IndexConstraint<>("ID", newUser.getId())); - newUser = userDao.find(newUser.getId()); - - Assert.assertTrue(foundUsers.contains(newUser)); - } - - { - User newUser = userDao.newRandomUser(); - userDao.insert(newUser); - - List foundUsers = Inmemo.find(User.class, new IndexConstraint<>("handle", newUser.getHandle())); - newUser = userDao.find(newUser.getId()); - - Assert.assertTrue(foundUsers.contains(newUser)); - } - - { - User newUser = userDao.newRandomUser(); - userDao.insert(newUser); - - List foundUsers = Inmemo.find(User.class, new IndexConstraint<>("handle2", newUser.getHandle())); - newUser = userDao.find(newUser.getId()); - - Assert.assertFalse(foundUsers.contains(newUser)); - } - - { - User newUser = userDao.newRandomUser(); - userDao.insert(newUser); - List foundUsers = Inmemo.find(User.class, new IndexConstraint<>("FIRST_HANDLE_LETTER", - newUser.getHandle().substring(0, 1))); - newUser = userDao.find(newUser.getId()); - - Assert.assertFalse(foundUsers.contains(newUser)); - - Thread.sleep(BASE_SLEEP_MS * 2); - - foundUsers = Inmemo.find(User.class, new IndexConstraint<>("FIRST_HANDLE_LETTER", - newUser.getHandle().substring(0, 1))); - Assert.assertTrue(foundUsers.contains(newUser)); - } - } - - @Test - public void testDeadlock() { - // Test user count. - { - Assert.assertEquals(USER_COUNT, userDao.findAll().size()); - } - - Random random = new Random(123L); - - final List userIds = new ArrayList<>(100); - for (int i = 0; i < 100; ++i) - userIds.add((long) random.nextInt((int) USER_COUNT)); - - // Create table. - Inmemo.createTable(User.class, "ID", null, new Indices.Builder() {{ - add(Index.createUnique("ID", Long.class, User::getId, id -> new Object[]{"id", id} - ) - ); - - add(Index.create("handle", String.class, User::getHandle, handle -> new Object[]{"handle", handle} - )); - - add(Index.create("handle2", String.class, User::getHandle)); - - add(Index.create("FIRST_HANDLE_LETTER", String.class, tableItem -> tableItem.getHandle().substring(0, 1))); - }}.build(), false); - - ExecutorService executorService = Executors.newFixedThreadPool(10); - - for (int i = 0; i < 10; ++i) { - executorService.submit(() -> { - for (Long userId : userIds) { - try { - User user = Inmemo.findOnly(true, User.class, new IndexConstraint<>("ID", userId)); - Assert.assertNotNull(user); - } catch (InmemoException e) { - Assert.assertTrue(e.getMessage().contains("Unable to find table for class name")); - } - } - }); - - executorService.submit(() -> { - for (Long userId : userIds) { - try { - Inmemo.insertOrUpdateByIds(User.class, userId); - } catch (InmemoException e) { - Assert.assertTrue(e.getMessage().contains("Unable to find table for class name")); - } - } - }); - } - - executorService.shutdown(); - - try { - Assert.assertTrue(executorService.awaitTermination(10, TimeUnit.SECONDS)); - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - } - } - - @Test - public void testJournal() throws IOException { - System.setProperty("Inmemo.UseJournal", "true"); - - File journalFile = new File("User.inmemo"); - journalFile.delete(); - - try { - Inmemo.dropTableIfExists(User.class); - Assert.assertFalse(journalFile.isFile()); - - // Create table. - { - Inmemo.createTable(User.class, "ID", null, new Indices.Builder() {{ - add(Index.create("ID", Long.class, User::getId)); - }}.build(), true); - } - - Map users = new HashMap<>(); - for (long id = 1; id <= USER_COUNT; id++) { - User inmemoUser = Inmemo.findOnly(true, User.class, new IndexConstraint<>("ID", id)); - User dbUser = userDao.find(id); - Assert.assertEquals(dbUser, inmemoUser); - users.put(id, inmemoUser); - } - - Inmemo.dropTableIfExists(User.class); - Assert.assertTrue(journalFile.isFile()); - { - Inmemo.createTable(User.class, "ID", null, new Indices.Builder() {{ - add(Index.create("ID", Long.class, User::getId)); - }}.build(), true); - } - - for (long id = 1; id <= USER_COUNT; id++) { - User inmemoUser = Inmemo.findOnly(true, User.class, new IndexConstraint<>("ID", id)); - User dbUser = userDao.find(id); - Assert.assertEquals(dbUser, inmemoUser); - Assert.assertEquals(users.get(id), inmemoUser); - } - } catch (Exception e) { - System.err.println("Unexpected exception: " + e.getMessage()); - throw new RuntimeException("Unexpected exception: " + e.getMessage(), e); - } finally { - Assert.assertTrue(journalFile.isFile()); - Inmemo.deleteJournal(User.class); - Assert.assertFalse(journalFile.isFile()); - System.setProperty("Inmemo.UseJournal", "false"); - } - } +package com.codeforces.inmemo; + +import com.codeforces.inmemo.dao.UserDao; +import com.codeforces.inmemo.dao.impl.UserDaoImpl; +import com.codeforces.inmemo.model.User; +import com.codeforces.inmemo.model.Wrapper; +import com.codeforces.inmemo.model.Wrapper.a; +import com.mchange.v2.c3p0.ComboPooledDataSource; +import org.apache.commons.lang3.RandomUtils; +import org.jacuzzi.core.Row; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +import javax.sql.DataSource; +import java.io.File; +import java.io.IOException; +import java.sql.Connection; +import java.sql.SQLException; +import java.util.*; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; + +import static org.hamcrest.core.Is.is; +import static org.hamcrest.core.IsNot.not; + +/** + * @author Mike Mirzayanov (mirzayanovmr@gmail.com) + */ +public class InmemoTest { + private static final long USER_COUNT = 10000; + private static final long BASE_SLEEP_MS = 1500; + + private UserDao userDao; + + private static DataSource newDataSource() { + ComboPooledDataSource comboPooledDataSource = new ComboPooledDataSource(); + comboPooledDataSource.setJdbcUrl("jdbc:hsqldb:mem:inmemo"); + comboPooledDataSource.setUser("sa"); + comboPooledDataSource.setPassword(""); + return comboPooledDataSource; + } + + @Before + public void setup() throws SQLException { + DataSource dataSource = newDataSource(); + + userDao = new UserDaoImpl(dataSource); + + Connection connection = dataSource.getConnection(); + connection.createStatement().execute("DROP TABLE User IF EXISTS"); + connection.createStatement().execute("CREATE TABLE User (" + + "ID BIGINT GENERATED BY DEFAULT AS IDENTITY (START WITH 1), " + + "HANDLE VARCHAR(255), " + + "OPENID VARCHAR(255), " + + "EMAIL VARCHAR(255), " + + "PASSWORD VARCHAR(255), " + + "ADMIN BOOLEAN, " + + "CREATIONTIME TIMESTAMP, " + + "LASTONLINETIME TIMESTAMP, " + + "DISABLED BOOLEAN," + + "TSHIRTSIZE VARCHAR(255) " + + ')' + ); + + for (int i = 0; i < USER_COUNT; i++) { + userDao.insertRandom(); + } + + Inmemo.setDataSource(dataSource); + } + + @Test + public void testEnumField() throws InterruptedException { + Inmemo.dropTableIfExists(User.class); + + // Create table. + { + Inmemo.createTable(User.class, "ID", null, new Indices.Builder() {{ + add(Index.create("ID", Long.class, User::getId)); + }}.build(), true); + } + + Map users = new HashMap<>(); + for (User.TShirtSize tShirtSize : User.TShirtSize.values()) { + User user = userDao.newRandomUser(); + user.setTShirtSize(tShirtSize); + userDao.insert(user); + users.put(user.getId(), user); + } + + Thread.sleep(BASE_SLEEP_MS); + + for (Map.Entry entry : users.entrySet()) { + User inmemoUser = Inmemo.findOnly(true, User.class, new IndexConstraint<>("ID", entry.getKey())); + org.junit.Assert.assertEquals(entry.getValue().getTShirtSize(), inmemoUser.getTShirtSize()); + } + } + + @Test + public void testRowFilter() throws InterruptedException { + Inmemo.dropTableIfExists(User.class); + + Inmemo.Filter adminFilter = new Inmemo.Filter() { + @Override + public boolean testRow(Row row) { + return Boolean.TRUE.equals(row.get("ADMIN")); + } + + @Override + public boolean testItem(User user) { + return user.isAdmin(); + } + }; + Inmemo.createTable(User.class, "ID", null, new Indices.Builder() {{ + add(Index.create("ID", Long.class, User::getId)); + }}.build(), adminFilter, true); + int size = Inmemo.size(User.class); + org.junit.Assert.assertTrue(size >= USER_COUNT * 0.45 && size <= USER_COUNT * 0.55); + + for (User user : userDao.findAll()) { + Inmemo.insertOrUpdate(user); + } + org.junit.Assert.assertEquals(size, Inmemo.size(User.class)); + + List admins = new ArrayList<>(); + List nonAdmins = new ArrayList<>(); + for (User user : userDao.findAll()) { + if (user.isAdmin()) { + admins.add(user); + } else { + nonAdmins.add(user); + } + } + + User randomAdmin = admins.get(RandomUtils.nextInt(0, admins.size())); + randomAdmin.setHandle("???"); + userDao.update(randomAdmin); + Inmemo.insertOrUpdateByIds(User.class, randomAdmin.getId()); + User inmemoRandomAdmin = Inmemo.findOnly(true, User.class, new IndexConstraint<>("ID", randomAdmin.getId())); + org.junit.Assert.assertEquals(randomAdmin.getHandle(), inmemoRandomAdmin.getHandle()); + + User randomNonAdmin = nonAdmins.get(RandomUtils.nextInt(0, nonAdmins.size())); + randomNonAdmin.setHandle("???"); + userDao.update(randomNonAdmin); + Inmemo.insertOrUpdateByIds(User.class, randomNonAdmin.getId()); + User inmemoRandomNonAdmin = Inmemo.findOnly(true, User.class, new IndexConstraint<>("ID", randomNonAdmin.getId())); + org.junit.Assert.assertNull(inmemoRandomNonAdmin); + } + + @Test + public void testCommon() throws InterruptedException { + Inmemo.dropTableIfExists(User.class); + + // Test user count. + { + Assert.assertEquals(USER_COUNT, userDao.findAll().size()); + } + System.out.println(1); + + // Create table. + { + Inmemo.createTable(User.class, "ID", null, new Indices.Builder() {{ + add(Index.create("ID", Long.class, User::getId)); + + add(Index.create("handle", String.class, User::getHandle)); + + add(Index.create("FIRST_HANDLE_LETTER", String.class, user -> user.getHandle().substring(0, 1))); + }}.build(), true); + } + System.out.println(2); + + // Assert size. + { + Assert.assertEquals(USER_COUNT, Inmemo.size(User.class)); + } + System.out.println(3); + + // Exactly one user with id=123. + { + List users = Inmemo.find(User.class, new IndexConstraint<>("ID", 123L), user -> true); + Assert.assertEquals(1, users.size()); + Assert.assertEquals(123L, users.get(0).getId()); + } + System.out.println(4); + + // Tests that there is no user[id=USER_COUNT + 1], inserts it, waits, tests that the table contains it. + { + Assert.assertEquals(0, Inmemo.find(User.class, new IndexConstraint<>("ID", USER_COUNT + 1), user -> true).size()); + + userDao.insertRandom(); + Thread.sleep(BASE_SLEEP_MS); + + List users = Inmemo.find(User.class, new IndexConstraint<>("ID", USER_COUNT + 1), user -> true); + Assert.assertEquals(1, users.size()); + Assert.assertEquals(USER_COUNT + 1, users.get(0).getId()); + } + System.out.println(5); + + // Tests that there is no user[id=USER_COUNT + 2], inserts it, enforces update, tests that the table contains it. + { + Assert.assertEquals(0, Inmemo.find(User.class, new IndexConstraint<>("ID", USER_COUNT + 2), user -> true).size()); + + userDao.insertRandom(); + Inmemo.update(User.class); + + List users = Inmemo.find(User.class, new IndexConstraint<>("ID", USER_COUNT + 2), user -> true); + Assert.assertEquals(1, users.size()); + Assert.assertEquals(USER_COUNT + 2, users.get(0).getId()); + } + System.out.println(6); + + // Tests using second index that the number of users with first letter 'e' in handle is expected. + { + long eUsers1 = Inmemo.findCount(User.class, new IndexConstraint<>("FIRST_HANDLE_LETTER", "e"), user -> true); + + long eUsers2 = Inmemo.find(User.class, new IndexConstraint<>("FIRST_HANDLE_LETTER", "e"), user -> true).size(); + + int expectedCount = userDao.findCountByHandlePrefix("e"); + Assert.assertEquals(expectedCount, eUsers1); + Assert.assertEquals(expectedCount, eUsers2); + } + System.out.println(7); + + // Tests matcher. + { + long xyUsers = Inmemo.findCount(User.class, new IndexConstraint<>("FIRST_HANDLE_LETTER", "x"), user -> user.getHandle().charAt(1) == 'y'); + + Assert.assertEquals(userDao.findCountByHandlePrefix("xy"), xyUsers); + } + System.out.println(8); + + // Tests that if we change user state in memory, it will no affect table unless we use insertOrUpdate. + { + User user13 = Inmemo.find(User.class, new IndexConstraint<>("ID", 13L), user -> true).get(0); + + user13.setHandle("handle13"); + + org.junit.Assert.assertThat(user13.getHandle(), not(Inmemo.find(User.class, new IndexConstraint<>("ID", 13L), user -> true).get(0).getHandle())); + + Inmemo.insertOrUpdate(user13); + + org.junit.Assert.assertThat(user13.getHandle(), is(Inmemo.find(User.class, new IndexConstraint<>("ID", 13L), user -> true).get(0).getHandle())); + } + System.out.println(9); + + // Tests that findOnly throws exception if non-unique and throwOnNotUnique parameter is true; + { + boolean hasException = false; + User existingUser = null; + try { + existingUser = Inmemo.findOnly(true, User.class, new IndexConstraint<>("ID", USER_COUNT / 5)); + } catch (InmemoException e) { + hasException = true; + } + Assert.assertFalse(hasException); + + User user = userDao.newRandomUser(); + user.setHandle(existingUser.getHandle()); + Inmemo.insertOrUpdate(user); + + List possibleUsers = Arrays.asList(existingUser, user); + + hasException = false; + try { + User foundUser = Inmemo.findOnly(false, User.class, new IndexConstraint<>("handle", user.getHandle())); + Assert.assertNotNull(foundUser); + Assert.assertTrue(possibleUsers.contains(foundUser)); + } catch (InmemoException e) { + hasException = e.getMessage().toLowerCase().contains("unique"); + } + + Assert.assertFalse(hasException); + + hasException = false; + try { + User foundUser = Inmemo.findOnly(true, User.class, new IndexConstraint<>("handle", user.getHandle())); + Assert.assertNotNull(foundUser); + Assert.assertTrue(possibleUsers.contains(foundUser)); + } catch (InmemoException e) { + hasException = true; + } + + Assert.assertTrue(hasException); + } + System.out.println(10); + + // Tests insertOrUpdateByIds method + for (long i = 1; i <= USER_COUNT; i++) { + User user = Inmemo.findOnly(true, User.class, new IndexConstraint<>("ID", i)); + String newHandle = userDao.getRandomString(); + user.setHandle(newHandle); + + userDao.update(user); + Inmemo.insertOrUpdateByIds(User.class, user.getId()); + + User newUser = Inmemo.findOnly(true, User.class, new IndexConstraint<>("ID", user.getId())); + Assert.assertEquals(newHandle, newUser.getHandle()); + } + System.out.println(11); + } + + @Test + public void testCompatibleClasses() throws InterruptedException { + Inmemo.dropTableIfExists(Wrapper.a.class); + + // Test user count. + { + Assert.assertEquals(USER_COUNT, userDao.findAll().size()); + } + + // Create table. + { + Inmemo.createTable(User.class, "ID", null, new Indices.Builder() {{ + add(Index.create("ID", Long.class, User::getId)); + add(Index.create("FIRST_HANDLE_LETTER", String.class, user -> user.getHandle().substring(0, 1))); + }}.build(), true); + } + +// Assert.assertEquals(1, Inmemo.findCount(User2.class, new IndexConstraint("ID", 11), new Matcher() { +// @Override +// public boolean match(User2 tableItem) { +// return true; +// } +// })); + + Assert.assertEquals(1, Inmemo.findCount(Wrapper.a.class, new IndexConstraint("ID", 11L), user -> true)); + + // Tests that there is no user[id=USER_COUNT + 1], inserts it, waits, tests that the table contains it. + { + List users = Inmemo.find(a.class, + new IndexConstraint<>("ID", USER_COUNT + 1), user -> true); + if (users.size() != 0) { + System.out.println(users); + } + + Assert.assertEquals(0, Inmemo.find(Wrapper.a.class, + new IndexConstraint<>("ID", USER_COUNT + 1), user -> true).size()); + + userDao.insertRandom(); + Thread.sleep(BASE_SLEEP_MS); + Assert.assertEquals(1, Inmemo.find(Wrapper.a.class, + new IndexConstraint<>("ID", USER_COUNT + 1), user -> true).size()); + } + + // Tests that there is no user[id=USER_COUNT + 2], inserts it, waits, tests that the table contains it. + { + Assert.assertEquals(0, Inmemo.find(Wrapper.a.class, + new IndexConstraint<>("ID", USER_COUNT + 2), user -> true).size()); + + a x = new a(); + x.setId(USER_COUNT + 2); + x.setHandle("aa"); + Inmemo.insertOrUpdate(x); + + Assert.assertEquals(1, Inmemo.find(Wrapper.a.class, + new IndexConstraint<>("ID", USER_COUNT + 2), user -> true).size()); + + Assert.assertEquals(1, + Inmemo.find(User.class, new IndexConstraint<>("ID", USER_COUNT + 2), user -> user.getPassword() == null).size()); + Assert.assertEquals(0, + Inmemo.find(User.class, new IndexConstraint<>("ID", USER_COUNT + 2), user -> user.getPassword() != null).size()); + } + } + + @Test + public void testUnique() throws InterruptedException { + Inmemo.dropTableIfExists(User.class); + + // Test user count. + { + Assert.assertEquals(USER_COUNT, userDao.findAll().size()); + } + + // Create table. + { + Inmemo.createTable(User.class, "ID", null, new Indices.Builder() {{ + add(Index.createUnique("ID", Long.class, User::getId)); + add(Index.createUnique("handle", String.class, User::getHandle)); + add(Index.create("FIRST_HANDLE_LETTER", String.class, tableItem -> tableItem.getHandle().substring(0, 1))); + }}.build(), true); + } + + // Assert size. + { + Assert.assertEquals(USER_COUNT, Inmemo.size(User.class)); + } + + // Exactly one user with id=123. + { + Assert.assertEquals(1, Inmemo.find(User.class, new IndexConstraint<>("ID", 123L), user -> true).size()); + } + + // No one user with id=123 and false matcher. + { + Assert.assertEquals(0, Inmemo.find(User.class, new IndexConstraint<>("ID", 123L), user -> false).size()); + } + + // Exactly one user with id=123. + { + Assert.assertEquals(1, Inmemo.findCount(User.class, new IndexConstraint<>("ID", 123L))); + } + + // Exactly one user with id=123. + { + Assert.assertEquals(123L, Inmemo.findOnly(true, User.class, new IndexConstraint<>("ID", 123L)).getId()); + } + + // Tests that there is no user[id=USER_COUNT + 1], inserts it, waits, tests that the table contains it. + { + Assert.assertNull(Inmemo.findOnly(false, User.class, new IndexConstraint<>("ID", USER_COUNT + 1))); + + userDao.insertRandom(); + Thread.sleep(BASE_SLEEP_MS); + + Assert.assertEquals(USER_COUNT + 1, Inmemo.findOnly(false, User.class, new IndexConstraint<>("ID", USER_COUNT + 1)).getId()); + } + + // Tests using second index that the number of users with first letter 'e' in handle is expected. + { + long id = USER_COUNT / 2; + + User userById = Inmemo.findOnly(true, User.class, new IndexConstraint<>("ID", id)); + Assert.assertEquals(id, userById.getId()); + + User userByHandle = Inmemo.findOnly(true, User.class, new IndexConstraint<>("handle", userById.getHandle())); + + Assert.assertNotNull(userByHandle); + Assert.assertNotSame(userById, userByHandle); + Assert.assertEquals(userById, userByHandle); + + List usersByFirstHandleLetter = Inmemo.find(User.class, new IndexConstraint<>("FIRST_HANDLE_LETTER", userById.getHandle().substring(0, 1))); + Assert.assertTrue(usersByFirstHandleLetter.contains(userById)); + } + + // Tests that if non-unique then throw exception. + { + User existingUser = Inmemo.findOnly(true, User.class, new IndexConstraint<>("ID", USER_COUNT / 5)); + User user = userDao.newRandomUser(); + user.setHandle(existingUser.getHandle()); + + boolean hasException = false; + try { + Inmemo.insertOrUpdate(user); + } catch (InmemoException e) { + hasException = e.getMessage().toLowerCase().contains("unique"); + } + + Assert.assertTrue(hasException); + } + + // Tests matcher for unique indices + { + long id = USER_COUNT / 5; + User existingUser = Inmemo.findOnly(true, User.class, new IndexConstraint<>("ID", id)); + final String handle = existingUser.getHandle(); + User notFoundUser = Inmemo.findOnly(true, User.class, new IndexConstraint<>("ID", id), + user -> !user.getHandle().equals(handle) + ); + + Assert.assertNull(notFoundUser); + + List emptyUserList = Inmemo.find(User.class, new IndexConstraint<>("ID", id), + user -> !user.getHandle().equals(handle) + ); + + Assert.assertTrue(emptyUserList.isEmpty()); + } + } + + @Test + public void testEmergency() throws InterruptedException { + Inmemo.dropTableIfExists(User.class); + + // Test user count. + { + Assert.assertEquals(USER_COUNT, userDao.findAll().size()); + } + + // Create table. + { + Inmemo.createTable(User.class, "ID", null, new Indices.Builder() {{ + add(Index.createUnique("ID", Long.class, User::getId, indexValue -> new Object[]{"ID", indexValue} + )); + add(Index.createUnique("handle", String.class, User::getHandle)); + add(Index.createUnique("idAndHandle", String.class, user -> user.getId() + "," + user.getHandle(), indexValue -> { + String[] tokens = indexValue.split(","); + return new Object[]{"ID", Long.valueOf(tokens[0]), "HANDLE", tokens[1]}; + } + )); + add(Index.create("FIRST_HANDLE_LETTER", String.class, tableItem -> tableItem.getHandle().substring(0, 1))); + }}.build(), true); + } + + // Assert size. + { + Assert.assertEquals(USER_COUNT, Inmemo.size(User.class)); + } + + Map traces = Thread.getAllStackTraces(); + for (Thread thread : traces.keySet()) { + if ("InmemoUpdater#class com.codeforces.inmemo.model.User".equals(thread.getName())) { + thread.interrupt(); + try { + Thread.sleep(BASE_SLEEP_MS * 2); + } catch (InterruptedException e) { + // No operations. + } + } + } + + userDao.insertRandom(); + Assert.assertEquals(USER_COUNT + 1, userDao.findAll().size()); + Assert.assertEquals(USER_COUNT + 1, Inmemo.findOnly(true, User.class, new IndexConstraint<>("ID", USER_COUNT + 1)).getId()); + + userDao.insertRandom(); + Assert.assertEquals(1, Inmemo.findCount(User.class, new IndexConstraint<>("ID", USER_COUNT + 1))); + Assert.assertEquals(1, Inmemo.findCount(User.class, new IndexConstraint<>("ID", USER_COUNT + 2))); + Assert.assertEquals(0, Inmemo.findCount(User.class, new IndexConstraint<>("ID", USER_COUNT + 3))); + + userDao.insertRandom(); + Assert.assertEquals(1, Inmemo.findCount(User.class, new IndexConstraint<>("ID", USER_COUNT + 3))); + + Assert.assertEquals(0, Inmemo.find(User.class, new IndexConstraint<>("ID", USER_COUNT + 4)).size()); + userDao.insertRandom(); + Assert.assertEquals(1, Inmemo.find(User.class, new IndexConstraint<>("ID", USER_COUNT + 4)).size()); + + User newUser = userDao.newRandomUser(); + userDao.insert(newUser); + User foundUser = Inmemo.findOnly(true, User.class, new IndexConstraint<>("idAndHandle", newUser.getId() + "," + newUser.getHandle())); + newUser = userDao.find(newUser.getId()); + Assert.assertEquals(newUser, foundUser); + } + + @Test + public void testEmergencyDatabaseQuery() throws InterruptedException { + Inmemo.dropTableIfExists(User.class); + + // Test user count. + { + Assert.assertEquals(USER_COUNT, userDao.findAll().size()); + } + + // Create table. + { + Inmemo.createTable(User.class, "ID", null, new Indices.Builder() {{ + add(Index.createUnique("ID", Long.class, User::getId, id -> new Object[]{"id", id} + ) + ); + + add(Index.create("handle", String.class, User::getHandle, handle -> new Object[]{"handle", handle} + )); + + add(Index.create("handle2", String.class, User::getHandle)); + + add(Index.create("FIRST_HANDLE_LETTER", String.class, tableItem -> tableItem.getHandle().substring(0, 1))); + }}.build(), true); + } + + // Assert size. + { + Assert.assertEquals(USER_COUNT, Inmemo.size(User.class)); + } + + { + User newUser = userDao.newRandomUser(); + userDao.insert(newUser); + + List foundUsers = Inmemo.find(User.class, new IndexConstraint<>("ID", newUser.getId())); + newUser = userDao.find(newUser.getId()); + + Assert.assertTrue(foundUsers.contains(newUser)); + } + + { + User newUser = userDao.newRandomUser(); + userDao.insert(newUser); + + List foundUsers = Inmemo.find(User.class, new IndexConstraint<>("handle", newUser.getHandle())); + newUser = userDao.find(newUser.getId()); + + Assert.assertTrue(foundUsers.contains(newUser)); + } + + { + User newUser = userDao.newRandomUser(); + userDao.insert(newUser); + + List foundUsers = Inmemo.find(User.class, new IndexConstraint<>("handle2", newUser.getHandle())); + newUser = userDao.find(newUser.getId()); + + Assert.assertFalse(foundUsers.contains(newUser)); + } + + { + User newUser = userDao.newRandomUser(); + userDao.insert(newUser); + List foundUsers = Inmemo.find(User.class, new IndexConstraint<>("FIRST_HANDLE_LETTER", + newUser.getHandle().substring(0, 1))); + newUser = userDao.find(newUser.getId()); + + Assert.assertFalse(foundUsers.contains(newUser)); + + Thread.sleep(BASE_SLEEP_MS * 2); + + foundUsers = Inmemo.find(User.class, new IndexConstraint<>("FIRST_HANDLE_LETTER", + newUser.getHandle().substring(0, 1))); + Assert.assertTrue(foundUsers.contains(newUser)); + } + } + + @Test + public void testDeadlock() { + // Test user count. + { + Assert.assertEquals(USER_COUNT, userDao.findAll().size()); + } + + Random random = new Random(123L); + + final List userIds = new ArrayList<>(100); + for (int i = 0; i < 100; ++i) + userIds.add((long) random.nextInt((int) USER_COUNT)); + + // Create table. + Inmemo.createTable(User.class, "ID", null, new Indices.Builder() {{ + add(Index.createUnique("ID", Long.class, User::getId, id -> new Object[]{"id", id} + ) + ); + + add(Index.create("handle", String.class, User::getHandle, handle -> new Object[]{"handle", handle} + )); + + add(Index.create("handle2", String.class, User::getHandle)); + + add(Index.create("FIRST_HANDLE_LETTER", String.class, tableItem -> tableItem.getHandle().substring(0, 1))); + }}.build(), false); + + ExecutorService executorService = Executors.newFixedThreadPool(10); + + for (int i = 0; i < 10; ++i) { + executorService.submit(() -> { + for (Long userId : userIds) { + try { + User user = Inmemo.findOnly(true, User.class, new IndexConstraint<>("ID", userId)); + Assert.assertNotNull(user); + } catch (InmemoException e) { + Assert.assertTrue(e.getMessage().contains("Unable to find table for class name")); + } + } + }); + + executorService.submit(() -> { + for (Long userId : userIds) { + try { + Inmemo.insertOrUpdateByIds(User.class, userId); + } catch (InmemoException e) { + Assert.assertTrue(e.getMessage().contains("Unable to find table for class name")); + } + } + }); + } + + executorService.shutdown(); + + try { + Assert.assertTrue(executorService.awaitTermination(10, TimeUnit.SECONDS)); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } + + @Test + public void testJournal() throws IOException { + System.setProperty("Inmemo.UseJournal", "true"); + + File journalFile = new File("User.inmemo"); + journalFile.delete(); + + try { + Inmemo.dropTableIfExists(User.class); + Assert.assertFalse(journalFile.isFile()); + + // Create table. + { + Inmemo.createTable(User.class, "ID", null, new Indices.Builder() {{ + add(Index.create("ID", Long.class, User::getId)); + }}.build(), true); + } + + Map users = new HashMap<>(); + for (long id = 1; id <= USER_COUNT; id++) { + User inmemoUser = Inmemo.findOnly(true, User.class, new IndexConstraint<>("ID", id)); + User dbUser = userDao.find(id); + Assert.assertEquals(dbUser, inmemoUser); + users.put(id, inmemoUser); + } + + Inmemo.dropTableIfExists(User.class); + Assert.assertTrue(journalFile.isFile()); + { + Inmemo.createTable(User.class, "ID", null, new Indices.Builder() {{ + add(Index.create("ID", Long.class, User::getId)); + }}.build(), true); + } + + for (long id = 1; id <= USER_COUNT; id++) { + User inmemoUser = Inmemo.findOnly(true, User.class, new IndexConstraint<>("ID", id)); + User dbUser = userDao.find(id); + Assert.assertEquals(dbUser, inmemoUser); + Assert.assertEquals(users.get(id), inmemoUser); + } + } catch (Exception e) { + System.err.println("Unexpected exception: " + e.getMessage()); + throw new RuntimeException("Unexpected exception: " + e.getMessage(), e); + } finally { + Assert.assertTrue(journalFile.isFile()); + Inmemo.deleteJournal(User.class); + Assert.assertFalse(journalFile.isFile()); + System.setProperty("Inmemo.UseJournal", "false"); + } + } } \ No newline at end of file diff --git a/src/test/java/com/codeforces/inmemo/UnsetJournalSupportTest.java b/src/test/java/com/codeforces/inmemo/UnsetJournalSupportTest.java index 60e56e2..2ea56a1 100644 --- a/src/test/java/com/codeforces/inmemo/UnsetJournalSupportTest.java +++ b/src/test/java/com/codeforces/inmemo/UnsetJournalSupportTest.java @@ -1,178 +1,178 @@ -package com.codeforces.inmemo; - -import com.codeforces.inmemo.model.JournalDisabledUser; -import com.codeforces.inmemo.model.JournalEnabledUser; -import com.codeforces.inmemo.model.JournalLateUnsetUser; -import org.jacuzzi.core.Row; -import org.jacuzzi.core.RowRoll; -import org.junit.After; -import org.junit.Assert; -import org.junit.Before; -import org.junit.Test; - -import java.io.File; -import java.nio.file.Files; - -public class UnsetJournalSupportTest { - private String oldUseJournalProperty; - private File oldJournalsDir; - private File tempJournalsDir; - - @Before - public void setUp() throws Exception { - oldUseJournalProperty = System.getProperty("Inmemo.UseJournal"); - System.setProperty("Inmemo.UseJournal", "true"); - oldJournalsDir = Table.getJournalsDirForTesting(); - tempJournalsDir = Files.createTempDirectory("inmemo-journals-").toFile(); - Table.setJournalsDir(tempJournalsDir); - } - - @After - public void tearDown() { - if (oldUseJournalProperty == null) { - System.clearProperty("Inmemo.UseJournal"); - } else { - System.setProperty("Inmemo.UseJournal", oldUseJournalProperty); - } - - if (oldJournalsDir != null) { - Table.setJournalsDirForTesting(oldJournalsDir); - } - - deleteRecursively(tempJournalsDir); - } - - @Test - public void testUnsetDisablesJournal() throws Exception { - Inmemo.unsetJournalSupport(JournalDisabledUser.class); - Table table = new Table<>(JournalDisabledUser.class, "id", null); - - Assert.assertFalse(table.isUseJournal()); - table.insertOrUpdate(user(1L, "disabled", "disabled@example.com"), - row(1L, "disabled", "disabled@example.com")); - table.writeJournal(); - - Assert.assertFalse(journalFile(JournalDisabledUser.class).exists()); - Assert.assertNull(table.readJournal()); - } - - @Test - public void testStaleJournalFileDeletedOnConstruction() throws Exception { - Inmemo.unsetJournalSupport(JournalDisabledUser.class); - File journalFile = journalFile(JournalDisabledUser.class); - Files.write(journalFile.toPath(), new byte[]{1, 2, 3}); - - Assert.assertTrue(journalFile.isFile()); - new Table<>(JournalDisabledUser.class, "id", null); - - Assert.assertFalse(journalFile.exists()); - } - - @Test - public void testDefaultTableStillJournals() throws Exception { - Table table = new Table<>(JournalEnabledUser.class, "id", null); - - Assert.assertTrue(table.isUseJournal()); - table.insertOrUpdate(enabledUser(2L, "enabled", "enabled@example.com"), - row(2L, "enabled", "enabled@example.com")); - table.writeJournal(); - - Assert.assertTrue(journalFile(JournalEnabledUser.class).isFile()); - RowRoll rows = table.readJournal(); - Assert.assertNotNull(rows); - Assert.assertEquals(1, rows.size()); - - Row savedRow = rows.getRow(0); - Assert.assertEquals(2L, ((Number) savedRow.get("id")).longValue()); - Assert.assertEquals("enabled", savedRow.get("handle")); - Assert.assertEquals("enabled@example.com", savedRow.get("email")); - } - - @Test - public void testUnsetAfterCreateTableThrows() { - try { - Inmemo.putTableForUnsetJournalSupportTestOnly(JournalLateUnsetUser.class); - Inmemo.unsetJournalSupport(JournalLateUnsetUser.class); - Assert.fail("Late first unsetJournalSupport call must fail."); - } catch (IllegalStateException expected) { - // Expected. - } finally { - Inmemo.removeTableForUnsetJournalSupportTestOnly(JournalLateUnsetUser.class); - } - } - - @Test - public void testGlobalOffAndUnsetCompose() throws Exception { - System.clearProperty("Inmemo.UseJournal"); - Inmemo.unsetJournalSupport(JournalDisabledUser.class); - File staleJournalFile = journalFile(JournalDisabledUser.class); - Files.write(staleJournalFile.toPath(), new byte[]{1, 2, 3}); - - Table disabledTable = new Table<>(JournalDisabledUser.class, "id", null); - Table enabledTable = new Table<>(JournalEnabledUser.class, "id", null); - - Assert.assertFalse(disabledTable.isUseJournal()); - Assert.assertFalse(staleJournalFile.exists()); - Assert.assertFalse(enabledTable.isUseJournal()); - } - - @Test - public void testUnsetIsIdempotent() { - Inmemo.unsetJournalSupport(JournalDisabledUser.class); - Inmemo.unsetJournalSupport(JournalDisabledUser.class); - - try { - Inmemo.putTableForUnsetJournalSupportTestOnly(JournalDisabledUser.class); - Inmemo.unsetJournalSupport(JournalDisabledUser.class); - } finally { - Inmemo.removeTableForUnsetJournalSupportTestOnly(JournalDisabledUser.class); - } - } - - private static File journalFile(Class clazz) { - return new File(Table.getJournalsDirForTesting(), clazz.getSimpleName() + ".inmemo"); - } - - private static JournalDisabledUser user(long id, String handle, String email) { - JournalDisabledUser user = new JournalDisabledUser(); - user.setId(id); - user.setHandle(handle); - user.setEmail(email); - return user; - } - - private static JournalEnabledUser enabledUser(long id, String handle, String email) { - JournalEnabledUser user = new JournalEnabledUser(); - user.setId(id); - user.setHandle(handle); - user.setEmail(email); - return user; - } - - private static Row row(long id, String handle, String email) { - Row row = new Row(); - row.put("id", id); - row.put("handle", handle); - row.put("email", email); - return row; - } - - private static void deleteRecursively(File file) { - if (file == null || !file.exists()) { - return; - } - - if (file.isDirectory()) { - File[] files = file.listFiles(); - if (files != null) { - for (File child : files) { - deleteRecursively(child); - } - } - } - - if (!file.delete() && file.exists()) { - throw new AssertionError("Can't delete " + file.getAbsolutePath()); - } - } -} +package com.codeforces.inmemo; + +import com.codeforces.inmemo.model.JournalDisabledUser; +import com.codeforces.inmemo.model.JournalEnabledUser; +import com.codeforces.inmemo.model.JournalLateUnsetUser; +import org.jacuzzi.core.Row; +import org.jacuzzi.core.RowRoll; +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +import java.io.File; +import java.nio.file.Files; + +public class UnsetJournalSupportTest { + private String oldUseJournalProperty; + private File oldJournalsDir; + private File tempJournalsDir; + + @Before + public void setUp() throws Exception { + oldUseJournalProperty = System.getProperty("Inmemo.UseJournal"); + System.setProperty("Inmemo.UseJournal", "true"); + oldJournalsDir = Table.getJournalsDirForTesting(); + tempJournalsDir = Files.createTempDirectory("inmemo-journals-").toFile(); + Table.setJournalsDir(tempJournalsDir); + } + + @After + public void tearDown() { + if (oldUseJournalProperty == null) { + System.clearProperty("Inmemo.UseJournal"); + } else { + System.setProperty("Inmemo.UseJournal", oldUseJournalProperty); + } + + if (oldJournalsDir != null) { + Table.setJournalsDirForTesting(oldJournalsDir); + } + + deleteRecursively(tempJournalsDir); + } + + @Test + public void testUnsetDisablesJournal() throws Exception { + Inmemo.unsetJournalSupport(JournalDisabledUser.class); + Table table = new Table<>(JournalDisabledUser.class, "id", null); + + Assert.assertFalse(table.isUseJournal()); + table.insertOrUpdate(user(1L, "disabled", "disabled@example.com"), + row(1L, "disabled", "disabled@example.com")); + table.writeJournal(); + + Assert.assertFalse(journalFile(JournalDisabledUser.class).exists()); + Assert.assertNull(table.readJournal()); + } + + @Test + public void testStaleJournalFileDeletedOnConstruction() throws Exception { + Inmemo.unsetJournalSupport(JournalDisabledUser.class); + File journalFile = journalFile(JournalDisabledUser.class); + Files.write(journalFile.toPath(), new byte[]{1, 2, 3}); + + Assert.assertTrue(journalFile.isFile()); + new Table<>(JournalDisabledUser.class, "id", null); + + Assert.assertFalse(journalFile.exists()); + } + + @Test + public void testDefaultTableStillJournals() throws Exception { + Table table = new Table<>(JournalEnabledUser.class, "id", null); + + Assert.assertTrue(table.isUseJournal()); + table.insertOrUpdate(enabledUser(2L, "enabled", "enabled@example.com"), + row(2L, "enabled", "enabled@example.com")); + table.writeJournal(); + + Assert.assertTrue(journalFile(JournalEnabledUser.class).isFile()); + RowRoll rows = table.readJournal(); + Assert.assertNotNull(rows); + Assert.assertEquals(1, rows.size()); + + Row savedRow = rows.getRow(0); + Assert.assertEquals(2L, ((Number) savedRow.get("id")).longValue()); + Assert.assertEquals("enabled", savedRow.get("handle")); + Assert.assertEquals("enabled@example.com", savedRow.get("email")); + } + + @Test + public void testUnsetAfterCreateTableThrows() { + try { + Inmemo.putTableForUnsetJournalSupportTestOnly(JournalLateUnsetUser.class); + Inmemo.unsetJournalSupport(JournalLateUnsetUser.class); + Assert.fail("Late first unsetJournalSupport call must fail."); + } catch (IllegalStateException expected) { + // Expected. + } finally { + Inmemo.removeTableForUnsetJournalSupportTestOnly(JournalLateUnsetUser.class); + } + } + + @Test + public void testGlobalOffAndUnsetCompose() throws Exception { + System.clearProperty("Inmemo.UseJournal"); + Inmemo.unsetJournalSupport(JournalDisabledUser.class); + File staleJournalFile = journalFile(JournalDisabledUser.class); + Files.write(staleJournalFile.toPath(), new byte[]{1, 2, 3}); + + Table disabledTable = new Table<>(JournalDisabledUser.class, "id", null); + Table enabledTable = new Table<>(JournalEnabledUser.class, "id", null); + + Assert.assertFalse(disabledTable.isUseJournal()); + Assert.assertFalse(staleJournalFile.exists()); + Assert.assertFalse(enabledTable.isUseJournal()); + } + + @Test + public void testUnsetIsIdempotent() { + Inmemo.unsetJournalSupport(JournalDisabledUser.class); + Inmemo.unsetJournalSupport(JournalDisabledUser.class); + + try { + Inmemo.putTableForUnsetJournalSupportTestOnly(JournalDisabledUser.class); + Inmemo.unsetJournalSupport(JournalDisabledUser.class); + } finally { + Inmemo.removeTableForUnsetJournalSupportTestOnly(JournalDisabledUser.class); + } + } + + private static File journalFile(Class clazz) { + return new File(Table.getJournalsDirForTesting(), clazz.getSimpleName() + ".inmemo"); + } + + private static JournalDisabledUser user(long id, String handle, String email) { + JournalDisabledUser user = new JournalDisabledUser(); + user.setId(id); + user.setHandle(handle); + user.setEmail(email); + return user; + } + + private static JournalEnabledUser enabledUser(long id, String handle, String email) { + JournalEnabledUser user = new JournalEnabledUser(); + user.setId(id); + user.setHandle(handle); + user.setEmail(email); + return user; + } + + private static Row row(long id, String handle, String email) { + Row row = new Row(); + row.put("id", id); + row.put("handle", handle); + row.put("email", email); + return row; + } + + private static void deleteRecursively(File file) { + if (file == null || !file.exists()) { + return; + } + + if (file.isDirectory()) { + File[] files = file.listFiles(); + if (files != null) { + for (File child : files) { + deleteRecursively(child); + } + } + } + + if (!file.delete() && file.exists()) { + throw new AssertionError("Can't delete " + file.getAbsolutePath()); + } + } +} diff --git a/src/test/java/com/codeforces/inmemo/dao/UserDao.java b/src/test/java/com/codeforces/inmemo/dao/UserDao.java index 3f6aff8..2042d6d 100644 --- a/src/test/java/com/codeforces/inmemo/dao/UserDao.java +++ b/src/test/java/com/codeforces/inmemo/dao/UserDao.java @@ -1,20 +1,20 @@ -package com.codeforces.inmemo.dao; - -import com.codeforces.inmemo.model.User; - -import java.util.List; - -/** - * @author MikeMirzayanov (mirzayanovmr@gmail.com) - */ -public interface UserDao { - User find(long id); - void insertRandom(); - void update(User user); - void insert(User user); - List findAll(); - User newRandomUser(); - int findCountByHandlePrefix(String prefix); - - String getRandomString(); -} +package com.codeforces.inmemo.dao; + +import com.codeforces.inmemo.model.User; + +import java.util.List; + +/** + * @author MikeMirzayanov (mirzayanovmr@gmail.com) + */ +public interface UserDao { + User find(long id); + void insertRandom(); + void update(User user); + void insert(User user); + List findAll(); + User newRandomUser(); + int findCountByHandlePrefix(String prefix); + + String getRandomString(); +} diff --git a/src/test/java/com/codeforces/inmemo/dao/impl/UserDaoImpl.java b/src/test/java/com/codeforces/inmemo/dao/impl/UserDaoImpl.java index ea6345a..2e83eee 100644 --- a/src/test/java/com/codeforces/inmemo/dao/impl/UserDaoImpl.java +++ b/src/test/java/com/codeforces/inmemo/dao/impl/UserDaoImpl.java @@ -1,60 +1,60 @@ -package com.codeforces.inmemo.dao.impl; - -import com.codeforces.inmemo.dao.UserDao; -import com.codeforces.inmemo.model.User; -import org.jacuzzi.core.GenericDaoImpl; - -import javax.sql.DataSource; -import java.util.Date; -import java.util.Random; - -/** - * @author MikeMirzayanov (mirzayanovmr@gmail.com) - */ -public class UserDaoImpl extends GenericDaoImpl implements UserDao { - private Random random = new Random(); - - public UserDaoImpl(DataSource dataSource) { - super(dataSource); - } - - @Override - public User find(long id) { - return super.find(id); - } - - @Override - public void insertRandom() { - insert(newRandomUser()); - } - - @Override - public User newRandomUser() { - User user = new User(); - user.setHandle(getRandomString()); - user.setEmail(getRandomString() + "@gmail.com"); - user.setOpenId(getRandomString() + "@openid"); - user.setPassword("pw" + getRandomString()); - user.setCreationTime(new Date(new Date().getTime() + random.nextInt(1000000))); - user.setLastOnlineTime(new Date(new Date().getTime() + random.nextInt(1000000))); - user.setDisabled(random.nextBoolean()); - user.setAdmin(random.nextBoolean()); - return user; - } - - @Override - public int findCountByHandlePrefix(String prefix) { - return (int) findCountBy("SUBSTRING(HANDLE, 1, ?)=?", prefix.length(), prefix); - } - - public String getRandomString() { - StringBuilder result = new StringBuilder(); - - for (int i = 0; i < 16; i++) { - result.append((char)('a' + random.nextInt(26))); - } - - return result.toString(); - } - -} +package com.codeforces.inmemo.dao.impl; + +import com.codeforces.inmemo.dao.UserDao; +import com.codeforces.inmemo.model.User; +import org.jacuzzi.core.GenericDaoImpl; + +import javax.sql.DataSource; +import java.util.Date; +import java.util.Random; + +/** + * @author MikeMirzayanov (mirzayanovmr@gmail.com) + */ +public class UserDaoImpl extends GenericDaoImpl implements UserDao { + private Random random = new Random(); + + public UserDaoImpl(DataSource dataSource) { + super(dataSource); + } + + @Override + public User find(long id) { + return super.find(id); + } + + @Override + public void insertRandom() { + insert(newRandomUser()); + } + + @Override + public User newRandomUser() { + User user = new User(); + user.setHandle(getRandomString()); + user.setEmail(getRandomString() + "@gmail.com"); + user.setOpenId(getRandomString() + "@openid"); + user.setPassword("pw" + getRandomString()); + user.setCreationTime(new Date(new Date().getTime() + random.nextInt(1000000))); + user.setLastOnlineTime(new Date(new Date().getTime() + random.nextInt(1000000))); + user.setDisabled(random.nextBoolean()); + user.setAdmin(random.nextBoolean()); + return user; + } + + @Override + public int findCountByHandlePrefix(String prefix) { + return (int) findCountBy("SUBSTRING(HANDLE, 1, ?)=?", prefix.length(), prefix); + } + + public String getRandomString() { + StringBuilder result = new StringBuilder(); + + for (int i = 0; i < 16; i++) { + result.append((char)('a' + random.nextInt(26))); + } + + return result.toString(); + } + +} diff --git a/src/test/java/com/codeforces/inmemo/model/JournalDisabledUser.java b/src/test/java/com/codeforces/inmemo/model/JournalDisabledUser.java index 2ea8698..b96d0aa 100644 --- a/src/test/java/com/codeforces/inmemo/model/JournalDisabledUser.java +++ b/src/test/java/com/codeforces/inmemo/model/JournalDisabledUser.java @@ -1,36 +1,36 @@ -package com.codeforces.inmemo.model; - -import com.codeforces.inmemo.HasId; -import org.jacuzzi.mapping.Id; - -public class JournalDisabledUser implements HasId { - @Id - private long id; - private String handle; - private String email; - - @Override - public long getId() { - return id; - } - - public void setId(long id) { - this.id = id; - } - - public String getHandle() { - return handle; - } - - public void setHandle(String handle) { - this.handle = handle; - } - - public String getEmail() { - return email; - } - - public void setEmail(String email) { - this.email = email; - } -} +package com.codeforces.inmemo.model; + +import com.codeforces.inmemo.HasId; +import org.jacuzzi.mapping.Id; + +public class JournalDisabledUser implements HasId { + @Id + private long id; + private String handle; + private String email; + + @Override + public long getId() { + return id; + } + + public void setId(long id) { + this.id = id; + } + + public String getHandle() { + return handle; + } + + public void setHandle(String handle) { + this.handle = handle; + } + + public String getEmail() { + return email; + } + + public void setEmail(String email) { + this.email = email; + } +} diff --git a/src/test/java/com/codeforces/inmemo/model/JournalEnabledUser.java b/src/test/java/com/codeforces/inmemo/model/JournalEnabledUser.java index 87a3bd4..3d2bb53 100644 --- a/src/test/java/com/codeforces/inmemo/model/JournalEnabledUser.java +++ b/src/test/java/com/codeforces/inmemo/model/JournalEnabledUser.java @@ -1,36 +1,36 @@ -package com.codeforces.inmemo.model; - -import com.codeforces.inmemo.HasId; -import org.jacuzzi.mapping.Id; - -public class JournalEnabledUser implements HasId { - @Id - private long id; - private String handle; - private String email; - - @Override - public long getId() { - return id; - } - - public void setId(long id) { - this.id = id; - } - - public String getHandle() { - return handle; - } - - public void setHandle(String handle) { - this.handle = handle; - } - - public String getEmail() { - return email; - } - - public void setEmail(String email) { - this.email = email; - } -} +package com.codeforces.inmemo.model; + +import com.codeforces.inmemo.HasId; +import org.jacuzzi.mapping.Id; + +public class JournalEnabledUser implements HasId { + @Id + private long id; + private String handle; + private String email; + + @Override + public long getId() { + return id; + } + + public void setId(long id) { + this.id = id; + } + + public String getHandle() { + return handle; + } + + public void setHandle(String handle) { + this.handle = handle; + } + + public String getEmail() { + return email; + } + + public void setEmail(String email) { + this.email = email; + } +} diff --git a/src/test/java/com/codeforces/inmemo/model/JournalLateUnsetUser.java b/src/test/java/com/codeforces/inmemo/model/JournalLateUnsetUser.java index cd81193..707f7ff 100644 --- a/src/test/java/com/codeforces/inmemo/model/JournalLateUnsetUser.java +++ b/src/test/java/com/codeforces/inmemo/model/JournalLateUnsetUser.java @@ -1,36 +1,36 @@ -package com.codeforces.inmemo.model; - -import com.codeforces.inmemo.HasId; -import org.jacuzzi.mapping.Id; - -public class JournalLateUnsetUser implements HasId { - @Id - private long id; - private String handle; - private String email; - - @Override - public long getId() { - return id; - } - - public void setId(long id) { - this.id = id; - } - - public String getHandle() { - return handle; - } - - public void setHandle(String handle) { - this.handle = handle; - } - - public String getEmail() { - return email; - } - - public void setEmail(String email) { - this.email = email; - } -} +package com.codeforces.inmemo.model; + +import com.codeforces.inmemo.HasId; +import org.jacuzzi.mapping.Id; + +public class JournalLateUnsetUser implements HasId { + @Id + private long id; + private String handle; + private String email; + + @Override + public long getId() { + return id; + } + + public void setId(long id) { + this.id = id; + } + + public String getHandle() { + return handle; + } + + public void setHandle(String handle) { + this.handle = handle; + } + + public String getEmail() { + return email; + } + + public void setEmail(String email) { + this.email = email; + } +} diff --git a/src/test/java/com/codeforces/inmemo/model/Person.java b/src/test/java/com/codeforces/inmemo/model/Person.java index 2d485bb..7d49961 100644 --- a/src/test/java/com/codeforces/inmemo/model/Person.java +++ b/src/test/java/com/codeforces/inmemo/model/Person.java @@ -1,33 +1,33 @@ -package com.codeforces.inmemo.model; - -import com.codeforces.inmemo.HasId; -import org.jacuzzi.mapping.Id; - -/** @author MikeMirzayanov (mirzayanovmr@gmail.com) */ -public class Person implements HasId { - @Id - private long id; - private Sex sex; - - @Override - public long getId() { - return id; - } - - public void setId(long id) { - this.id = id; - } - - public Sex getSex() { - return sex; - } - - public void setSex(Sex sex) { - this.sex = sex; - } - - public enum Sex { - MALE, - FEMALE - } -} +package com.codeforces.inmemo.model; + +import com.codeforces.inmemo.HasId; +import org.jacuzzi.mapping.Id; + +/** @author MikeMirzayanov (mirzayanovmr@gmail.com) */ +public class Person implements HasId { + @Id + private long id; + private Sex sex; + + @Override + public long getId() { + return id; + } + + public void setId(long id) { + this.id = id; + } + + public Sex getSex() { + return sex; + } + + public void setSex(Sex sex) { + this.sex = sex; + } + + public enum Sex { + MALE, + FEMALE + } +} diff --git a/src/test/java/com/codeforces/inmemo/model/User.java b/src/test/java/com/codeforces/inmemo/model/User.java index 3f2d97c..1c436a3 100644 --- a/src/test/java/com/codeforces/inmemo/model/User.java +++ b/src/test/java/com/codeforces/inmemo/model/User.java @@ -1,182 +1,182 @@ -package com.codeforces.inmemo.model; - -import com.codeforces.inmemo.HasId; -import org.jacuzzi.mapping.Id; - -import java.util.Date; - -/** @author MikeMirzayanov (mirzayanovmr@gmail.com) */ -public class User implements HasId { - @Id - private long id; - private String handle; - private String openId; - private String email; - private String password; - private boolean admin; - private Date creationTime; - private Date lastOnlineTime; - private boolean disabled; - private TShirtSize tShirtSize; - - public long getId() { - return id; - } - - public void setId(long id) { - this.id = id; - } - - public String getHandle() { - return handle; - } - - public void setHandle(String handle) { - this.handle = handle; - } - - public String getOpenId() { - return openId; - } - - public void setOpenId(String openId) { - this.openId = openId; - } - - public String getEmail() { - return email; - } - - public void setEmail(String email) { - this.email = email; - } - - public String getPassword() { - return password; - } - - public void setPassword(String password) { - this.password = password; - } - - public boolean isAdmin() { - return admin; - } - - public void setAdmin(boolean admin) { - this.admin = admin; - } - - public Date getCreationTime() { - return creationTime == null ? null : new Date(creationTime.getTime()); - } - - public void setCreationTime(Date creationTime) { - this.creationTime = creationTime; - } - - public Date getLastOnlineTime() { - return lastOnlineTime; - } - - public void setLastOnlineTime(Date lastOnlineTime) { - this.lastOnlineTime = lastOnlineTime; - } - - public boolean isDisabled() { - return disabled; - } - - public void setDisabled(boolean disabled) { - this.disabled = disabled; - } - - public TShirtSize getTShirtSize() { - return tShirtSize; - } - - public void setTShirtSize(TShirtSize tShirtSize) { - this.tShirtSize = tShirtSize; - } - - public int getSomething(int arg) { - return arg + 2; - } - - public int getSomethingWithTwoArgs(int arg1, int arg2) { - return arg1 + arg2; - } - - public void setSomething() {} - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - - User user = (User) o; - - if (id != user.id) return false; - if (admin != user.admin) return false; - if (disabled != user.disabled) return false; - if (handle != null ? !handle.equals(user.handle) : user.handle != null) return false; - if (openId != null ? !openId.equals(user.openId) : user.openId != null) return false; - if (email != null ? !email.equals(user.email) : user.email != null) return false; - if (password != null ? !password.equals(user.password) : user.password != null) return false; - if (creationTime != null ? !creationTime.equals(user.creationTime) : user.creationTime != null) return false; - if (lastOnlineTime != null ? !lastOnlineTime.equals(user.lastOnlineTime) : user.lastOnlineTime != null) - return false; - return tShirtSize == user.tShirtSize; - - } - - @Override - public int hashCode() { - int result = (int) (id ^ (id >>> 32)); - result = 31 * result + (handle != null ? handle.hashCode() : 0); - result = 31 * result + (openId != null ? openId.hashCode() : 0); - result = 31 * result + (email != null ? email.hashCode() : 0); - result = 31 * result + (password != null ? password.hashCode() : 0); - result = 31 * result + (admin ? 1 : 0); - result = 31 * result + (creationTime != null ? creationTime.hashCode() : 0); - result = 31 * result + (lastOnlineTime != null ? lastOnlineTime.hashCode() : 0); - result = 31 * result + (disabled ? 1 : 0); - result = 31 * result + (tShirtSize != null ? tShirtSize.hashCode() : 0); - return result; - } - - @Override - public String toString() { - return "User{" + - "id=" + id + - ", handle='" + handle + '\'' + - ", openId='" + openId + '\'' + - ", email='" + email + '\'' + - ", password='" + password + '\'' + - ", admin=" + admin + - ", creationTime=" + creationTime + - ", lastOnlineTime=" + lastOnlineTime + - ", disabled=" + disabled + - ", tShirtSize=" + tShirtSize + - '}'; - } - - public enum TShirtSize { - XS, - S, - M, - L, - XL, - XXL, - XXXL, - XXXXL, - XXXXXL, - WXXS, - WXS, - WS, - WM, - WL, - WXL, - WXXL - } -} +package com.codeforces.inmemo.model; + +import com.codeforces.inmemo.HasId; +import org.jacuzzi.mapping.Id; + +import java.util.Date; + +/** @author MikeMirzayanov (mirzayanovmr@gmail.com) */ +public class User implements HasId { + @Id + private long id; + private String handle; + private String openId; + private String email; + private String password; + private boolean admin; + private Date creationTime; + private Date lastOnlineTime; + private boolean disabled; + private TShirtSize tShirtSize; + + public long getId() { + return id; + } + + public void setId(long id) { + this.id = id; + } + + public String getHandle() { + return handle; + } + + public void setHandle(String handle) { + this.handle = handle; + } + + public String getOpenId() { + return openId; + } + + public void setOpenId(String openId) { + this.openId = openId; + } + + public String getEmail() { + return email; + } + + public void setEmail(String email) { + this.email = email; + } + + public String getPassword() { + return password; + } + + public void setPassword(String password) { + this.password = password; + } + + public boolean isAdmin() { + return admin; + } + + public void setAdmin(boolean admin) { + this.admin = admin; + } + + public Date getCreationTime() { + return creationTime == null ? null : new Date(creationTime.getTime()); + } + + public void setCreationTime(Date creationTime) { + this.creationTime = creationTime; + } + + public Date getLastOnlineTime() { + return lastOnlineTime; + } + + public void setLastOnlineTime(Date lastOnlineTime) { + this.lastOnlineTime = lastOnlineTime; + } + + public boolean isDisabled() { + return disabled; + } + + public void setDisabled(boolean disabled) { + this.disabled = disabled; + } + + public TShirtSize getTShirtSize() { + return tShirtSize; + } + + public void setTShirtSize(TShirtSize tShirtSize) { + this.tShirtSize = tShirtSize; + } + + public int getSomething(int arg) { + return arg + 2; + } + + public int getSomethingWithTwoArgs(int arg1, int arg2) { + return arg1 + arg2; + } + + public void setSomething() {} + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + User user = (User) o; + + if (id != user.id) return false; + if (admin != user.admin) return false; + if (disabled != user.disabled) return false; + if (handle != null ? !handle.equals(user.handle) : user.handle != null) return false; + if (openId != null ? !openId.equals(user.openId) : user.openId != null) return false; + if (email != null ? !email.equals(user.email) : user.email != null) return false; + if (password != null ? !password.equals(user.password) : user.password != null) return false; + if (creationTime != null ? !creationTime.equals(user.creationTime) : user.creationTime != null) return false; + if (lastOnlineTime != null ? !lastOnlineTime.equals(user.lastOnlineTime) : user.lastOnlineTime != null) + return false; + return tShirtSize == user.tShirtSize; + + } + + @Override + public int hashCode() { + int result = (int) (id ^ (id >>> 32)); + result = 31 * result + (handle != null ? handle.hashCode() : 0); + result = 31 * result + (openId != null ? openId.hashCode() : 0); + result = 31 * result + (email != null ? email.hashCode() : 0); + result = 31 * result + (password != null ? password.hashCode() : 0); + result = 31 * result + (admin ? 1 : 0); + result = 31 * result + (creationTime != null ? creationTime.hashCode() : 0); + result = 31 * result + (lastOnlineTime != null ? lastOnlineTime.hashCode() : 0); + result = 31 * result + (disabled ? 1 : 0); + result = 31 * result + (tShirtSize != null ? tShirtSize.hashCode() : 0); + return result; + } + + @Override + public String toString() { + return "User{" + + "id=" + id + + ", handle='" + handle + '\'' + + ", openId='" + openId + '\'' + + ", email='" + email + '\'' + + ", password='" + password + '\'' + + ", admin=" + admin + + ", creationTime=" + creationTime + + ", lastOnlineTime=" + lastOnlineTime + + ", disabled=" + disabled + + ", tShirtSize=" + tShirtSize + + '}'; + } + + public enum TShirtSize { + XS, + S, + M, + L, + XL, + XXL, + XXXL, + XXXXL, + XXXXXL, + WXXS, + WXS, + WS, + WM, + WL, + WXL, + WXXL + } +} diff --git a/src/test/java/com/codeforces/inmemo/model/User2.java b/src/test/java/com/codeforces/inmemo/model/User2.java index 88f8b11..3bdf51b 100644 --- a/src/test/java/com/codeforces/inmemo/model/User2.java +++ b/src/test/java/com/codeforces/inmemo/model/User2.java @@ -1,92 +1,92 @@ -package com.codeforces.inmemo.model; - -import com.codeforces.inmemo.HasId; -import org.jacuzzi.mapping.Id; - -import java.util.Date; - -/** @author MikeMirzayanov (mirzayanovmr@gmail.com) */ -public class User2 implements HasId { - @Id - private long id; - private String handle; - private String openId; - private String email; - private String password; - private boolean admin; - private Date creationTime; - private Date lastOnlineTime; - private boolean disabled; - - public long getId() { - return id; - } - - public void setId(long id) { - this.id = id; - } - - public String getHandle() { - return handle; - } - - public void setHandle(String handle) { - this.handle = handle; - } - - public String getOpenId() { - return openId; - } - - public void setOpenId(String openId) { - this.openId = openId; - } - - public String getEmail() { - return email; - } - - public void setEmail(String email) { - this.email = email; - } - - public String getPassword() { - return password; - } - - public void setPassword(String password) { - this.password = password; - } - - public boolean isAdmin() { - return admin; - } - - public void setAdmin(boolean admin) { - this.admin = admin; - } - - public Date getCreationTime() { - return creationTime; - } - - public void setCreationTime(Date creationTime) { - this.creationTime = creationTime; - } - - public Date getLastOnlineTime() { - return lastOnlineTime; - } - - public void setLastOnlineTime(Date lastOnlineTime) { - this.lastOnlineTime = lastOnlineTime; - } - - public boolean isDisabled() { - return disabled; - } - - public void setDisabled(boolean disabled) { - this.disabled = disabled; - } -} +package com.codeforces.inmemo.model; + +import com.codeforces.inmemo.HasId; +import org.jacuzzi.mapping.Id; + +import java.util.Date; + +/** @author MikeMirzayanov (mirzayanovmr@gmail.com) */ +public class User2 implements HasId { + @Id + private long id; + private String handle; + private String openId; + private String email; + private String password; + private boolean admin; + private Date creationTime; + private Date lastOnlineTime; + private boolean disabled; + + public long getId() { + return id; + } + + public void setId(long id) { + this.id = id; + } + + public String getHandle() { + return handle; + } + + public void setHandle(String handle) { + this.handle = handle; + } + + public String getOpenId() { + return openId; + } + + public void setOpenId(String openId) { + this.openId = openId; + } + + public String getEmail() { + return email; + } + + public void setEmail(String email) { + this.email = email; + } + + public String getPassword() { + return password; + } + + public void setPassword(String password) { + this.password = password; + } + + public boolean isAdmin() { + return admin; + } + + public void setAdmin(boolean admin) { + this.admin = admin; + } + + public Date getCreationTime() { + return creationTime; + } + + public void setCreationTime(Date creationTime) { + this.creationTime = creationTime; + } + + public Date getLastOnlineTime() { + return lastOnlineTime; + } + + public void setLastOnlineTime(Date lastOnlineTime) { + this.lastOnlineTime = lastOnlineTime; + } + + public boolean isDisabled() { + return disabled; + } + + public void setDisabled(boolean disabled) { + this.disabled = disabled; + } +} diff --git a/src/test/java/com/codeforces/inmemo/model/Wrapper.java b/src/test/java/com/codeforces/inmemo/model/Wrapper.java index 3551c95..4df7d61 100644 --- a/src/test/java/com/codeforces/inmemo/model/Wrapper.java +++ b/src/test/java/com/codeforces/inmemo/model/Wrapper.java @@ -1,9 +1,9 @@ -package com.codeforces.inmemo.model; - -/** - * @author MikeMirzayanov (mirzayanovmr@gmail.com) - */ -public class Wrapper { - public static class a extends User { - } -} +package com.codeforces.inmemo.model; + +/** + * @author MikeMirzayanov (mirzayanovmr@gmail.com) + */ +public class Wrapper { + public static class a extends User { + } +} diff --git a/src/test/resources/log4j.properties b/src/test/resources/log4j.properties index 98f2276..ccfd652 100644 --- a/src/test/resources/log4j.properties +++ b/src/test/resources/log4j.properties @@ -1,6 +1,6 @@ -log4j.rootCategory=INFO,stdout -log4j.logger.com.codeforces.inmemo=INFO - -log4j.appender.stdout=org.apache.log4j.ConsoleAppender -log4j.appender.stdout.layout=org.apache.log4j.PatternLayout -log4j.appender.stdout.layout.ConversionPattern=%-5p [%d{yyyy-MM-dd HH:mm:ss,SSS}] %C{1}: %m%n +log4j.rootCategory=INFO,stdout +log4j.logger.com.codeforces.inmemo=INFO + +log4j.appender.stdout=org.apache.log4j.ConsoleAppender +log4j.appender.stdout.layout=org.apache.log4j.PatternLayout +log4j.appender.stdout.layout.ConversionPattern=%-5p [%d{yyyy-MM-dd HH:mm:ss,SSS}] %C{1}: %m%n