Skip to content

Commit 15bcff6

Browse files
committed
core: Opaque: Clarify mutable/immutable Opaques, add ByteBuffer support
Currently, Opaque is merely a wrapper around byte[], which adds hashCode/equals functionality, preventing mutations, so we can use it as a key in a HashMap. When we process a byte stream (from a ByteBuffer), we may want to look up values from a HashMap. However, we would still need to create new byte[]-backed Opaque objects for the lookup. This creates unnecessary allocations. Improve the Opaque interface to clarify that by itself, Opaques can be mutable. Provide a #toImmutableOpaque() method which is used when storing values in a Map. Adjust FileTracker to use immutable Opaques when storing in a Map. Provide a mutable Opaque implementation backed by a ByteBuffer, and improve equals() for OpaqueImpl. Add a small test case to ensure sanity. Related: #149 Signed-off-by: Christian Kohlschütter <[email protected]>
1 parent 3393960 commit 15bcff6

File tree

3 files changed

+178
-5
lines changed

3 files changed

+178
-5
lines changed

core/src/main/java/org/dcache/nfs/util/Opaque.java

Lines changed: 137 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,14 +22,19 @@
2222
import java.nio.ByteBuffer;
2323
import java.util.Arrays;
2424
import java.util.Base64;
25+
import java.util.Objects;
2526

2627
/**
27-
* Describes something that can be used as a key in {@link java.util.Map} and that can be converted to a {@code byte[]}
28-
* and a Base64 string representation.
28+
* Describes something that can be used as a key for {@link java.util.HashMap} and that can be converted to a
29+
* {@code byte[]} and a Base64 string representation.
30+
* <p>
31+
* Note that {@link Opaque}s that are <em>stored</em> in {@link java.util.HashMap} need to be immutable. Call
32+
* {@link #toImmutableOpaque()} when necessary (e.g., when using {@link java.util.HashMap#put(Object, Object)},
33+
* {@link java.util.HashMap#computeIfAbsent(Object, java.util.function.Function)}, etc.
2934
*/
3035
public interface Opaque {
3136
/**
32-
* Returns an {@link Opaque} instance based on a copy of the given bytes.
37+
* Returns an immutable {@link Opaque} instance based on a copy of the given bytes.
3338
*
3439
* @param bytes The bytes.
3540
* @return The {@link Opaque} instance.
@@ -39,7 +44,8 @@ static Opaque forBytes(byte[] bytes) {
3944
}
4045

4146
/**
42-
* Returns an {@link Opaque} instance based on a copy of the {@code length} bytes from the given {@link ByteBuffer}.
47+
* Returns an immutable {@link Opaque} instance based on a copy of the {@code length} bytes from the given
48+
* {@link ByteBuffer}.
4349
*
4450
* @param buf The buffer.
4551
* @param length The number of bytes.
@@ -52,6 +58,23 @@ static Opaque forBytes(ByteBuffer buf, int length) {
5258
return new OpaqueImpl(bytes);
5359
}
5460

61+
/**
62+
* Returns a <em>mutable</em> {@link Opaque} instance backed on the byte contents of the given {@link ByteBuffer},
63+
* for the given number of bytes starting from the given absolute index.
64+
* <p>
65+
* Note that the returned {@link Opaque} is typically not suitable for <em>storing</em> in a
66+
* {@link java.util.HashMap}, but merely for lookups. Call {@link #toImmutableOpaque()} when necessary.
67+
*
68+
* @param buf The buffer backing the {@link Opaque}.
69+
* @param index The absolute index to start from.
70+
* @param length The number of bytes.
71+
* @return The {@link Opaque} instance.
72+
* @see #toImmutableOpaque()
73+
*/
74+
static Opaque forMutableByteBuffer(ByteBuffer buf, int index, int length) {
75+
return new OpaqueBufferImpl(buf, index, length);
76+
}
77+
5578
/**
5679
* Default implementation for {@link #hashCode()}.
5780
*
@@ -102,6 +125,13 @@ static boolean defaultEquals(Opaque obj, Object other) {
102125
*/
103126
String toBase64();
104127

128+
/**
129+
* Returns an immutable {@link Opaque}, which may be the instance itself if it is already immutable.
130+
*
131+
* @return An immutable opaque.
132+
*/
133+
Opaque toImmutableOpaque();
134+
105135
/**
106136
* Writes the bytes of this {@link Opaque} to the given {@link ByteBuffer}.
107137
*
@@ -177,6 +207,19 @@ public boolean equals(Object o) {
177207

178208
if (o instanceof OpaqueImpl) {
179209
return Arrays.equals(_opaque, ((OpaqueImpl) o)._opaque);
210+
} else if (o instanceof OpaqueBufferImpl) {
211+
OpaqueBufferImpl other = (OpaqueBufferImpl) o;
212+
if (other.numBytes() != _opaque.length) {
213+
return false;
214+
}
215+
ByteBuffer otherBuf = other.buf;
216+
int otherIndex = other.index;
217+
for (int i = 0, n = _opaque.length, oi = otherIndex; i < n; i++, oi++) {
218+
if (_opaque[i] != otherBuf.get(oi)) {
219+
return false;
220+
}
221+
}
222+
return true;
180223
} else {
181224
return Arrays.equals(_opaque, ((Opaque) o).toBytes());
182225
}
@@ -196,5 +239,95 @@ public String toString() {
196239
public int numBytes() {
197240
return _opaque.length;
198241
}
242+
243+
@Override
244+
public Opaque toImmutableOpaque() {
245+
return this;
246+
}
247+
}
248+
249+
final class OpaqueBufferImpl implements Opaque {
250+
private final ByteBuffer buf;
251+
private final int index;
252+
private final int length;
253+
254+
private OpaqueBufferImpl(ByteBuffer buf, int index, int length) {
255+
this.buf = Objects.requireNonNull(buf);
256+
this.index = index;
257+
this.length = length;
258+
}
259+
260+
@Override
261+
public byte[] toBytes() {
262+
byte[] bytes = new byte[length];
263+
buf.get(index, bytes);
264+
return bytes;
265+
}
266+
267+
@Override
268+
public int numBytes() {
269+
return length;
270+
}
271+
272+
@Override
273+
public String toBase64() {
274+
return Base64.getEncoder().withoutPadding().encodeToString(toBytes());
275+
}
276+
277+
@Override
278+
public Opaque toImmutableOpaque() {
279+
return Opaque.forBytes(toBytes());
280+
}
281+
282+
@Override
283+
public int hashCode() {
284+
int result = 1;
285+
for (int i = index, n = index + length; i < n; i++) {
286+
byte element = buf.get(i);
287+
result = 31 * result + element;
288+
}
289+
290+
return result;
291+
}
292+
293+
@Override
294+
public boolean equals(Object o) {
295+
if (o == this) {
296+
return true;
297+
}
298+
if (!(o instanceof Opaque)) {
299+
return false;
300+
}
301+
if (length != ((Opaque) o).numBytes()) {
302+
return false;
303+
}
304+
305+
if (o instanceof OpaqueImpl) {
306+
byte[] otherBytes = ((OpaqueImpl) o)._opaque;
307+
for (int i = index, n = index + length, oi = 0; i < n; i++, oi++) {
308+
if (buf.get(i) != otherBytes[oi]) {
309+
return false;
310+
}
311+
}
312+
return true;
313+
} else if (o instanceof OpaqueBufferImpl) {
314+
OpaqueBufferImpl other = (OpaqueBufferImpl) o;
315+
ByteBuffer otherBuf = other.buf;
316+
int otherIndex = other.index;
317+
for (int i = index, n = index + length, oi = otherIndex; i < n; i++, oi++) {
318+
if (buf.get(i) != otherBuf.get(oi)) {
319+
return false;
320+
}
321+
}
322+
return true;
323+
} else {
324+
return toImmutableOpaque().equals(o);
325+
}
326+
}
327+
328+
@Override
329+
public String toString() {
330+
return super.toString() + "[" + toBase64() + "]";
331+
}
199332
}
200333
}

core/src/main/java/org/dcache/nfs/v4/FileTracker.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -242,7 +242,7 @@ public OpenRecord addOpen(NFS4Client client, StateOwner owner, Inode inode, int
242242
// client explicitly requested write delegation
243243
boolean wantWriteDelegation = (shareAccess & nfs4_prot.OPEN4_SHARE_ACCESS_WANT_WRITE_DELEG) != 0;
244244

245-
Opaque fileId = inode.getFileIdKey();
245+
final Opaque fileId = inode.getFileIdKey().toImmutableOpaque();
246246
Lock lock = filesLock.get(fileId);
247247
lock.lock();
248248
try {
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
package org.dcache.nfs.util;
2+
3+
import static org.junit.Assert.assertEquals;
4+
import static org.junit.Assert.assertNotEquals;
5+
6+
import java.nio.ByteBuffer;
7+
8+
import org.junit.Test;
9+
10+
public class OpaqueTest {
11+
@Test
12+
public void testMutable() {
13+
ByteBuffer buf = ByteBuffer.allocate(64);
14+
buf.putInt(3, 0xAABBCCDD);
15+
buf.putInt(7, 0xEEFF0011);
16+
17+
Opaque bufOpaque = Opaque.forMutableByteBuffer(buf, 3, 4);
18+
Opaque bytesOpaque = Opaque.forBytes(new byte[] {(byte) 0xAA, (byte) 0xBB, (byte) 0xCC, (byte) 0xDD});
19+
20+
assertEquals(bufOpaque.toBase64(), bytesOpaque.toBase64());
21+
assertEquals(bufOpaque, bytesOpaque);
22+
assertEquals(bytesOpaque, bufOpaque);
23+
assertEquals(bytesOpaque.hashCode(), bufOpaque.hashCode());
24+
25+
// unrelated changes should not affect equality
26+
buf.put(2, (byte) 0x7f);
27+
buf.putInt(7, 0);
28+
assertEquals(bufOpaque.toBase64(), bytesOpaque.toBase64());
29+
assertEquals(bufOpaque, bytesOpaque);
30+
assertEquals(bytesOpaque, bufOpaque);
31+
assertEquals(bytesOpaque.hashCode(), bufOpaque.hashCode());
32+
33+
// change contents of mutable buffer
34+
buf.put(6, (byte) 0x12);
35+
assertNotEquals(bufOpaque.toBase64(), bytesOpaque.toBase64());
36+
assertNotEquals(bufOpaque, bytesOpaque);
37+
assertNotEquals(bytesOpaque, bufOpaque);
38+
assertNotEquals(bytesOpaque.hashCode(), bufOpaque.hashCode());
39+
}
40+
}

0 commit comments

Comments
 (0)