diff --git a/.gitignore b/.gitignore index ac0e92fe..ba870438 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,7 @@ *.iml target dependency-reduced-pom.xml +.project +.settings +.classpath +.factorypath diff --git a/oncrpc4j-core/src/main/java/org/dcache/oncrpc4j/util/Opaque.java b/oncrpc4j-core/src/main/java/org/dcache/oncrpc4j/util/Opaque.java new file mode 100644 index 00000000..13f66609 --- /dev/null +++ b/oncrpc4j-core/src/main/java/org/dcache/oncrpc4j/util/Opaque.java @@ -0,0 +1,372 @@ +/* + * Copyright (c) 2009 - 2020 Deutsches Elektronen-Synchroton, + * Member of the Helmholtz Association, (DESY), HAMBURG, GERMANY + * + * This library is free software; you can redistribute it and/or modify + * it under the terms of the GNU Library General Public License as + * published by the Free Software Foundation; either version 2 of the + * License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public + * License along with this program (see the file COPYING.LIB for more + * details); if not, write to the Free Software Foundation, Inc., + * 675 Mass Ave, Cambridge, MA 02139, USA. + */ +package org.dcache.oncrpc4j.util; + +import java.nio.ByteBuffer; +import java.util.Arrays; +import java.util.Base64; +import java.util.Objects; + +/** + * Describes something that can be used as a key for {@link java.util.HashMap} and that can be converted to a + * {@code byte[]} and a Base64 string representation. + *

+ * Note that {@link Opaque}s that are stored in {@link java.util.HashMap} need to be immutable. Call + * {@link #toImmutableOpaque()} when necessary (e.g., when using {@link java.util.HashMap#put(Object, Object)}, + * {@link java.util.HashMap#computeIfAbsent(Object, java.util.function.Function)}, etc. + */ +public interface Opaque { + /** + * Returns an immutable {@link Opaque} instance based on a copy of the given bytes. + * + * @param bytes The bytes. + * @return The {@link Opaque} instance. + */ + static Opaque forBytes(byte[] bytes) { + return new OpaqueImmutableImpl(bytes.clone()); + } + + /** + * Returns an mutable {@link Opaque} instance based on the given byte array. + *

+ * Note that the returned {@link Opaque} is typically not suitable for storing in a + * {@link java.util.HashMap}, but merely for lookups. Call {@link #toImmutableOpaque()} when necessary. + * + * @param bytes The bytes. + * @return The {@link Opaque} instance. + */ + static Opaque forMutableByteArray(byte[] bytes) { + return new OpaqueImpl(bytes); + } + + /** + * Returns an immutable {@link Opaque} instance based on a copy of the {@code length} bytes from the given + * {@link ByteBuffer}. + * + * @param buf The buffer. + * @param length The number of bytes. + * @return The {@link Opaque} instance. + */ + static Opaque forBytes(ByteBuffer buf, int length) { + byte[] bytes = new byte[length]; + buf.get(bytes); + + return new OpaqueImmutableImpl(bytes); + } + + /** + * Returns a mutable {@link Opaque} instance backed on the byte contents of the given {@link ByteBuffer}, + * for the given number of bytes starting from the given absolute index. + *

+ * Note that the returned {@link Opaque} is typically not suitable for storing in a + * {@link java.util.HashMap}, but merely for lookups. Call {@link #toImmutableOpaque()} when necessary. + * + * @param buf The buffer backing the {@link Opaque}. + * @param index The absolute index to start from. + * @param length The number of bytes. + * @return The {@link Opaque} instance. + * @see #toImmutableOpaque() + */ + static Opaque forMutableByteBuffer(ByteBuffer buf, int index, int length) { + return new OpaqueBufferImpl(buf, index, length); + } + + /** + * Default implementation for {@link #hashCode()}. + * + * @param obj The instance object. + * @return The hash code. + * @see #hashCode() + */ + static int defaultHashCode(Opaque obj) { + return Arrays.hashCode(obj.toBytes()); + } + + /** + * Default implementation for {@link #equals(Object)}. + * + * @param obj The instance object. + * @param other The other object. + * @return {@code true} if equal. + * @see #equals(Object) + */ + static boolean defaultEquals(Opaque obj, Object other) { + if (other == obj) { + return true; + } + if (!(other instanceof Opaque)) { + return false; + } + return Arrays.equals(obj.toBytes(), ((Opaque) other).toBytes()); + } + + /** + * Returns a byte-representation of this opaque object. + * + * @return A new array. + */ + byte[] toBytes(); + + /** + * Returns the number of bytes in this opaque object; + * + * @return The number of bytes; + */ + int numBytes(); + + /** + * Returns a Base64 string representing this opaque object. + * + * @return A Base64 string. + */ + String toBase64(); + + /** + * Returns an immutable {@link Opaque}, which may be the instance itself if it is already immutable. + * + * @return An immutable opaque. + */ + Opaque toImmutableOpaque(); + + /** + * Writes the bytes of this {@link Opaque} to the given {@link ByteBuffer}. + * + * @param buf The target buffer. + */ + default void putBytes(ByteBuffer buf) { + buf.put(toBytes()); + } + + /** + * Returns the hashCode based on the byte-representation of this instance. + *

+ * This method must behave like {@link #defaultHashCode(Opaque)}, but may be optimized. + * + * @return The hashCode. + */ + @Override + int hashCode(); + + /** + * Compares this object to another one. + *

+ * This method must behave like {@link #defaultEquals(Opaque, Object)}, but may be optimized. + * + * @return {@code true} if both objects are equal. + */ + @Override + boolean equals(Object o); + + class OpaqueImpl implements Opaque { + final byte[] _opaque; + + OpaqueImpl(byte[] opaque) { + _opaque = opaque; + } + + @Override + public byte[] toBytes() { + return _opaque.clone(); + } + + @Override + public int hashCode() { + return Arrays.hashCode(_opaque); + } + + @Override + public String toBase64() { + return toBase64Impl(); + } + + protected String toBase64Impl() { + return Base64.getEncoder().withoutPadding().encodeToString(_opaque); + } + + @Override + public void putBytes(ByteBuffer buf) { + buf.put(_opaque); + } + + @Override + public boolean equals(Object o) { + if (o == this) { + return true; + } + if (!(o instanceof Opaque)) { + return false; + } + + if (o instanceof OpaqueImpl) { + return Arrays.equals(_opaque, ((OpaqueImpl) o)._opaque); + } else if (o instanceof OpaqueBufferImpl) { + OpaqueBufferImpl other = (OpaqueBufferImpl) o; + if (other.numBytes() != _opaque.length) { + return false; + } + ByteBuffer otherBuf = other.buf; + int otherIndex = other.index; + for (int i = 0, n = _opaque.length, oi = otherIndex; i < n; i++, oi++) { + if (_opaque[i] != otherBuf.get(oi)) { + return false; + } + } + return true; + } else { + return Arrays.equals(_opaque, ((Opaque) o).toBytes()); + } + } + + /** + * Returns a (potentially non-stable) debug string. + * + * @see #toBase64() + */ + @Override + public String toString() { + return super.toString() + "[" + toBase64() + "]"; + } + + @Override + public int numBytes() { + return _opaque.length; + } + + @Override + public Opaque toImmutableOpaque() { + return Opaque.forBytes(_opaque); + } + } + + final class OpaqueImmutableImpl extends OpaqueImpl { + private String base64 = null; + private int hashCode; + + protected OpaqueImmutableImpl(byte[] opaque) { + super(opaque); + } + + @Override + public int hashCode() { + if (hashCode == 0) { + hashCode = Arrays.hashCode(_opaque); + } + return hashCode; + } + + @Override + public String toBase64() { + if (base64 == null) { + base64 = toBase64Impl(); + } + return base64; + } + + @Override + public Opaque toImmutableOpaque() { + return this; + } + } + + final class OpaqueBufferImpl implements Opaque { + private final ByteBuffer buf; + private final int index; + private final int length; + + private OpaqueBufferImpl(ByteBuffer buf, int index, int length) { + this.buf = Objects.requireNonNull(buf); + this.index = index; + this.length = length; + } + + @Override + public byte[] toBytes() { + byte[] bytes = new byte[length]; + buf.get(index, bytes); + return bytes; + } + + @Override + public int numBytes() { + return length; + } + + @Override + public String toBase64() { + return Base64.getEncoder().withoutPadding().encodeToString(toBytes()); + } + + @Override + public Opaque toImmutableOpaque() { + return Opaque.forBytes(toBytes()); + } + + @Override + public int hashCode() { + int result = 1; + for (int i = index, n = index + length; i < n; i++) { + byte element = buf.get(i); + result = 31 * result + element; + } + + return result; + } + + @Override + public boolean equals(Object o) { + if (o == this) { + return true; + } + if (!(o instanceof Opaque)) { + return false; + } + if (length != ((Opaque) o).numBytes()) { + return false; + } + + if (o instanceof OpaqueImpl) { + byte[] otherBytes = ((OpaqueImpl) o)._opaque; + for (int i = index, n = index + length, oi = 0; i < n; i++, oi++) { + if (buf.get(i) != otherBytes[oi]) { + return false; + } + } + return true; + } else if (o instanceof OpaqueBufferImpl) { + OpaqueBufferImpl other = (OpaqueBufferImpl) o; + ByteBuffer otherBuf = other.buf; + int otherIndex = other.index; + for (int i = index, n = index + length, oi = otherIndex; i < n; i++, oi++) { + if (buf.get(i) != otherBuf.get(oi)) { + return false; + } + } + return true; + } else { + return toImmutableOpaque().equals(o); + } + } + + @Override + public String toString() { + return super.toString() + "[" + toBase64() + "]"; + } + } +} diff --git a/pom.xml b/pom.xml index f81261d5..2e3f04b3 100644 --- a/pom.xml +++ b/pom.xml @@ -47,7 +47,7 @@ UTF-8 - 11 + 17