Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

repository: Add a File Resource cache to ResourceBuilder #5372

Merged
merged 4 commits into from Oct 10, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
@@ -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;