/**
* Copyright 2015 StreamSets Inc.
*
* Licensed under the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.streamsets.pipeline;
import java.io.IOException;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.SortedSet;
import java.util.TreeSet;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
public class ApplicationPackage {
private static final String MACOS_JAVA_EXTENSIONS_DIR = "/System/Library/Java/Extensions/";
private static final String JAVA_HOME;
private static final String STREAMSETS_PACKAGE = "com.streamsets";
private static final String CLASS_FILE_SUFFIX = ".class";
private static final String JAR_FILE_SUFFIX = ".jar";
static {
String javaHome = System.getProperty("java.home");
if (javaHome.endsWith("jre")) {
javaHome = javaHome.substring(0, javaHome.lastIndexOf("jre"));
}
JAVA_HOME = javaHome;
if(!ApplicationPackage.class.getPackage().getName().startsWith(STREAMSETS_PACKAGE)) {
throw new RuntimeException("Refactor occurred without changing StreamSets package name");
}
}
private static final Map<ClassLoader, ApplicationPackage> instances = new HashMap<>();
private final SortedSet<String> packages;
private final int lengthOfSmallestPackage;
private int operationCount;
public ApplicationPackage(ClassLoader cl) {
this(findApplicationPackageNames(cl));
}
public ApplicationPackage(SortedSet<String> packages) {
this.packages = packages;
this.operationCount = 0;
int lengthOfSmallestPackage = Integer.MAX_VALUE;
for (String packageName : packages) {
lengthOfSmallestPackage = Math.min(packageName.length(), lengthOfSmallestPackage);
}
this.lengthOfSmallestPackage = lengthOfSmallestPackage;
}
@Override
public String toString() {
return "ApplicationPackage{" +
"packages=" + packages +
", lengthOfSmallestPackage=" + lengthOfSmallestPackage +
'}';
}
public boolean isApplication(String packageName) {
if (packageName.isEmpty()) {
return false;
}
/*
* packages contains a list of java packages and if the argument
* resides in any of those packages, we want to return true that
* this package should be considered an application package.
*
* One option is to perform a linear search but that is of course
* would be very slow. As such packages is stored as a sorted set
* which allows us to use the sort order of the package names
* to reduce the number of operations.
*
* Assume we have packages a. b. c. if searching for b.1. the ideal
* case is to search the set sub-set of (b.) and to find this sub-set
* we need two bounds. The lower bound is easy, it's b.1 while the
* upper bound is also trivial in this case either b or b.
*
* In general the upper bounds should be the largest portion of the
* needle which excludes no possible matches. For example assume we
* have packages org.apache.hadoop. and org.apache.xerces. and the need
* is org.apache.hadoop.hdfs., for correctness the longest upper bound is
* org.apache.hadoop.
*/
String canonicalName = ClassLoaderUtil.canonicalizeClassOrResource(packageName);
String from;
if (canonicalName.length() > lengthOfSmallestPackage) {
from = canonicalName.substring(0, lengthOfSmallestPackage - 1);
} else if (lengthOfSmallestPackage == Integer.MAX_VALUE) {
return false;
} else {
from = canonicalName.substring(0, 1);
}
for (String otherPackageName : packages.subSet(from, canonicalName)) {
operationCount++;
if (canonicalName.startsWith(otherPackageName)) {
return true;
}
}
return false;
}
public int getOperationCount() {
return operationCount;
}
public static synchronized ApplicationPackage get(ClassLoader cl) {
ApplicationPackage result = instances.get(cl);
if (result == null) {
result = new ApplicationPackage(cl);
instances.put(cl, result);
}
return result;
}
/**
* Finds package names associated with a class loader which which are not JVM level
* packages. Anything inside java home are specifically excluded as well as some
* OS specific install locations (MacOS).
*/
private static SortedSet<String> findApplicationPackageNames(ClassLoader cl) {
SortedSet<String> packages = new TreeSet<>();
while (cl != null) {
if (cl instanceof URLClassLoader) {
for (URL url : ((URLClassLoader)cl).getURLs()) {
String path = url.getPath();
if (!path.startsWith(JAVA_HOME) && !path.startsWith(MACOS_JAVA_EXTENSIONS_DIR) &&
path.endsWith(JAR_FILE_SUFFIX)) {
try {
try (ZipInputStream zip = new ZipInputStream(url.openStream())) {
for (ZipEntry entry = zip.getNextEntry(); entry != null; entry = zip.getNextEntry()) {
if (!entry.isDirectory() && entry.getName().endsWith(CLASS_FILE_SUFFIX)) {
// This ZipEntry represents a class. Now, what class does it represent?
String className = entry.getName().replace('/', '.'); // including ".class"
className = className.substring(0, className.length() - CLASS_FILE_SUFFIX.length());
if (className.contains(".") && !className.startsWith(STREAMSETS_PACKAGE)) {
// must end with a . as we don't want o.a.h matching o.a.ha
packages.add(className.substring(0, className.lastIndexOf('.')) + ".");
}
}
}
}
} catch (IOException unlikely) {
// since these are local URL we will likely only
// hit this if there is a corrupt jar in the classpath
// which we will ignore
if (SDCClassLoader.isDebug()) {
System.err.println("Error opening '" + url + "' : " + unlikely);
unlikely.printStackTrace();
}
}
}
}
}
cl = cl.getParent();
}
SystemPackage systemPackage = new SystemPackage(SDCClassLoader.SYSTEM_API_CHILDREN_CLASSES);
Iterator<String> iterator = packages.iterator();
while (iterator.hasNext()) {
String packageName = iterator.next();
if (systemPackage.isSystem(packageName)) {
iterator.remove();
}
}
removeLogicalDuplicates(packages);
return packages;
}
/**
* Traverses sorted list of packages and removes logical duplicates. For example
* if the set contains akka., akka.io., and akka.util. only akka. will remain.
* Note that if the set contains only akka.io. and akka.util. both will remain.
* Otherwise all of the org.apache. would devolve to org.
*/
static void removeLogicalDuplicates(SortedSet<String> packages) {
Iterator<String> iterator = packages.iterator();
if (!iterator.hasNext()) {
return;
}
String last = iterator.next();
while (iterator.hasNext()) {
String current = iterator.next();
if (current.startsWith(last)) {
iterator.remove();
} else {
last = current;
}
}
}
}