| /*=============================================================================# |
| # 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() { |
| } |
| |
| } |