blob: 0e03970fcdf2feb9f0e041668e9d4fb4c53dac1f [file] [log] [blame]
/*=============================================================================#
# Copyright (c) 2018, 2021 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.FileSystem;
import java.nio.file.FileSystemAlreadyExistsException;
import java.nio.file.FileSystemNotFoundException;
import java.nio.file.FileSystems;
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.runtime.bundle.BundleEntryProvider.DevBinPathEntryProvider;
import org.eclipse.statet.jcommons.runtime.bundle.BundleEntryProvider.JarFilePathEntryProvider;
import org.eclipse.statet.jcommons.status.ErrorStatus;
import org.eclipse.statet.jcommons.status.StatusException;
@NonNullByDefault
public class Bundles {
public static final String BUNDLE_RESOLVERS_PROPERTY_KEY= "org.eclipse.statet.jcommons.runtime.bundle.BundleResolvers"; //$NON-NLS-1$
private static final Pattern REF_CLASS_JAR_PATTERN= Pattern.compile(
Pattern.quote(BUNDLE_ID + "/target/" + BUNDLE_ID) + "[^/!]+.jar(?:[!]|$)" ); //$NON-NLS-1$
/**
* Creates a bundle resolver.
*
* <ol>
* <li>Bundle resolver specified by system property {@link #BUNDLE_RESOLVERS_PROPERTY_KEY}.</li>
* <li>Bundle resolver determined automatically.</li>
* </ol>
*
* @return a bundle resolver
*/
public static BundleResolver createResolver() throws StatusException {
String id= System.getProperty(BUNDLE_RESOLVERS_PROPERTY_KEY);
if (id == null || id.isEmpty()) {
final BundleEntry bundleEntry= detectEntry(Bundles.class);
if (bundleEntry instanceof BundleEntry.Jar
&& !REF_CLASS_JAR_PATTERN.matcher(bundleEntry.getUrlString()).find()) {
id= DefaultBundleResolver.ID;
}
else {
id= RefClassBundleResolver.ID;
}
}
return createResolver(id);
}
static BundleResolver createResolver(final String id) throws StatusException {
if (id.equals(DefaultBundleResolver.ID)) {
return new DefaultBundleResolver(
detectEntryProvider(Bundles.class) );
}
if (id.equals(RefClassBundleResolver.ID)) {
return new RefClassBundleResolver();
}
if (id.indexOf('.') > 0) {
try {
final Class<?> resolverClass= Class.forName(id);
return (BundleResolver)resolverClass.newInstance();
}
catch (final ClassNotFoundException | InstantiationException | IllegalAccessException e) {
throw new StatusException(new ErrorStatus(BUNDLE_ID,
String.format("Failed to create bundle resolver '%1$s'.", id),
e ));
}
}
throw new StatusException(new ErrorStatus(BUNDLE_ID,
String.format("Unknown bundle resolver '%1$s'.", id) ));
}
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 BundleEntryProvider detectEntryProvider(final Class<?> refClass)
throws StatusException {
return detectEntryProvider(refClass, null);
}
public static BundleEntryProvider detectEntryProvider(final String refUrl)
throws StatusException {
try {
return detectEntryProvider(refUrl, null);
}
catch (final Exception e) {
throw new StatusException(new ErrorStatus(BUNDLE_ID,
String.format("Failed to autodetect bundle location" +
"\n\turl= %1$s", //$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;
{ final Path detectedBaseDirectory= getPath(detectedBaseUri);
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;
{ final Path detectedBaseDirectory= getPath(detectedBaseUri);
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);
}
}
}
/**
* Enhanced Path.get supporting jar files.
*/
static Path getPath(final URI url) throws IOException {
FileSystem fs= null;
while (true) {
try {
final Path path= Paths.get(url);
return path.normalize();
}
catch (final FileSystemNotFoundException e) {
final Map<String, String> fsEnv= new HashMap<>();
// fsEnv.put("create", "true"); //$NON-NLS-1$ //$NON-NLS-2$
try {
fs= FileSystems.newFileSystem(url, fsEnv);
// closeables= ImCollections.newList(fs);
}
catch (final FileSystemAlreadyExistsException exists) {}
}
}
}
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 ));
}
}
}
private Bundles() {
}
}