Skip to content

Commit

Permalink
Merge pull request #5372 from bjhargrave/issues/5367
Browse files Browse the repository at this point in the history
repository: Add a File Resource cache to ResourceBuilder
  • Loading branch information
bjhargrave committed Oct 10, 2022
2 parents f383580 + 7b42679 commit 31f140a
Show file tree
Hide file tree
Showing 7 changed files with 303 additions and 31 deletions.
@@ -0,0 +1,127 @@
package aQute.bnd.osgi.resource;

import static org.assertj.core.api.Assertions.assertThatObject;

import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.attribute.BasicFileAttributeView;
import java.nio.file.attribute.BasicFileAttributes;
import java.nio.file.attribute.FileTime;
import java.time.Instant;

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.condition.DisabledOnOs;
import org.junit.jupiter.api.condition.OS;

import aQute.bnd.osgi.resource.FileResourceCache.CacheKey;
import aQute.bnd.test.jupiter.InjectTemporaryDirectory;
import aQute.lib.io.IO;

class FileResourceCacheKeyTest {

@Test
void unchanged(@InjectTemporaryDirectory
Path tmp) throws Exception {
Path subject = tmp.resolve("test");
IO.store("line1", subject, StandardCharsets.UTF_8);
CacheKey key1 = new CacheKey(subject);
CacheKey key2 = new CacheKey(subject);
assertThatObject(key1).isEqualTo(key2);
assertThatObject(key1).hasSameHashCodeAs(key2);
}

@Test
void change_modified(@InjectTemporaryDirectory
Path tmp) throws Exception {
Path subject = tmp.resolve("test");
IO.store("line1", subject, StandardCharsets.UTF_8);
CacheKey key1 = new CacheKey(subject);
BasicFileAttributes attributes = Files.getFileAttributeView(subject, BasicFileAttributeView.class)
.readAttributes();
FileTime lastModifiedTime = attributes.lastModifiedTime();
Instant plusSeconds = lastModifiedTime.toInstant()
.plusSeconds(10L);
Files.setLastModifiedTime(subject, FileTime.from(plusSeconds));
CacheKey key2 = new CacheKey(subject);
assertThatObject(key1).isNotEqualTo(key2);
assertThatObject(key1).doesNotHaveSameHashCodeAs(key2);
}

@Test
void change_size(@InjectTemporaryDirectory
Path tmp) throws Exception {
Path subject = tmp.resolve("test");
IO.store("line1", subject, StandardCharsets.UTF_8);
CacheKey key1 = new CacheKey(subject);
BasicFileAttributes attributes = Files.getFileAttributeView(subject, BasicFileAttributeView.class)
.readAttributes();
FileTime lastModifiedTime = attributes.lastModifiedTime();
IO.store("line100", subject, StandardCharsets.UTF_8);
Files.setLastModifiedTime(subject, lastModifiedTime);
CacheKey key2 = new CacheKey(subject);
assertThatObject(key1).isNotEqualTo(key2);
assertThatObject(key1).doesNotHaveSameHashCodeAs(key2);
}

@DisabledOnOs(value = OS.WINDOWS, disabledReason = "Windows FS does not support fileKey")
@Test
void change_filekey(@InjectTemporaryDirectory
Path tmp) throws Exception {
Path subject = tmp.resolve("test");
IO.store("line1", subject, StandardCharsets.UTF_8);
CacheKey key1 = new CacheKey(subject);
BasicFileAttributes attributes = Files.getFileAttributeView(subject, BasicFileAttributeView.class)
.readAttributes();
assertThatObject(attributes.fileKey()).isNotNull();
FileTime lastModifiedTime = attributes.lastModifiedTime();
Path subject2 = tmp.resolve("test.tmp");
IO.store("line2", subject2, StandardCharsets.UTF_8);
Files.setLastModifiedTime(subject2, lastModifiedTime);
IO.rename(subject2, subject);
CacheKey key2 = new CacheKey(subject);
attributes = Files.getFileAttributeView(subject, BasicFileAttributeView.class)
.readAttributes();
assertThatObject(attributes.fileKey()).isNotNull();
assertThatObject(key1).as("key2 not equal")
.isNotEqualTo(key2);
assertThatObject(key1).as("key2 different hash")
.doesNotHaveSameHashCodeAs(key2);
}

@Test
void change_file_modified(@InjectTemporaryDirectory
Path tmp) throws Exception {
Path subject = tmp.resolve("test");
IO.store("line1", subject, StandardCharsets.UTF_8);
CacheKey key1 = new CacheKey(subject);
Path subject2 = tmp.resolve("test.tmp");
IO.store("line2", subject2, StandardCharsets.UTF_8);
BasicFileAttributes attributes = Files.getFileAttributeView(subject2, BasicFileAttributeView.class)
.readAttributes();
FileTime lastModifiedTime = attributes.lastModifiedTime();
Instant plusSeconds = lastModifiedTime.toInstant()
.plusSeconds(10L);
Files.setLastModifiedTime(subject2, FileTime.from(plusSeconds));
IO.rename(subject2, subject);
CacheKey key2 = new CacheKey(subject);
assertThatObject(key1).as("key2 not equal")
.isNotEqualTo(key2);
assertThatObject(key1).as("key2 different hash")
.doesNotHaveSameHashCodeAs(key2);
}

@Test
void different_files(@InjectTemporaryDirectory
Path tmp) throws Exception {
Path subject1 = tmp.resolve("test1");
IO.store("line1", subject1, StandardCharsets.UTF_8);
CacheKey key1 = new CacheKey(subject1);
Path subject2 = tmp.resolve("test2");
IO.copy(subject1, subject2);
CacheKey key2 = new CacheKey(subject2);
assertThatObject(key1).isNotEqualTo(key2);
assertThatObject(key1).doesNotHaveSameHashCodeAs(key2);
}

}
11 changes: 4 additions & 7 deletions biz.aQute.bndlib/src/aQute/bnd/osgi/resource/DeferredValue.java
Expand Up @@ -4,25 +4,22 @@

import java.util.function.Supplier;

import aQute.bnd.memoize.Memoize;

class DeferredValue<T> implements Supplier<T> {
private final Class<T> type;
private final Supplier<? extends T> supplier;
private final int hashCode;
private T value;

DeferredValue(Class<T> type, Supplier<? extends T> supplier, int hashCode) {
this.type = requireNonNull(type);
this.supplier = requireNonNull(supplier);
this.supplier = Memoize.supplier(supplier);
this.hashCode = hashCode;
}

@Override
public T get() {
T v = value;
if (v == null) {
return value = supplier.get();
}
return v;
return supplier.get();
}

Class<T> type() {
Expand Down
136 changes: 136 additions & 0 deletions biz.aQute.bndlib/src/aQute/bnd/osgi/resource/FileResourceCache.java
@@ -0,0 +1,136 @@
package aQute.bnd.osgi.resource;

import static aQute.bnd.exceptions.SupplierWithException.asSupplierOrElse;
import static aQute.bnd.osgi.Constants.MIME_TYPE_BUNDLE;
import static aQute.bnd.osgi.Constants.MIME_TYPE_JAR;

import java.io.File;
import java.io.IOException;
import java.net.URI;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.attribute.BasicFileAttributeView;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;

import org.osgi.resource.Resource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import aQute.bnd.exceptions.Exceptions;
import aQute.bnd.osgi.Domain;
import aQute.libg.cryptography.SHA256;

class FileResourceCache {
private final static Logger logger = LoggerFactory.getLogger(FileResourceCache.class);
private final static long EXPIRED_DURATION_NANOS = TimeUnit.NANOSECONDS.convert(30L,
TimeUnit.MINUTES);
private static final FileResourceCache INSTANCE = new FileResourceCache();
private final Map<CacheKey, Resource> cache;
private long time;

private FileResourceCache() {
cache = new ConcurrentHashMap<>();
time = System.nanoTime();
}

static FileResourceCache getInstance() {
return INSTANCE;
}

Resource getResource(File file, URI uri) {
if (!file.isFile()) {
return null;
}
// Make sure we don't grow infinitely
final long now = System.nanoTime();
if ((now - time) > EXPIRED_DURATION_NANOS) {
time = now;
cache.keySet()
.removeIf(key -> (now - key.time) > EXPIRED_DURATION_NANOS);
}
CacheKey cacheKey = new CacheKey(file);
Resource resource = cache.computeIfAbsent(cacheKey, key -> {
logger.debug("parsing {}", file);
ResourceBuilder rb = new ResourceBuilder();
try {
Domain manifest = Domain.domain(file);
boolean hasIdentity = false;
if (manifest != null) {
hasIdentity = rb.addManifest(manifest);
}
String mime = hasIdentity ? MIME_TYPE_BUNDLE : MIME_TYPE_JAR;
DeferredValue<String> sha256 = new DeferredComparableValue<>(String.class,
asSupplierOrElse(() -> SHA256.digest(file)
.asHex(), null),
key.hashCode());
rb.addContentCapability(uri, sha256, file.length(), mime);

if (hasIdentity) {
rb.addHashes(file);
}
} catch (Exception e) {
throw Exceptions.duck(e);
}
return rb.build();
});
return resource;
}

static final class CacheKey {
private final Object fileKey;
private final long lastModifiedTime;
private final long size;
final long time;

CacheKey(File file) {
this(file.toPath());
}

CacheKey(Path path) {
BasicFileAttributes attributes;
try {
attributes = Files.getFileAttributeView(path, BasicFileAttributeView.class)
.readAttributes();
} catch (IOException e) {
throw Exceptions.duck(e);
}
if (!attributes.isRegularFile()) {
throw new IllegalArgumentException("File must be a regular file: " + path);
}
Object fileKey = attributes.fileKey();
this.fileKey = (fileKey != null) ? fileKey //
: path.toAbsolutePath(); // Windows FS does not have fileKey
this.lastModifiedTime = attributes.lastModifiedTime()
.toMillis();
this.size = attributes.size();
this.time = System.nanoTime();
}

@Override
public int hashCode() {
return (Objects.hashCode(fileKey) * 31 + Long.hashCode(lastModifiedTime)) * 31 + Long.hashCode(size);
}

@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (!(obj instanceof CacheKey)) {
return false;
}
CacheKey other = (CacheKey) obj;
return Objects.equals(fileKey, other.fileKey) && (lastModifiedTime == other.lastModifiedTime)
&& (size == other.size);
}

@Override
public String toString() {
return Objects.toString(fileKey);
}
}
}
42 changes: 19 additions & 23 deletions biz.aQute.bndlib/src/aQute/bnd/osgi/resource/ResourceBuilder.java
@@ -1,6 +1,5 @@
package aQute.bnd.osgi.resource;

import static aQute.bnd.exceptions.SupplierWithException.asSupplierOrElse;
import static aQute.bnd.osgi.Constants.DUPLICATE_MARKER;
import static aQute.bnd.osgi.Constants.MIME_TYPE_BUNDLE;
import static aQute.bnd.osgi.Constants.MIME_TYPE_JAR;
Expand Down Expand Up @@ -61,7 +60,6 @@
import aQute.lib.hierarchy.Hierarchy;
import aQute.lib.hierarchy.NamedNode;
import aQute.lib.zip.JarIndex;
import aQute.libg.cryptography.SHA256;
import aQute.libg.reporter.ReporterAdapter;
import aQute.service.reporter.Reporter;

Expand All @@ -73,13 +71,19 @@ public class ResourceBuilder {

private boolean built = false;

public ResourceBuilder() {}

public ResourceBuilder(Resource source) {
this();
addResource(source);
}

public ResourceBuilder addResource(Resource source) {
addCapabilities(source.getCapabilities(null));
addRequirements(source.getRequirements(null));
return this;
}

public ResourceBuilder() {}

public ResourceBuilder addCapability(Capability capability) {
CapReqBuilder builder = CapReqBuilder.clone(capability);
return addCapability(builder);
Expand Down Expand Up @@ -724,30 +728,17 @@ public boolean addFile(File file, URI uri) throws Exception {
if (uri == null)
uri = file.toURI();

Domain manifest = Domain.domain(file);
boolean hasIdentity = false;
if (manifest != null) {
hasIdentity = addManifest(manifest);
}
String mime = hasIdentity ? MIME_TYPE_BUNDLE : MIME_TYPE_JAR;
int deferredHashCode = hashCode(file);
DeferredValue<String> sha256 = new DeferredComparableValue<>(String.class,
asSupplierOrElse(() -> SHA256.digest(file)
.asHex(), null),
deferredHashCode);
addContentCapability(uri, sha256, file.length(), mime);

if (hasIdentity) {
addHashes(file);
Resource fileResource = FileResourceCache.getInstance()
.getResource(file, uri);
if (fileResource != null) {
addResource(fileResource);
hasIdentity = !fileResource.getCapabilities(IdentityNamespace.IDENTITY_NAMESPACE)
.isEmpty();
}
return hasIdentity;
}

private static int hashCode(File file) {
return file.getAbsoluteFile()
.hashCode();
}

/**
* Add simple class name hashes to the exported packages. This should not be
* called before any package capabilities are set since we only hash class
Expand Down Expand Up @@ -833,6 +824,11 @@ public Resource build() {
return null;
}

@Override
public ResourceBuilder addResource(Resource source) {
return ResourceBuilder.this.addResource(source);
}

@Override
public ResourceBuilder addCapability(Capability capability) {
return ResourceBuilder.this.addCapability(capability);
Expand Down
@@ -1,4 +1,4 @@
@Version("4.2.0")
@Version("4.3.0")
package aQute.bnd.osgi.resource;

import org.osgi.annotation.versioning.Version;

0 comments on commit 31f140a

Please sign in to comment.