| /******************************************************************************* |
| * Copyright (c) 2007, 2018 IBM Corporation and others. |
| * All rights reserved. |
| * This program and the accompanying materials are made available under the |
| * terms of the Eclipse Public License 2.0 which accompanies this distribution, |
| * and is available at |
| * https://www.eclipse.org/legal/epl-2.0/ |
| * |
| * SPDX-License-Identifier: EPL-2.0 |
| * |
| * Contributors: |
| * IBM Corporation - initial API and implementation |
| * Red Hat, Inc (Krzysztof Daniel) - Bug 421935: Extend simpleconfigurator to |
| * read .info files from many locations |
| ******************************************************************************/ |
| package org.eclipse.equinox.internal.simpleconfigurator.utils; |
| |
| import java.io.*; |
| import java.net.*; |
| import java.nio.file.Files; |
| import java.nio.file.attribute.FileTime; |
| import java.util.*; |
| import org.eclipse.equinox.internal.simpleconfigurator.Activator; |
| import org.osgi.framework.Version; |
| |
| public class SimpleConfiguratorUtils { |
| |
| private static final String LINK_KEY = "link"; |
| private static final String LINK_FILE_EXTENSION = ".link"; |
| private static final String UNC_PREFIX = "//"; |
| private static final String VERSION_PREFIX = "#version="; |
| public static final String ENCODING_UTF8 = "#encoding=UTF-8"; |
| public static final Version COMPATIBLE_VERSION = new Version(1, 0, 0); |
| |
| private static final String FILE_SCHEME = "file"; |
| private static final String REFERENCE_PREFIX = "reference:"; |
| private static final String FILE_PREFIX = "file:"; |
| private static final String COMMA = ","; |
| private static final String ENCODED_COMMA = "%2C"; |
| |
| private static final Set<File> reportedExtensions = Collections.synchronizedSet(new HashSet<File>(0)); |
| |
| public static List<BundleInfo> readConfiguration(URL url, URI base) throws IOException { |
| List<BundleInfo> result = new ArrayList<>(); |
| |
| //old behaviour |
| result.addAll(readConfigurationFromFile(url, base)); |
| |
| if (!Activator.EXTENDED) { |
| return result; |
| } |
| readExtendedConfigurationFiles(result); |
| //dedup - some bundles may be listed more than once |
| removeDuplicates(result); |
| return result; |
| } |
| |
| public static void removeDuplicates(List<BundleInfo> result) { |
| if (result.size() > 1) { |
| int index = 0; |
| while (index < result.size()) { |
| String aSymbolicName = result.get(index).getSymbolicName(); |
| String aVersion = result.get(index).getVersion(); |
| |
| for (int i = index + 1; i < result.size();) { |
| String bSymbolicName = result.get(i).getSymbolicName(); |
| String bVersion = result.get(i).getVersion(); |
| if (aSymbolicName.equals(bSymbolicName) && aVersion.equals(bVersion)) { |
| result.remove(i); |
| } else { |
| i++; |
| } |
| } |
| |
| index++; |
| } |
| } |
| } |
| |
| public static void readExtendedConfigurationFiles(List<BundleInfo> result) throws IOException, FileNotFoundException, MalformedURLException { |
| //extended behaviour |
| List<File> files; |
| try { |
| files = getInfoFiles(); |
| for (File info : files) { |
| List<BundleInfo> list = readConfigurationFromFile(info.toURL(), info.getParentFile().toURI()); |
| // extensions are relative to extension root, not to the framework |
| // it is necessary to replace relative locations with absolute ones |
| for (int i = 0; i < list.size(); i++) { |
| BundleInfo singleInfo = list.get(i); |
| if (singleInfo.getBaseLocation() != null) { |
| singleInfo = new BundleInfo(singleInfo.getSymbolicName(), singleInfo.getVersion(), singleInfo.getBaseLocation().resolve(singleInfo.getLocation()), singleInfo.getStartLevel(), singleInfo.isMarkedAsStarted()); |
| list.remove(i); |
| list.add(i, singleInfo); |
| } |
| } |
| if (Activator.DEBUG) { |
| System.out.println("List of bundles to be loaded from " + info.toURL()); |
| for (BundleInfo b : list) { |
| System.out.println(b.getSymbolicName() + "_" + b.getVersion()); |
| } |
| } |
| result.addAll(list); |
| } |
| } catch (URISyntaxException e) { |
| throw new IllegalArgumentException("Couldn't parse simpleconfigurator extensions", e); |
| } |
| } |
| |
| public static ArrayList<File> getInfoFiles() throws IOException, FileNotFoundException, URISyntaxException { |
| ArrayList<File> files = new ArrayList<>(1); |
| |
| if (Activator.EXTENSIONS != null) { |
| //configured simpleconfigurator extensions location |
| String stringExtenionLocation = Activator.EXTENSIONS; |
| String[] locationToCheck = stringExtenionLocation.split(","); |
| for (String location : locationToCheck) { |
| files.addAll(getInfoFilesFromLocation(location)); |
| } |
| } |
| return files; |
| } |
| |
| private static ArrayList<File> getInfoFilesFromLocation(String locationToCheck) throws IOException, FileNotFoundException, URISyntaxException { |
| ArrayList<File> result = new ArrayList<>(1); |
| |
| File extensionsLocation = new File(locationToCheck); |
| |
| if (extensionsLocation.exists() && extensionsLocation.isDirectory()) { |
| //extension location contains extensions |
| File[] extensions = extensionsLocation.listFiles(); |
| for (File extension : extensions) { |
| if (extension.isFile() && extension.getName().endsWith(LINK_FILE_EXTENSION)) { |
| Properties link = new Properties(); |
| link.load(new FileInputStream(extension)); |
| String newInfoName = link.getProperty(LINK_KEY); |
| URI newInfoURI = new URI(newInfoName); |
| File newInfoFile = null; |
| if (newInfoURI.isAbsolute()) { |
| newInfoFile = new File(newInfoName); |
| } else { |
| newInfoFile = new File(extension.getParentFile(), newInfoName); |
| } |
| if (newInfoFile.exists()) { |
| extension = newInfoFile.getParentFile(); |
| } |
| } |
| |
| if (extension.isDirectory()) { |
| if (Files.isWritable(extension.toPath())) { |
| synchronized (reportedExtensions) { |
| if (!reportedExtensions.contains(extension)) { |
| reportedExtensions.add(extension); |
| System.err.println("Fragment directory should be read only " + extension); |
| } |
| } |
| continue; |
| } |
| File[] listFiles = extension.listFiles(); |
| // new magic - multiple info files, f.e. |
| // egit.info (git feature) |
| // cdt.link (properties file containing link=path) to other info file |
| for (File file : listFiles) { |
| //if it is a info file - load it |
| if (file.getName().endsWith(".info")) { |
| result.add(file); |
| } |
| // if it is a link - dereference it |
| } |
| } else if (Activator.DEBUG) { |
| synchronized (reportedExtensions) { |
| if (!reportedExtensions.contains(extension)) { |
| reportedExtensions.add(extension); |
| System.out.println("Unrecognized fragment " + extension); |
| } |
| } |
| } |
| } |
| } |
| return result; |
| } |
| |
| private static List<BundleInfo> readConfigurationFromFile(URL url, URI base) throws IOException { |
| InputStream stream = null; |
| try { |
| stream = url.openStream(); |
| } catch (IOException e) { |
| // if the exception is a FNF we return an empty bundle list |
| if (e instanceof FileNotFoundException) |
| return Collections.emptyList(); |
| throw e; |
| } |
| |
| try { |
| return readConfiguration(stream, base); |
| } finally { |
| stream.close(); |
| } |
| } |
| |
| /** |
| * Read the configuration from the given InputStream |
| * |
| * @param stream - the stream is always closed |
| * @param base |
| * @return List of {@link BundleInfo} |
| * @throws IOException |
| */ |
| public static List<BundleInfo> readConfiguration(InputStream stream, URI base) throws IOException { |
| List<BundleInfo> bundles = new ArrayList<>(); |
| |
| BufferedInputStream bufferedStream = new BufferedInputStream(stream); |
| String encoding = determineEncoding(bufferedStream); |
| |
| String line; |
| try (BufferedReader r = new BufferedReader(encoding == null ? new InputStreamReader(bufferedStream) : new InputStreamReader(bufferedStream, encoding));) { |
| while ((line = r.readLine()) != null) { |
| line = line.trim(); |
| //ignore any comment or empty lines |
| if (line.length() == 0) |
| continue; |
| |
| if (line.startsWith("#")) {//$NON-NLS-1$ |
| parseCommentLine(line); |
| continue; |
| } |
| |
| BundleInfo bundleInfo = parseBundleInfoLine(line, base); |
| if (bundleInfo != null) |
| bundles.add(bundleInfo); |
| } |
| } |
| return bundles; |
| } |
| |
| /* |
| * We expect the first line of the bundles.info to be |
| * #encoding=UTF-8 |
| * if it isn't, then it is an older bundles.info and should be |
| * read with the default encoding |
| */ |
| private static String determineEncoding(BufferedInputStream stream) { |
| byte[] utfBytes = ENCODING_UTF8.getBytes(); |
| byte[] buffer = new byte[utfBytes.length]; |
| |
| int bytesRead = -1; |
| stream.mark(utfBytes.length + 1); |
| try { |
| bytesRead = stream.read(buffer); |
| } catch (IOException e) { |
| //do nothing |
| } |
| |
| if (bytesRead == utfBytes.length && Arrays.equals(utfBytes, buffer)) |
| return "UTF-8"; |
| |
| //if the first bytes weren't the encoding, need to reset |
| try { |
| stream.reset(); |
| } catch (IOException e) { |
| // nothing |
| } |
| return null; |
| } |
| |
| public static void parseCommentLine(String line) { |
| // version |
| if (line.startsWith(VERSION_PREFIX)) { |
| String version = line.substring(VERSION_PREFIX.length()).trim(); |
| if (!COMPATIBLE_VERSION.equals(new Version(version))) |
| throw new IllegalArgumentException("Invalid version: " + version); |
| } |
| } |
| |
| public static BundleInfo parseBundleInfoLine(String line, URI base) { |
| // symbolicName,version,location,startLevel,markedAsStarted |
| StringTokenizer tok = new StringTokenizer(line, COMMA); |
| int numberOfTokens = tok.countTokens(); |
| if (numberOfTokens < 5) |
| throw new IllegalArgumentException("Line does not contain at least 5 tokens: " + line); |
| |
| String symbolicName = tok.nextToken().trim(); |
| String version = tok.nextToken().trim(); |
| URI location = parseLocation(tok.nextToken().trim()); |
| int startLevel = Integer.parseInt(tok.nextToken().trim()); |
| boolean markedAsStarted = Boolean.parseBoolean(tok.nextToken()); |
| BundleInfo result = new BundleInfo(symbolicName, version, location, startLevel, markedAsStarted); |
| if (!location.isAbsolute()) |
| result.setBaseLocation(base); |
| return result; |
| } |
| |
| public static URI parseLocation(String location) { |
| // decode any commas we previously encoded when writing this line |
| int encodedCommaIndex = location.indexOf(ENCODED_COMMA); |
| while (encodedCommaIndex != -1) { |
| location = location.substring(0, encodedCommaIndex) + COMMA + location.substring(encodedCommaIndex + 3); |
| encodedCommaIndex = location.indexOf(ENCODED_COMMA); |
| } |
| |
| if (File.separatorChar != '/') { |
| int colon = location.indexOf(':'); |
| String scheme = colon < 0 ? null : location.substring(0, colon); |
| if (scheme == null || scheme.equals(FILE_SCHEME)) |
| location = location.replace(File.separatorChar, '/'); |
| //if the file is a UNC path, insert extra leading // if needed to make a valid URI (see bug 207103) |
| if (scheme == null) { |
| if (location.startsWith(UNC_PREFIX) && !location.startsWith(UNC_PREFIX, 2)) |
| location = UNC_PREFIX + location; |
| } else { |
| //insert UNC prefix after the scheme |
| if (location.startsWith(UNC_PREFIX, colon + 1) && !location.startsWith(UNC_PREFIX, colon + 3)) |
| location = location.substring(0, colon + 3) + location.substring(colon + 1); |
| } |
| } |
| |
| try { |
| URI uri = new URI(location); |
| if (!uri.isOpaque()) |
| return uri; |
| } catch (URISyntaxException e1) { |
| // this will catch the use of invalid URI characters (e.g. spaces, etc.) |
| // ignore and fall through |
| } |
| |
| try { |
| return URIUtil.fromString(location); |
| } catch (URISyntaxException e) { |
| throw new IllegalArgumentException("Invalid location: " + location); |
| } |
| } |
| |
| public static void transferStreams(List<InputStream> sources, OutputStream destination) throws IOException { |
| destination = new BufferedOutputStream(destination); |
| try { |
| for (int i = 0; i < sources.size(); i++) { |
| try (InputStream source = new BufferedInputStream(sources.get(i))) { |
| byte[] buffer = new byte[8192]; |
| while (true) { |
| int bytesRead = -1; |
| if ((bytesRead = source.read(buffer)) == -1) |
| break; |
| destination.write(buffer, 0, bytesRead); |
| } |
| } |
| } |
| } finally { |
| try { |
| destination.close(); |
| } catch (IOException e) { |
| // ignore |
| } |
| } |
| } |
| |
| // This will produce an unencoded URL string |
| public static String getBundleLocation(BundleInfo bundle, boolean useReference) { |
| URI location = bundle.getLocation(); |
| String scheme = location.getScheme(); |
| String host = location.getHost(); |
| String path = location.getPath(); |
| |
| if (location.getScheme() == null) { |
| URI baseLocation = bundle.getBaseLocation(); |
| if (baseLocation != null && baseLocation.getScheme() != null) { |
| scheme = baseLocation.getScheme(); |
| host = baseLocation.getHost(); |
| } |
| } |
| |
| String bundleLocation = null; |
| try { |
| URL bundleLocationURL = new URL(scheme, host, path); |
| bundleLocation = bundleLocationURL.toExternalForm(); |
| |
| } catch (MalformedURLException e1) { |
| bundleLocation = location.toString(); |
| } |
| |
| if (useReference && bundleLocation.startsWith(FILE_PREFIX)) |
| bundleLocation = REFERENCE_PREFIX + bundleLocation; |
| return bundleLocation; |
| } |
| |
| public static long getExtendedTimeStamp() { |
| long regularTimestamp = -1; |
| if (Activator.EXTENDED) { |
| try { |
| ArrayList<File> infoFiles = SimpleConfiguratorUtils.getInfoFiles(); |
| for (File f : infoFiles) { |
| long infoFileLastModified = getFileLastModified(f); |
| // pick latest modified always |
| if (infoFileLastModified > regularTimestamp) { |
| regularTimestamp = infoFileLastModified; |
| } |
| } |
| } catch (IOException | URISyntaxException e) { |
| if (Activator.DEBUG) { |
| e.printStackTrace(); |
| } |
| } |
| if (Activator.DEBUG) { |
| System.out.println("Fragments timestamp: " + regularTimestamp); |
| } |
| } |
| return regularTimestamp; |
| } |
| |
| public static long getFileLastModified(File file) { |
| long lastModified = file.lastModified(); |
| if (lastModified == 0) { |
| try { |
| // Note that "ctime" is different to a file's creation time (on posix |
| // platforms creation time is a synonym for last modified time) |
| FileTime ctime = (FileTime) Files.getAttribute(file.toPath(), "unix:ctime"); |
| lastModified = ctime.toMillis(); |
| } catch (UnsupportedOperationException | IllegalArgumentException | IOException e) { |
| // We expect this attribute to not exist on non-posix platforms like Windows |
| } |
| } |
| return lastModified; |
| } |
| } |