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