/** * Copyright (C) 2014-2017 Xavier Witdouck * * 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 com.zavtech.morpheus.array.mapped; import java.io.File; import java.io.IOException; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.io.RandomAccessFile; import java.nio.ByteBuffer; import java.nio.channels.FileChannel; import java.time.Instant; import java.time.ZoneId; import java.time.ZonedDateTime; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.function.Predicate; import com.zavtech.morpheus.array.ArrayBuilder; import com.zavtech.morpheus.array.ArrayCursor; import com.zavtech.morpheus.array.ArrayException; import com.zavtech.morpheus.array.Array; import com.zavtech.morpheus.array.ArrayBase; import com.zavtech.morpheus.array.ArrayStyle; import com.zavtech.morpheus.array.ArrayValue; /** * An Array implementation containing mapped ZonedDateTime values stored as a longs of Epoch Millis. * * <p><strong>This is open source software released under the <a href="http://www.apache.org/licenses/LICENSE-2.0">Apache 2.0 License</a></strong></p> * * @author Xavier Witdouck */ class MappedArrayOfZonedDateTimes extends ArrayBase<ZonedDateTime> { private static final long serialVersionUID = 1L; private static final int BYTE_COUNT = 10; private static final Map<ZoneId,Short> zoneIdMap1 = new HashMap<>(); private static final Map<Short,ZoneId> zoneIdMap2 = new HashMap<>(); /** * Static initializer */ static { short counter = 0; final List<String> keys = new ArrayList<>(); keys.add("Z"); keys.addAll(ZoneId.getAvailableZoneIds()); for (String key: keys) { final short index = ++counter; final ZoneId zoneId = ZoneId.of(key); zoneIdMap1.put(zoneId, index); zoneIdMap2.put(index, zoneId); } } private static final long nullValue = Long.MIN_VALUE; private static final short NULL_ZONE = -1; private static final short UTC_ZONE = zoneIdMap1.get(ZoneId.of("UTC")); private File file; private int length; private FileChannel channel; private ByteBuffer byteBuffer; private long defaultValueAsLong; private short defaultZoneId; private ZonedDateTime defaultValue; /** * Constructor * @param length the length for this array * @param defaultValue the default value for array * @param file the memory mapped file reference */ MappedArrayOfZonedDateTimes(int length, ZonedDateTime defaultValue, File file) { super(ZonedDateTime.class, ArrayStyle.MAPPED, false); try { this.file = file; this.length = length; this.defaultValue = defaultValue; this.defaultValueAsLong = defaultValue != null ? defaultValue.toInstant().toEpochMilli() : nullValue; this.defaultZoneId = defaultValue != null ? zoneIdMap1.get(defaultValue.getZone()) : NULL_ZONE; this.channel = new RandomAccessFile(file, "rw").getChannel(); this.byteBuffer = channel.map(FileChannel.MapMode.READ_WRITE, 0, BYTE_COUNT * length); this.fill(defaultValue); } catch (Exception ex) { throw new ArrayException("Failed to initialise memory mapped array on file: " + file.getAbsolutePath(), ex); } } /** * Constructor * @param source the source array to shallow copy * @param parallel true for the parallel version */ private MappedArrayOfZonedDateTimes(MappedArrayOfZonedDateTimes source, boolean parallel) { super(source.type(), ArrayStyle.MAPPED, parallel); this.file = source.file; this.length = source.length; this.defaultValue = source.defaultValue; this.defaultValueAsLong = source.defaultValueAsLong; this.defaultZoneId = source.defaultZoneId; this.channel = source.channel; this.byteBuffer = source.byteBuffer; } @Override public final int length() { return length; } @Override public float loadFactor() { return 1F; } @Override public final ZonedDateTime defaultValue() { return defaultValue; } @Override public final Array<ZonedDateTime> parallel() { return isParallel() ? this : new MappedArrayOfZonedDateTimes(this, true); } @Override public final Array<ZonedDateTime> sequential() { return isParallel() ? new MappedArrayOfZonedDateTimes(this, false) : this; } @Override() public final Array<ZonedDateTime> copy() { try { final File newFile = MappedArrayConstructor.randomFile(true); final MappedArrayOfZonedDateTimes copy = new MappedArrayOfZonedDateTimes(length, defaultValue, newFile); for (int i=0; i<length; ++i) { final int longIndex = i * BYTE_COUNT; final int shortIndex = i * BYTE_COUNT + 8; final long epochMillis = byteBuffer.getLong(longIndex); final short zoneId = byteBuffer.getShort(shortIndex); copy.byteBuffer.putLong(longIndex, epochMillis); copy.byteBuffer.putShort(shortIndex, zoneId); } return copy; } catch (Exception ex) { throw new ArrayException("Failed to copy Array: " + this, ex); } } @Override() public final Array<ZonedDateTime> copy(int[] indexes) { try { final File newFile = MappedArrayConstructor.randomFile(true); final MappedArrayOfZonedDateTimes copy = new MappedArrayOfZonedDateTimes(indexes.length, defaultValue, newFile); for (int i=0; i<indexes.length; ++i) { final int toIndex = i * BYTE_COUNT; final int fromIndex = indexes[i] * BYTE_COUNT; final long epochMillis = byteBuffer.getLong(fromIndex); final short zoneId = byteBuffer.getShort(fromIndex + 8); copy.byteBuffer.putLong(toIndex, epochMillis); copy.byteBuffer.putShort(toIndex + 8, zoneId); } return copy; } catch (Exception ex) { throw new ArrayException("Failed to copy Array: " + this, ex); } } @Override() public final Array<ZonedDateTime> copy(int start, int end) { try { final int newLength = end - start; final File newFile = MappedArrayConstructor.randomFile(true); final MappedArrayOfZonedDateTimes copy = new MappedArrayOfZonedDateTimes(newLength, defaultValue, newFile); for (int i=0; i<newLength; ++i) { final int toIndex = i * BYTE_COUNT; final int fromIndex = (start + i) * BYTE_COUNT; final long epochMillis = byteBuffer.getLong(fromIndex); final short zoneId = byteBuffer.getShort(fromIndex + 8); copy.byteBuffer.putLong(toIndex, epochMillis); copy.byteBuffer.putShort(toIndex + 8, zoneId); } return copy; } catch (Exception ex) { throw new ArrayException("Failed top copy subset of Array", ex); } } @Override protected final Array<ZonedDateTime> sort(int start, int end, int multiplier) { return doSort(start, end, (i, j) -> { final long v1 = byteBuffer.getLong(i * BYTE_COUNT); final long v2 = byteBuffer.getLong(j * BYTE_COUNT); return multiplier * Long.compare(v1, v2); }); } @Override public final int compare(int i, int j) { final long v1 = byteBuffer.getLong(i * BYTE_COUNT); final long v2 = byteBuffer.getLong(j * BYTE_COUNT); return Long.compare(v1, v2); } @Override public final Array<ZonedDateTime> swap(int i, int j) { final int x = i * BYTE_COUNT; final int y = j * BYTE_COUNT; final long v1 = byteBuffer.getLong(x); final long v2 = byteBuffer.getLong(y); final short z1 = byteBuffer.getShort(x + 8); final short z2 = byteBuffer.getShort(y + 8); this.byteBuffer.putLong(x, v2); this.byteBuffer.putLong(y, v1); this.byteBuffer.putShort(x + 8, z2); this.byteBuffer.putShort(y + 8, z1); return this; } @Override public final Array<ZonedDateTime> filter(Predicate<ArrayValue<ZonedDateTime>> predicate) { final ArrayCursor<ZonedDateTime> cursor = cursor(); final ArrayBuilder<ZonedDateTime> builder = ArrayBuilder.of(length(), type()); for (int i=0; i<length; ++i) { cursor.moveTo(i); final boolean match = predicate.test(cursor); if (match) { builder.add(cursor.getValue()); } } return builder.toArray(); } @Override public final Array<ZonedDateTime> update(Array<ZonedDateTime> from, int[] fromIndexes, int[] toIndexes) { if (fromIndexes.length != toIndexes.length) { throw new ArrayException("The from index array must have the same length as the to index array"); } else { if (from instanceof MappedArrayOfZonedDateTimes) { final MappedArrayOfZonedDateTimes other = (MappedArrayOfZonedDateTimes)from; for (int i=0; i<fromIndexes.length; ++i) { final int toIndex = toIndexes[i] * BYTE_COUNT; final int fromIndex = fromIndexes[i] * BYTE_COUNT; this.byteBuffer.putLong(toIndex, other.byteBuffer.getLong(fromIndex)); this.byteBuffer.putShort(toIndex + 8, byteBuffer.getShort(fromIndex + 8)); } } else { for (int i=0; i<fromIndexes.length; ++i) { final int toIndex = toIndexes[i]; final int fromIndex = fromIndexes[i]; final ZonedDateTime update = from.getValue(fromIndex); this.setValue(toIndex, update); } } } return this; } @Override public final Array<ZonedDateTime> update(int toIndex, Array<ZonedDateTime> from, int fromIndex, int length) { if (from instanceof MappedArrayOfZonedDateTimes) { final MappedArrayOfZonedDateTimes other = (MappedArrayOfZonedDateTimes)from; for (int i=0; i<length; ++i) { final int x = toIndex * BYTE_COUNT; final int y = fromIndex * BYTE_COUNT; this.byteBuffer.putLong(x, other.byteBuffer.getLong(y)); this.byteBuffer.putShort(x + 8, byteBuffer.getShort(y + 8)); } } else { for (int i=0; i<length; ++i) { final ZonedDateTime update = from.getValue(fromIndex + i); this.setValue(toIndex + i, update); } } return this; } @Override public final Array<ZonedDateTime> expand(int newLength) { try { if (newLength > length) { this.byteBuffer = channel.map(FileChannel.MapMode.READ_WRITE, 0, BYTE_COUNT * newLength); this.fill(defaultValue, length, newLength); this.length = newLength; } return this; } catch (Exception ex) { throw new ArrayException("Failed to expand size of memory mapped array at " + file.getAbsolutePath(), ex); } } @Override public Array<ZonedDateTime> fill(ZonedDateTime value, int start, int end) { final long fillEpochMillis = value == null ? nullValue : value.toInstant().toEpochMilli(); final short fillZoneId = value == null ? NULL_ZONE : zoneIdMap1.get(value.getZone()); for (int i=start; i<end; ++i) { final int index = i * BYTE_COUNT; this.byteBuffer.putLong(index, fillEpochMillis); this.byteBuffer.putShort(index + 8, fillZoneId); } return this; } @Override public boolean isNull(int index) { return byteBuffer.getLong(index * BYTE_COUNT) == nullValue; } @Override public final boolean isEqualTo(int index, ZonedDateTime value) { final long epochMillis = byteBuffer.getLong(index * BYTE_COUNT); if (value == null) { return epochMillis == nullValue; } else { final long valueAsEpochMills = value.toInstant().toEpochMilli(); if (epochMillis == valueAsEpochMills) { return false; } else { final ZoneId zoneId = value.getZone(); final short code1 = zoneIdMap1.get(zoneId); final short code2 = byteBuffer.getShort(index * BYTE_COUNT + 8); return code1 == code2; } } } @Override public final long getLong(int index) { this.checkBounds(index, length); return byteBuffer.getLong(index * BYTE_COUNT); } @Override @SuppressWarnings("unchecked") public final ZonedDateTime getValue(int index) { this.checkBounds(index, length); final int byteIndex = index * BYTE_COUNT; final long value = byteBuffer.getLong(byteIndex); if (value == nullValue) { return null; } else { final short zoneId = byteBuffer.getShort(byteIndex + 8); final ZoneId zone = zoneIdMap2.get(zoneId); final Instant instant = Instant.ofEpochMilli(value); return ZonedDateTime.ofInstant(instant, zone); } } @Override public final long setLong(int index, long value) { this.checkBounds(index, length); final int byteIndex = index * BYTE_COUNT; final long oldMillis = byteBuffer.getLong(byteIndex); final short oldZone = byteBuffer.getShort(byteIndex + 8); this.byteBuffer.putLong(byteIndex, value); if (oldZone < 0) { this.byteBuffer.putShort(byteIndex + 8, UTC_ZONE); } return oldMillis; } @Override public final ZonedDateTime setValue(int index, ZonedDateTime value) { this.checkBounds(index, length); final int byteIndex = index * BYTE_COUNT; final ZonedDateTime oldValue = getValue(index); if (value == null) { this.byteBuffer.putLong(byteIndex, nullValue); this.byteBuffer.putShort(byteIndex + 8, NULL_ZONE); return oldValue; } else { this.byteBuffer.putLong(byteIndex, value.toInstant().toEpochMilli()); this.byteBuffer.putShort(byteIndex + 8, zoneIdMap1.get(value.getZone())); return oldValue; } } @Override public int binarySearch(int start, int end, ZonedDateTime value) { try { int low = start; int high = end - 1; final long epochMillis = value != null ? value.toInstant().toEpochMilli() : Long.MIN_VALUE; while (low <= high) { final int midIndex = (low + high) >>> 1; final long midValue = byteBuffer.getLong(midIndex * BYTE_COUNT); final int result = Long.compare(midValue, epochMillis); if (result < 0) { low = midIndex + 1; } else if (result > 0) { high = midIndex - 1; } else { return midIndex; } } return -(low + 1); } catch (Exception ex) { throw new ArrayException("Binary search of array failed", ex); } } @Override public final void read(ObjectInputStream is, int count) throws IOException { for (int i=0; i<count; ++i) { final int byteIndex = i * BYTE_COUNT; final long epochMillis = is.readLong(); final short zoneId = is.readShort(); this.byteBuffer.putLong(byteIndex, epochMillis); this.byteBuffer.putShort(byteIndex + 8, zoneId); } } @Override public final void write(ObjectOutputStream os, int[] indexes) throws IOException { for (int index : indexes) { final int byteIndex = index * BYTE_COUNT; final long epochMillis = byteBuffer.getLong(byteIndex); final short zoneId = byteBuffer.getShort(byteIndex + 8); os.writeLong(epochMillis); os.writeShort(zoneId); } } /** Custom serialization */ private void writeObject(ObjectOutputStream os) throws IOException { os.writeInt(length); os.writeLong(defaultValueAsLong); os.writeShort(defaultZoneId); os.writeObject(defaultValue); for (int i=0; i<length; ++i) { final int byteIndex = i * BYTE_COUNT; final long epochMillis = byteBuffer.getLong(byteIndex); final short zoneId = byteBuffer.getShort(byteIndex + 8); os.writeLong(epochMillis); os.writeShort(zoneId); } } @SuppressWarnings("unchecked") /** Custom serialization */ private void readObject(ObjectInputStream is) throws IOException, ClassNotFoundException { this.file = MappedArrayConstructor.randomFile(true); this.length = is.readInt(); this.defaultValueAsLong = is.readLong(); this.defaultZoneId = is.readShort(); this.defaultValue = (ZonedDateTime)is.readObject(); this.channel = new RandomAccessFile(file, "rw").getChannel(); this.byteBuffer = channel.map(FileChannel.MapMode.READ_WRITE, 0, BYTE_COUNT * length); for (int i=0; i<length; ++i) { final int byteIndex = i * BYTE_COUNT; final long epochMillis = is.readLong(); final short zoneId = is.readShort(); this.byteBuffer.putLong(byteIndex, epochMillis); this.byteBuffer.putShort(byteIndex + 8, zoneId); } } }