diff --git a/pom.xml b/pom.xml index ec76020dc..29f224587 100755 --- a/pom.xml +++ b/pom.xml @@ -133,8 +133,8 @@ -Xlint:deprecation 1.6 1.6 - 1.6 - 1.6 + 1.8 + 1.8 UTF-8 diff --git a/src/main/java/net/openhft/chronicle/map/ChronicleMapBuilder.java b/src/main/java/net/openhft/chronicle/map/ChronicleMapBuilder.java index be93dadf3..7ae77d5bf 100644 --- a/src/main/java/net/openhft/chronicle/map/ChronicleMapBuilder.java +++ b/src/main/java/net/openhft/chronicle/map/ChronicleMapBuilder.java @@ -18,6 +18,9 @@ package net.openhft.chronicle.map; import net.openhft.chronicle.map.serialization.MetaBytesInterop; import net.openhft.chronicle.map.serialization.MetaBytesWriter; +import net.openhft.chronicle.map.serialization.MetaProvider; +import net.openhft.chronicle.map.threadlocal.Provider; +import net.openhft.chronicle.map.threadlocal.ThreadLocalCopies; import net.openhft.lang.Maths; import net.openhft.lang.io.*; import net.openhft.lang.io.serialization.*; @@ -66,6 +69,8 @@ public class ChronicleMapBuilder implements Cloneable { private ObjectSerializer objectSerializer; private MapEventListener> eventListener = MapEventListeners.nop(); + private V defaultValue = null; + private DefaultValueProvider defaultValueProvider = NullValueProvider.INSTANCE; ChronicleMapBuilder(Class keyClass, Class valueClass) { keyBuilder = new SerializationBuilder(keyClass, SerializationBuilder.Role.KEY); @@ -459,6 +464,65 @@ public class ChronicleMapBuilder implements Cloneable { return eventListener; } + /** + * Specifies the value to be put for each key queried in {@link ChronicleMap#get get()} and + * {@link ChronicleMap#getUsing(Object, Object) getUsing()} methods, if the key is absent + * in the map. Then this default value is returned from query method. + * + *

Setting default value to {@code null} is interpreted as map shouldn't put any default + * value for absent keys. This is by default. + * + *

This configuration overrides any previous + * {@link #defaultValueProvider(DefaultValueProvider)} configuration to this + * {@code ChronicleMapBuilder}. + * + * @param defaultValue the default value to be put to the map for absent keys during + * {@code get()} and {@code getUsing()} calls and returned from these calls + * @return this builder object back + */ + public ChronicleMapBuilder defaultValue(V defaultValue) { + this.defaultValue = defaultValue; + this.defaultValueProvider = null; + return this; + } + + /** + * Specifies the function to obtain a value for the key during {@link ChronicleMap#get get()} + * and {@link ChronicleMap#getUsing(Object, Object) getUsing()} calls, if the key is absent + * in the map. If the obtained value is non-null, it is put for the key in the map and then + * returned from current {@code get()} or {@code getUsing()} call. + * + *

This configuration overrides any previous {@link #defaultValue(Object)} configuration + * to this {@code ChronicleMapBuilder}. + * + * @param defaultValueProvider the strategy to obtain a default value by the absent key + * @return this builder object back + */ + public ChronicleMapBuilder defaultValueProvider( + DefaultValueProvider defaultValueProvider) { + this.defaultValueProvider = defaultValueProvider; + return this; + } + + /** + * Non-public because should be called only after {@link #preMapConstruction()} + */ + DefaultValueProvider defaultValueProvider() { + if (defaultValueProvider != null) + return defaultValueProvider; + if (defaultValue == null) + return NullValueProvider.INSTANCE; + Object originalValueWriter = valueBuilder.interop(); + Provider writerProvider = Provider.of(originalValueWriter.getClass()); + ThreadLocalCopies copies = writerProvider.getCopies(null); + Object valueWriter = writerProvider.get(copies, originalValueWriter); + MetaProvider metaWriterProvider = valueBuilder.metaInteropProvider(); + copies = metaWriterProvider.getCopies(copies); + MetaBytesWriter metaValueWriter = metaWriterProvider.get(copies, + valueBuilder.metaInterop(), valueWriter, defaultValue); + return new ConstantValueProvider(defaultValue, metaValueWriter, valueWriter); + } + public ChronicleMap create(File file) throws IOException { for (int i = 0; i < 10; i++) { if (file.exists() && file.length() > 0) { @@ -524,6 +588,9 @@ public class ChronicleMapBuilder implements Cloneable { } private void preMapConstruction() { + keyBuilder.objectSerializer(objectSerializer()); + valueBuilder.objectSerializer(objectSerializer()); + int maxSize = entrySize() * figureBufferAllocationFactor(); keyBuilder.maxSize(maxSize); valueBuilder.maxSize(maxSize); diff --git a/src/main/java/net/openhft/chronicle/map/ConstantValueProvider.java b/src/main/java/net/openhft/chronicle/map/ConstantValueProvider.java new file mode 100644 index 000000000..b6b9ff825 --- /dev/null +++ b/src/main/java/net/openhft/chronicle/map/ConstantValueProvider.java @@ -0,0 +1,52 @@ +/* + * Copyright 2014 Higher Frequency Trading http://www.higherfrequencytrading.com + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.openhft.chronicle.map; + +import net.openhft.chronicle.map.serialization.BytesReader; +import net.openhft.chronicle.map.serialization.MetaBytesWriter; +import net.openhft.lang.io.ByteBufferBytes; + +import java.nio.ByteBuffer; + +final class ConstantValueProvider implements DefaultValueProvider { + private static final long serialVersionUID = 0L; + + private transient V value; + private final int size; + private final byte[] serializedValueBytes; + + ConstantValueProvider(V value, MetaBytesWriter metaValueWriter, W valueWriter) { + this.value = value; + size = (int) metaValueWriter.size(valueWriter, value); + serializedValueBytes = new byte[size]; + ByteBufferBytes bytes = new ByteBufferBytes(ByteBuffer.wrap(serializedValueBytes)); + metaValueWriter.write(valueWriter, bytes, value); + } + + boolean wasDeserialized() { + return value == null; + } + + void initTransients(BytesReader reader) { + value = reader.read(new ByteBufferBytes(ByteBuffer.wrap(serializedValueBytes)), size); + } + + @Override + public V get(K key, V usingValue) { + return value; + } +} diff --git a/src/main/java/net/openhft/chronicle/map/DefaultValueProvider.java b/src/main/java/net/openhft/chronicle/map/DefaultValueProvider.java new file mode 100644 index 000000000..a179e12ee --- /dev/null +++ b/src/main/java/net/openhft/chronicle/map/DefaultValueProvider.java @@ -0,0 +1,45 @@ +/* + * Copyright 2014 Higher Frequency Trading http://www.higherfrequencytrading.com + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.openhft.chronicle.map; + +import java.io.Serializable; + +/** + * Default value computation strategy, used + * in {@link ChronicleMapBuilder#defaultValueProvider(DefaultValueProvider)} configuration. + * + * @param map key class + * @param map value class + */ +public interface DefaultValueProvider extends Serializable { + /** + * Returns a value to be put and returned from {@link ChronicleMap#get(Object)} get()} or + * {@link ChronicleMap#getUsing(Object, Object)} call for the specified key, if it is absent + * in the map. + * + *

If this method returns {@code null}, it isn't put for the key in the map. + * + * @param key key absent in the map + * @param usingValue an object provided as second parameter of current + * {@link ChronicleMap#getUsing(Object, Object)} call, or {@code null}, if this method is called + * within {@code get()} call. This object might be optionally initialized and returned back + * from this method. + * @return value to be put for the specified key in the map, or {@code null}, if the key should + * remain absent in the map + */ + V get(K key, V usingValue); +} diff --git a/src/main/java/net/openhft/chronicle/map/MapEventListener.java b/src/main/java/net/openhft/chronicle/map/MapEventListener.java index 68b4e88a2..8bde8ff79 100644 --- a/src/main/java/net/openhft/chronicle/map/MapEventListener.java +++ b/src/main/java/net/openhft/chronicle/map/MapEventListener.java @@ -31,27 +31,15 @@ public abstract class MapEventListener> private static final long serialVersionUID = 0L; /** - * This is called when there was no existing entry for a key. - * Optionally you can provide a value to add to the map. + * This method is called if the key is found in the map during {@link ChronicleMap#get get}, + * {@link ChronicleMap#getUsing getUsing} or {@link ChronicleMap#acquireUsing acquireUsing} + * method call. * - * @param map accessed - * @param keyBytes bytes of the key looked up - * @param key object used as key - * @param usingValue value provided to reuse, could be null. - * @return null if null should be returned, or a value to put in the map and return. - */ - public V onGetMissing(M map, K key, V usingValue) { - return null; - } - - /** - * This method is called if a value is found in the map. - * - * @param map accessed + * @param map the accessed map * @param entry bytes of the entry - * @param metaDataBytes length of meta data for this map. - * @param key looked up - * @param value found + * @param metaDataBytes length of meta data for this map + * @param key the key looked up + * @param value the value found for the key */ public void onGetFound(M map, Bytes entry, int metaDataBytes, K key, V value) { // do nothing diff --git a/src/main/java/net/openhft/chronicle/map/MapEventListeners.java b/src/main/java/net/openhft/chronicle/map/MapEventListeners.java index e07915ba6..4f23ffa18 100644 --- a/src/main/java/net/openhft/chronicle/map/MapEventListeners.java +++ b/src/main/java/net/openhft/chronicle/map/MapEventListeners.java @@ -35,13 +35,6 @@ public final class MapEventListeners { private static final long serialVersionUID = 0L; public final Logger LOGGER = LoggerFactory.getLogger(getClass()); - @Override - public Object onGetMissing(ChronicleMap map, - Object key, Object usingValue) { - LOGGER.info("{} missed {}", map.file(), key); - return null; - } - @Override public void onGetFound(ChronicleMap map, Bytes entry, int metaDataBytes, Object key, Object value) { @@ -84,13 +77,6 @@ public final class MapEventListeners { private static final long serialVersionUID = 0L; public final Logger LOGGER = LoggerFactory.getLogger(getClass()); - @Override - public Object onGetMissing(ChronicleMap map, - Object key, Object usingValue) { - LOGGER.info("{} missed {}", map.file(), key); - return null; - } - @Override public void onGetFound(ChronicleMap map, Bytes entry, int metaDataBytes, Object key, Object value) { diff --git a/src/main/java/net/openhft/chronicle/map/NullValueProvider.java b/src/main/java/net/openhft/chronicle/map/NullValueProvider.java new file mode 100644 index 000000000..c92ec6948 --- /dev/null +++ b/src/main/java/net/openhft/chronicle/map/NullValueProvider.java @@ -0,0 +1,26 @@ +/* + * Copyright 2014 Higher Frequency Trading http://www.higherfrequencytrading.com + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.openhft.chronicle.map; + +enum NullValueProvider implements DefaultValueProvider { + INSTANCE; + + @Override + public Object get(Object key, Object usingValue) { + return null; + } +} diff --git a/src/main/java/net/openhft/chronicle/map/ReplicatedChronicleMap.java b/src/main/java/net/openhft/chronicle/map/ReplicatedChronicleMap.java index 5c7df7df2..3ea999fe8 100644 --- a/src/main/java/net/openhft/chronicle/map/ReplicatedChronicleMap.java +++ b/src/main/java/net/openhft/chronicle/map/ReplicatedChronicleMap.java @@ -1299,12 +1299,6 @@ class ReplicatedChronicleMap, } } - @Override - public V onGetMissing(ChronicleMap map, K key, - V usingValue) { - return nextListener.onGetMissing(map, key, usingValue); - } - @Override public void onGetFound(ChronicleMap map, Bytes entry, int metaDataBytes, K key, V value) { diff --git a/src/main/java/net/openhft/chronicle/map/SerializationBuilder.java b/src/main/java/net/openhft/chronicle/map/SerializationBuilder.java index ca6743b2f..6728d638f 100644 --- a/src/main/java/net/openhft/chronicle/map/SerializationBuilder.java +++ b/src/main/java/net/openhft/chronicle/map/SerializationBuilder.java @@ -22,17 +22,22 @@ import net.openhft.lang.io.Bytes; import net.openhft.lang.io.serialization.*; import net.openhft.lang.io.serialization.impl.*; import net.openhft.lang.model.Byteable; -import net.openhft.lang.model.DataValueClasses; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.io.Externalizable; +import java.io.IOException; import java.io.Serializable; +import java.lang.reflect.Modifier; import static net.openhft.chronicle.map.Objects.hash; import static net.openhft.chronicle.map.serialization.SizeMarshallers.stopBit; final class SerializationBuilder implements Cloneable { + + private static boolean concreteClass(Class c) { + return !c.isInterface() && !Modifier.isAbstract(c.getModifiers()); + } private static boolean marshallerUseFactory(Class c) { return Byteable.class.isAssignableFrom(c) || @@ -51,25 +56,22 @@ final class SerializationBuilder implements Cloneable { private CopyingInterop copyingInterop = null; private MetaBytesInterop metaInterop; private MetaProvider metaInteropProvider; - private ObjectFactory factory; + private ObjectFactory factory; @SuppressWarnings("unchecked") SerializationBuilder(Class eClass, Role role) { this.bufferIdentity = role; this.eClass = eClass; - Class classForMarshaller = marshallerUseFactory(eClass) && eClass.isInterface() ? - DataValueClasses.directClassFor(eClass) : eClass; - ObjectFactory factory = null; - if (role == Role.VALUE) { - factory = marshallerUseFactory(eClass) ? - new AllocateInstanceObjectFactory( - eClass.isInterface() ? classForMarshaller : eClass) : + ObjectFactory factory = this.factory; + if (factory == null && role == Role.VALUE) { + factory = concreteClass(eClass) && marshallerUseFactory(eClass) ? + new AllocateInstanceObjectFactory(eClass) : NullObjectFactory.INSTANCE; } - if (Byteable.class.isAssignableFrom(eClass)) { - agileMarshaller(ByteableMarshaller.of((Class) classForMarshaller), factory); + if (concreteClass(eClass) && Byteable.class.isAssignableFrom(eClass)) { + agileMarshaller(ByteableMarshaller.of((Class) eClass), factory); } else if (eClass == CharSequence.class || eClass == String.class) { reader((BytesReader) CharSequenceReader.of(), factory); writer((BytesWriter) CharSequenceWriter.instance()); @@ -78,8 +80,10 @@ final class SerializationBuilder implements Cloneable { } else if (eClass == byte[].class) { reader((BytesReader) ByteArrayMarshaller.INSTANCE, factory); interop((BytesInterop) ByteArrayMarshaller.INSTANCE); - } else { - marshaller(chooseMarshaller(eClass, classForMarshaller), factory); + } else if (concreteClass(eClass)) { + BytesMarshaller marshaller = chooseMarshaller(eClass, eClass); + if (marshaller != null) + marshaller(marshaller, factory); } } @@ -93,7 +97,7 @@ final class SerializationBuilder implements Cloneable { return (BytesMarshaller) IntegerMarshaller.INSTANCE; if (eClass == Double.class) return (BytesMarshaller) DoubleMarshaller.INSTANCE; - return SerializableMarshaller.INSTANCE; + return null; } private SerializationBuilder copyingInterop(CopyingInterop copyingInterop) { @@ -148,6 +152,16 @@ final class SerializationBuilder implements Cloneable { return this; } + public SerializationBuilder objectSerializer(ObjectSerializer serializer) { + if (reader == null || interop == null) { + ObjectFactory factory = this.factory; + if (factory == null) + factory = NullObjectFactory.INSTANCE; + marshaller(new SerializableMarshaller(serializer), factory); + } + return this; + } + public SizeMarshaller sizeMarshaller() { return sizeMarshaller; } @@ -297,24 +311,46 @@ final class SerializationBuilder implements Cloneable { } - private enum SerializableMarshaller implements BytesMarshaller { - INSTANCE; + private static class SerializableMarshaller implements BytesMarshaller { + private static final long serialVersionUID = 0L; + + private final ObjectSerializer serializer; + + private SerializableMarshaller(ObjectSerializer serializer) { + this.serializer = serializer; + } @Override public void write(Bytes bytes, Object obj) { - bytes.writeObject(obj); + try { + serializer.writeSerializable(bytes, obj, null); + } catch (IOException e) { + throw new IllegalStateException(e); + } } @Nullable @Override public Object read(Bytes bytes) { - return bytes.readObject(); + try { + return serializer.readSerializable(bytes, null, null); + } catch (IOException e) { + throw new IllegalStateException(e); + } catch (ClassNotFoundException e) { + throw new IllegalStateException(e); + } } @Nullable @Override public Object read(Bytes bytes, @Nullable Object obj) { - return bytes.readInstance(null, obj); + try { + return serializer.readSerializable(bytes, null, obj); + } catch (IOException e) { + throw new IllegalStateException(e); + } catch (ClassNotFoundException e) { + throw new IllegalStateException(e); + } } } diff --git a/src/main/java/net/openhft/chronicle/map/VanillaChronicleMap.java b/src/main/java/net/openhft/chronicle/map/VanillaChronicleMap.java index 5aaae93ca..ee0f24c31 100644 --- a/src/main/java/net/openhft/chronicle/map/VanillaChronicleMap.java +++ b/src/main/java/net/openhft/chronicle/map/VanillaChronicleMap.java @@ -72,6 +72,7 @@ class VanillaChronicleMap, final MVW originalMetaValueWriter; final MetaProvider metaValueWriterProvider; final ObjectFactory valueFactory; + final DefaultValueProvider defaultValueProvider; final int metaDataBytes; // private final int replicas; @@ -118,6 +119,7 @@ class VanillaChronicleMap, originalMetaValueWriter = (MVW) valueBuilder.metaInterop(); metaValueWriterProvider = (MetaProvider) valueBuilder.metaInteropProvider(); valueFactory = valueBuilder.factory(); + defaultValueProvider = builder.defaultValueProvider(); lockTimeOutNS = builder.lockTimeOut(TimeUnit.NANOSECONDS); @@ -163,6 +165,16 @@ class VanillaChronicleMap, valueReaderProvider = Provider.of((Class) originalValueReader.getClass()); valueWriterProvider = Provider.of((Class) originalValueWriter.getClass()); + if (defaultValueProvider instanceof ConstantValueProvider) { + ConstantValueProvider constantValueProvider = + (ConstantValueProvider) defaultValueProvider; + if (constantValueProvider.wasDeserialized()) { + ThreadLocalCopies copies = valueReaderProvider.getCopies(null); + BytesReader valueReader = valueReaderProvider.get(copies, originalValueReader); + constantValueProvider.initTransients(valueReader); + } + } + @SuppressWarnings("unchecked") Segment[] ss = (Segment[]) Array.newInstance(segmentType(), actualSegments); this.segments = ss; @@ -765,7 +777,7 @@ class VanillaChronicleMap, } else { if (usingValue instanceof Byteable) ((Byteable) usingValue).bytes(null, 0); - return notifyMissed(key, usingValue); + return defaultValueProvider.get(key, usingValue); } } @@ -1070,13 +1082,6 @@ class VanillaChronicleMap, } } - V notifyMissed(K key, V usingValue) { - if (eventListener() != MapEventListeners.NOP) { - return eventListener().onGetMissing(VanillaChronicleMap.this, key, usingValue); - } - return null; - } - void notifyRemoved(long offset, K key, V value, final int pos) { if (eventListener() != MapEventListeners.NOP) { tmpBytes.storePositionAndSize(bytes, offset, entrySize); diff --git a/src/test/java/net/openhft/chronicle/map/DefaultValueTest.java b/src/test/java/net/openhft/chronicle/map/DefaultValueTest.java new file mode 100644 index 000000000..53b9697ac --- /dev/null +++ b/src/test/java/net/openhft/chronicle/map/DefaultValueTest.java @@ -0,0 +1,60 @@ +/* + * Copyright 2014 Higher Frequency Trading http://www.higherfrequencytrading.com + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.openhft.chronicle.map; + +import net.openhft.lang.model.DataValueClasses; +import net.openhft.lang.values.IntValue; +import org.junit.Test; + +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import static org.testng.Assert.assertEquals; + +public class DefaultValueTest { + + @Test + public void test() throws IllegalAccessException, InstantiationException, IOException { + File file = Builder.getPersistenceFile(); + try { + + ArrayList defaultValue = new ArrayList(); + defaultValue.add(42); + try (ChronicleMap> map = ChronicleMapBuilder + .of(String.class, (Class>) ((Class) List.class)) + .defaultValue(defaultValue) + .create(file)) { + + assertEquals(defaultValue, map.get("a")); + assertEquals(1, map.size()); + + map.put("b", Arrays.asList(1, 2)); + assertEquals(Arrays.asList(1, 2), map.get("b")); + } + + try (ChronicleMap> map = ChronicleMapBuilder + .of(String.class, (Class>) ((Class) List.class)).create(file)) { + assertEquals(defaultValue, map.get("c")); + } + } finally { + file.delete(); + } + } +} diff --git a/src/test/java/net/openhft/chronicle/map/example/WordCount.java b/src/test/java/net/openhft/chronicle/map/example/WordCount.java new file mode 100644 index 000000000..f0af6d943 --- /dev/null +++ b/src/test/java/net/openhft/chronicle/map/example/WordCount.java @@ -0,0 +1,46 @@ +package net.openhft.chronicle.map.example; + +import net.openhft.chronicle.map.ChronicleMap; +import net.openhft.chronicle.map.ChronicleMapBuilder; +import net.openhft.lang.model.Byteable; +import net.openhft.lang.values.IntValue; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.Arrays; + +import static java.nio.charset.StandardCharsets.UTF_8; + +public class WordCount { + + static String[] words; + static int expectedSize; + static { + try { + // english version of war and peace -> ascii + String fullText = + new String(Files.readAllBytes(Paths.get("war_and_peace.txt")), UTF_8); + words = fullText.split("\\s+"); + expectedSize = (int) Arrays.stream(words).distinct().count(); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + static interface ByteableIntValue extends IntValue, Byteable {} + + public int chronicleMap() throws IOException { + ChronicleMap map = + ChronicleMapBuilder.of(String.class, ByteableIntValue.class) + .entrySize(16) + .actualSegments(1) + .entries(expectedSize) + .create(); + ByteableIntValue v = map.acquireUsing("a", null); + for (String word : words) { + map.acquireUsing(word, v).addValue(1); + } + return map.acquireUsing("a", v).getValue(); + } +} \ No newline at end of file