blob: 986f19fd7f39c03e4371714c4e98c78e56b3412d [file] [log] [blame]
/*=============================================================================#
# Copyright (c) 2018, 2020 Stephan Wahlbrink and others.
#
# This program and the accompanying materials are made available under the
# terms of the Eclipse Public License 2.0 which is available at
# https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
# which is available at https://www.apache.org/licenses/LICENSE-2.0.
#
# SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
#
# Contributors:
# Stephan Wahlbrink <sw@wahlbrink.eu> - initial API and implementation
#=============================================================================*/
package org.eclipse.statet.jcommons.runtime.bundle;
import static org.eclipse.statet.internal.jcommons.runtime.CommonsRuntimeInternals.BUNDLE_ID;
import static org.eclipse.statet.jcommons.lang.ObjectUtils.nonNullAssert;
import static org.eclipse.statet.jcommons.runtime.CommonsRuntime.log;
import java.io.Closeable;
import java.io.IOException;
import java.net.URI;
import java.nio.file.DirectoryStream;
import java.nio.file.FileSystem;
import java.nio.file.FileSystemAlreadyExistsException;
import java.nio.file.FileSystemNotFoundException;
import java.nio.file.FileSystems;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.eclipse.statet.jcommons.collections.ImCollections;
import org.eclipse.statet.jcommons.collections.ImList;
import org.eclipse.statet.jcommons.lang.NonNullByDefault;
import org.eclipse.statet.jcommons.lang.Nullable;
import org.eclipse.statet.jcommons.runtime.ClassLoaderUtils;
import org.eclipse.statet.jcommons.runtime.UriUtils;
import org.eclipse.statet.jcommons.status.ErrorStatus;
import org.eclipse.statet.jcommons.status.StatusException;
@NonNullByDefault
public abstract class BundleEntryProvider {
public static class JarFilePathEntryProvider extends BundleEntryProvider {
private final Pattern namePattern;
public JarFilePathEntryProvider(final ImList<Path> baseDirectories, final Pattern namePattern,
final List<Closeable> closeables) {
super(baseDirectories, closeables);
this.namePattern= namePattern;
}
@Override
protected @Nullable BundleEntry createEntry(final Path candidate) {
final Matcher nameMatcher= this.namePattern.matcher(
nonNullAssert(candidate.getFileName()).toString() );
if (nameMatcher.matches() && Files.isRegularFile(candidate)) {
final String bundleId= nonNullAssert(nameMatcher.group(1));
return new BundleEntry.Jar(bundleId, candidate);
}
return null;
}
/** for tests */
@Nullable String getBundleId(final String fileName) {
final Matcher nameMatcher= this.namePattern.matcher(fileName);
if (nameMatcher.matches()) {
return nameMatcher.group(1);
}
return null;
}
@Override
public int hashCode() {
return super.hashCode() + this.namePattern.hashCode() * 17;
}
@Override
public boolean equals(final @Nullable Object obj) {
return (super.equals(obj)
&& this.namePattern.pattern().equals(((JarFilePathEntryProvider) obj).namePattern.pattern()) );
}
}
public static class DevBinPathEntryProvider extends BundleEntryProvider {
public DevBinPathEntryProvider(final ImList<Path> baseDirectories,
final List<Closeable> closeables) {
super(baseDirectories, closeables);
}
@Override
protected @Nullable BundleEntry createEntry(final Path candidate) {
final Path devBin;
if (Files.isDirectory(devBin= candidate.resolve("target/classes"))) { //$NON-NLS-1$
final String bundleId= nonNullAssert(candidate.getFileName()).toString();
return new BundleEntry(bundleId, devBin) {
@Override
public @Nullable Path getResourcePath(final String resource) {
Path path= super.getResourcePath(resource);
if (path != null) {
return path;
}
path= getPath().getParent().resolveSibling(resource);
if (Files.exists(path)) {
return path;
}
return null;
}
};
}
return null;
}
}
private final ImList<Path> baseDirectories;
private final List<Closeable> closeables;
protected BundleEntryProvider(final ImList<Path> baseDirectories,
final List<Closeable> closeables) {
this.baseDirectories= baseDirectories;
this.closeables= closeables;
}
public void dispose() {
close(this.closeables);
}
protected ImList<Path> getBaseDirectories() {
return this.baseDirectories;
}
public void getEntries(final List<BundleEntry> entries) {
for (final Path baseDirectory : this.baseDirectories) {
try {
getEntries(baseDirectory, entries);
}
catch (final Exception e) {
log(new ErrorStatus(BUNDLE_ID,
String.format("An error occurred when looking for path entries in '%1$s'.",
baseDirectory ),
e ));
}
}
}
protected void getEntries(final Path baseDirectory, final List<BundleEntry> entries)
throws IOException {
try (final DirectoryStream<Path> children= Files.newDirectoryStream(baseDirectory)) {
for (final Path child : children) {
final BundleEntry entry= createEntry(child);
if (entry != null) {
entries.add(entry);
}
}
}
}
protected @Nullable BundleEntry createEntry(final Path candidate) {
return null;
}
@Override
public int hashCode() {
return getClass().hashCode() + this.baseDirectories.hashCode();
}
@Override
public boolean equals(final @Nullable Object obj) {
if (this == obj) {
return true;
}
if (obj != null && getClass() == obj.getClass()) {
final BundleEntryProvider other= (BundleEntryProvider) obj;
return (this.baseDirectories.equals(other.baseDirectories));
}
return false;
}
private static final String FILE_PROTOCOL_REGEX= "\\Qfile:/\\E"; //$NON-NLS-1$
private static final String JAR_FILE_PROTOCOL_REGEX= "\\Qjar:file:/\\E"; //$NON-NLS-1$
private static final String BUNDLE_ID_REGEX= "[a-z]+(?:\\.?[a-z]+)*"; //$NON-NLS-1$
private static final String VER_1_REGEX= "\\_\\d+\\.\\d+[^!/]+"; //$NON-NLS-1$
private static final String VER_2_REGEX= "\\-\\d+\\.\\d+[^!/]+"; //$NON-NLS-1$
private static final String JAR_REGEX= "(?<![-._]sources?)\\Q.jar\\E"; //$NON-NLS-1$
private static final String AUTODETECT_REGEX=
"(?:" + //$NON-NLS-1$
"(" + FILE_PROTOCOL_REGEX + ".*)/(" + BUNDLE_ID_REGEX + ")\\Q/target/classes/\\E" + // match 1= file: .. //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
"|" + //$NON-NLS-1$
"(" + JAR_FILE_PROTOCOL_REGEX + ".*)/(" + BUNDLE_ID_REGEX + // match 2= jar:file: .. //$NON-NLS-1$ //$NON-NLS-2$
"(?:(" + VER_1_REGEX + ")|(" + VER_2_REGEX + "))?" + // match 3= ver_1, match 4= ver_2 //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
JAR_REGEX + ")\\Q!/\\E" + //$NON-NLS-1$
")"; //$NON-NLS-1$
private static final Pattern AUTODETECT_PATTERN= Pattern.compile(AUTODETECT_REGEX);
private static final int AUTODETECT_FILE_PROTOCOL_BASE_NUM= 1;
private static final int AUTODETECT_FILE_PROTOCOL_NAME_NUM= 2;
private static final int AUTODETECT_JAR_FILE_PROTOCOL_BASE_NUM= 3;
private static final int AUTODETECT_JAR_FILE_PROTOCOL_NAME_NUM= 4;
private static final int AUTODETECT_JAR_FILE_PROTOCOL_VER_1_NUM= 5;
private static final int AUTODETECT_JAR_FILE_PROTOCOL_VER_2_NUM= 6;
private static final Pattern JAR_VER_0_PATTERN= Pattern.compile("(.+?)" + JAR_REGEX); //$NON-NLS-1$
private static final Pattern JAR_VER_1_PATTERN= Pattern.compile("(.+?)" + VER_1_REGEX + JAR_REGEX); //$NON-NLS-1$
private static final Pattern JAR_VER_2_PATTERN= Pattern.compile("(.+?)" + VER_2_REGEX + JAR_REGEX); //$NON-NLS-1$
public static BundleEntryProvider detectEntryProvider(final Class<?> refClass,
final @Nullable List<Path> expliciteBaseDirectories) throws StatusException {
String refUrl= null;
try {
refUrl= ClassLoaderUtils.getClassLocationUrlString(refClass);
return detectEntryProvider(refUrl, expliciteBaseDirectories);
}
catch (final Exception e) {
throw new StatusException(new ErrorStatus(BUNDLE_ID,
String.format("Failed to autodetect bundle location" +
"\n\tclass= %1$s" + //$NON-NLS-1$
"\n\turl= %2$s", //$NON-NLS-1$
(refClass != null) ? refClass.getName() : "<NA>", //$NON-NLS-1$
(refUrl != null) ? '\'' + refUrl + '\'' : "<NA>" ), //$NON-NLS-1$
e ));
}
}
public static BundleEntry detectEntry(final Class<?> refClass) throws StatusException {
String refUrl= null;
try {
refUrl= ClassLoaderUtils.getClassLocationUrlString(refClass);
return detectEntry(refUrl);
}
catch (final Exception e) {
throw new StatusException(new ErrorStatus(BUNDLE_ID,
String.format("Failed to autodetect bundle location" +
"\n\tclass= %1$s" + //$NON-NLS-1$
"\n\turl= %2$s", //$NON-NLS-1$
(refClass != null) ? refClass.getName() : "<NA>", //$NON-NLS-1$
(refUrl != null) ? '\'' + refUrl + '\'' : "<NA>" ), //$NON-NLS-1$
e ));
}
}
static BundleEntryProvider detectEntryProvider(final String refUrl,
final @Nullable List<Path> expliciteBaseDirectories) throws Exception {
List<Closeable> closeables= ImCollections.emptyList();
try {
final Matcher matcher= AUTODETECT_PATTERN.matcher(refUrl);
if (matcher.matches()) {
int detectedType;
final URI detectedBaseUri;
if (matcher.start(AUTODETECT_FILE_PROTOCOL_BASE_NUM) != -1) { // file:
detectedType= 1;
final String s= nonNullAssert(matcher.group(AUTODETECT_FILE_PROTOCOL_BASE_NUM));
detectedBaseUri= new URI(s);
}
else if (matcher.start(AUTODETECT_JAR_FILE_PROTOCOL_BASE_NUM) != -1) { // jar:file:
detectedType= 2;
final String s= nonNullAssert(matcher.group(AUTODETECT_JAR_FILE_PROTOCOL_BASE_NUM));
if (s.indexOf(UriUtils.JAR_SEPARATOR) == -1) {
detectedBaseUri= new URI(s.substring(4)); // remove jar
}
else {
detectedBaseUri= new URI(s);
}
}
else {
throw new IllegalStateException();
}
final ImList<Path> baseDirectories;
{ Path detectedBaseDirectory;
while (true) {
try {
detectedBaseDirectory= Paths.get(detectedBaseUri);
break;
}
catch (final FileSystemNotFoundException e) {
final Map<String, String> fsEnv= new HashMap<>();
fsEnv.put("create", "true"); //$NON-NLS-1$ //$NON-NLS-2$
try {
final FileSystem fs= FileSystems.newFileSystem(detectedBaseUri, fsEnv);
// closeables= ImCollections.newList(fs);
}
catch (final FileSystemAlreadyExistsException exists) {}
}
}
detectedBaseDirectory= detectedBaseDirectory.normalize();
final List<Path> uniqueList= new ArrayList<>();
if (expliciteBaseDirectories != null && !expliciteBaseDirectories.isEmpty()) {
for (Path baseDirectory : expliciteBaseDirectories) {
baseDirectory= baseDirectory.normalize();
if (!uniqueList.contains(baseDirectory)) {
uniqueList.add(baseDirectory);
}
}
}
if (!uniqueList.contains(detectedBaseDirectory)) {
uniqueList.add(detectedBaseDirectory);
}
baseDirectories= ImCollections.toList(uniqueList);
}
final BundleEntryProvider provider;
if (detectedType == 1) {
provider= new DevBinPathEntryProvider(baseDirectories,
closeables );
}
else {
final Pattern fileNamePattern;
if (matcher.start(AUTODETECT_JAR_FILE_PROTOCOL_VER_1_NUM) != -1) {
fileNamePattern= JAR_VER_1_PATTERN;
}
else if (matcher.start(AUTODETECT_JAR_FILE_PROTOCOL_VER_2_NUM) != -1) {
fileNamePattern= JAR_VER_2_PATTERN;
}
else {
fileNamePattern= JAR_VER_0_PATTERN;
}
provider= new JarFilePathEntryProvider(baseDirectories, fileNamePattern,
closeables );
}
closeables= null;
return provider;
}
throw new UnsupportedOperationException("url= " + refUrl); //$NON-NLS-1$
}
finally {
if (closeables != null) {
close(closeables);
}
}
}
static BundleEntry detectEntry(final String refUrl) throws Exception {
List<Closeable> closeables= ImCollections.emptyList();
try {
final Matcher matcher= AUTODETECT_PATTERN.matcher(refUrl);
if (matcher.matches()) {
int detectedType;
final URI detectedBaseUri;
final String fileName;
if (matcher.start(AUTODETECT_FILE_PROTOCOL_BASE_NUM) != -1) { // file:
detectedType= 1;
final String s= nonNullAssert(matcher.group(AUTODETECT_FILE_PROTOCOL_BASE_NUM));
detectedBaseUri= new URI(s);
fileName= nonNullAssert(matcher.group(AUTODETECT_FILE_PROTOCOL_NAME_NUM));
}
else if (matcher.start(AUTODETECT_JAR_FILE_PROTOCOL_BASE_NUM) != -1) { // jar:file:
detectedType= 2;
final String s= nonNullAssert(matcher.group(AUTODETECT_JAR_FILE_PROTOCOL_BASE_NUM));
if (s.indexOf(UriUtils.JAR_SEPARATOR) == -1) {
detectedBaseUri= new URI(s.substring(4)); // remove jar
}
else {
detectedBaseUri= new URI(s);
}
fileName= nonNullAssert(matcher.group(AUTODETECT_JAR_FILE_PROTOCOL_NAME_NUM));
}
else {
throw new IllegalStateException();
}
final ImList<Path> baseDirectories;
{ Path detectedBaseDirectory;
while (true) {
try {
detectedBaseDirectory= Paths.get(detectedBaseUri);
break;
}
catch (final FileSystemNotFoundException e) {
}
}
detectedBaseDirectory= detectedBaseDirectory.normalize();
baseDirectories= ImCollections.newList(detectedBaseDirectory);
}
final BundleEntryProvider provider;
if (detectedType == 1) {
provider= new DevBinPathEntryProvider(baseDirectories,
closeables );
}
else {
final Pattern fileNamePattern;
if (matcher.start(AUTODETECT_JAR_FILE_PROTOCOL_VER_1_NUM) != -1) {
fileNamePattern= JAR_VER_1_PATTERN;
}
else if (matcher.start(AUTODETECT_JAR_FILE_PROTOCOL_VER_2_NUM) != -1) {
fileNamePattern= JAR_VER_2_PATTERN;
}
else {
fileNamePattern= JAR_VER_0_PATTERN;
}
provider= new JarFilePathEntryProvider(baseDirectories, fileNamePattern,
closeables );
}
closeables= null;
return nonNullAssert(
provider.createEntry(baseDirectories.get(0).resolve(fileName)) );
}
throw new UnsupportedOperationException("url= " + refUrl); //$NON-NLS-1$
}
finally {
if (closeables != null) {
close(closeables);
}
}
}
static void close(final List<Closeable> closeables) {
for (final Closeable closeable : closeables) {
try {
closeable.close();
}
catch (final Exception e) {
log(new ErrorStatus(BUNDLE_ID,
"An error occurred when disposing closable of path entry provider.",
e ));
}
}
}
}