blob: 4ab6505c843b14ba08a5891de597cf7e96d1eff0 [file] [log] [blame]
/**
* Licensed 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
* <p>
* http://www.apache.org/licenses/LICENSE-2.0
* <p>
* 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 org.apache.winegrower.deployer;
import org.apache.winegrower.Ripener;
import org.apache.winegrower.service.BundleRegistry;
import org.osgi.framework.Bundle;
import org.osgi.framework.BundleContext;
import org.osgi.framework.BundleEvent;
import org.osgi.framework.BundleListener;
import org.osgi.framework.Constants;
import org.osgi.framework.Filter;
import org.osgi.framework.FrameworkUtil;
import org.osgi.framework.InvalidSyntaxException;
import org.osgi.framework.ServiceReference;
import org.osgi.framework.ServiceRegistration;
import org.osgi.framework.Version;
import org.osgi.framework.dto.ServiceReferenceDTO;
import org.osgi.framework.wiring.BundleRevision;
import org.osgi.framework.wiring.BundleRevisions;
import org.osgi.framework.wiring.BundleWiring;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.MalformedURLException;
import java.net.URL;
import java.nio.file.FileVisitResult;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.SimpleFileVisitor;
import java.nio.file.attribute.BasicFileAttributes;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Dictionary;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Hashtable;
import java.util.List;
import java.util.Map;
import java.util.jar.Attributes;
import java.util.jar.JarFile;
import java.util.jar.Manifest;
import java.util.stream.Collector;
import java.util.stream.Stream;
import java.util.zip.ZipEntry;
import static java.util.Collections.enumeration;
import static java.util.Collections.list;
import static java.util.Collections.singletonList;
import static java.util.Optional.ofNullable;
import static java.util.function.Function.identity;
import static java.util.stream.Collectors.toList;
import static java.util.stream.Collectors.toMap;
public class BundleImpl implements Bundle {
private final File file;
private final ClassLoader loader;
private final long id;
private final BundleContextImpl context;
private final Version version;
private final String symbolicName;
private final Dictionary<String, String> headers;
private final File dataFileBase;
private final Collection<String> includedResources;
private final List<BundleRequirementImpl> requirements;
private final List<BundleCapabilityImpl> capabilities;
private final BundleRegistry registry;
private volatile BundleRevision bundleRevision;
private volatile BundleWiring bundleWiring;
private int state = Bundle.UNINSTALLED;
BundleImpl(final Manifest manifest, final File file, final BundleContextImpl context,
final Ripener.Configuration configuration, final long id,
final Collection<String> includedResources, final BundleRegistry registry) {
this.file = file;
this.dataFileBase = new File(configuration.getWorkDir(),
file == null ? Long.toString(System.identityHashCode(manifest)) : file.getName());
this.context = context;
this.id = id;
this.registry = registry;
this.loader = Thread.currentThread().getContextClassLoader();
this.includedResources = includedResources;
this.version = ofNullable(manifest.getMainAttributes().getValue(Constants.BUNDLE_VERSION))
.map(Version::new)
.orElse(Version.emptyVersion);
this.symbolicName = manifest.getMainAttributes().getValue(Constants.BUNDLE_SYMBOLICNAME);
this.headers = manifest.getMainAttributes().entrySet().stream()
.collect(Collector.of(
Hashtable::new,
(t, e) -> t.put(Attributes.Name.class.cast(e.getKey()).toString(), e.getValue().toString()),
(t1, t2) -> {
t1.putAll(t2);
return t1;
}));
final List<HeaderClause> requireClauses = Headers.parse(headers.get(Constants.REQUIRE_CAPABILITY));
Headers.coerceCapabilityClauses(requireClauses);
requirements = requireClauses.stream()
.flatMap(clause -> {
final String filterStr = clause.directives.get(Constants.FILTER_DIRECTIVE);
try {
final Filter filter = filterStr == null ? null : FrameworkUtil.createFilter(filterStr);
return clause.paths.stream()
.map(path -> new BundleRequirementImpl(
adapt(BundleRevision.class), path, clause.directives, clause.attributes, filter));
} catch (final InvalidSyntaxException e) {
throw new IllegalArgumentException(e);
}
})
.collect(toList());
final List<HeaderClause> provideClauses = Headers.parse(headers.get(Constants.PROVIDE_CAPABILITY));
Headers.coerceCapabilityClauses(provideClauses);
capabilities = provideClauses.stream()
.flatMap(clause -> clause.paths.stream()
.map(path -> new BundleCapabilityImpl(
adapt(BundleRevision.class), path, clause.directives, clause.attributes)))
.collect(toList());
}
ClassLoader getLoader() {
return loader;
}
List<BundleRequirementImpl> getRequirements() {
return requirements;
}
List<BundleCapabilityImpl> getCapabilities() {
return capabilities;
}
private Stream<BundleListener> allBundleListeners() {
return context.getRegistry().getBundles().values().stream()
.flatMap(it -> BundleContextImpl.class.cast(it.getBundle().getBundleContext()).getBundleListeners().stream());
}
void onStart() {
start();
final BundleEvent event = new BundleEvent(BundleEvent.STARTED, this);
allBundleListeners()
.forEach(listener -> listener.bundleChanged(event));
}
void onStop() {
stop();
final BundleEvent event = new BundleEvent(BundleEvent.STOPPED, this);
allBundleListeners().forEach(listener -> listener.bundleChanged(event));
}
@Override
public int getState() {
return state;
}
@Override
public void start(final int options) {
state = options;
}
@Override
public void start() {
start(Bundle.ACTIVE);
}
@Override
public void stop(final int options) {
state = options;
}
@Override
public void stop() {
stop(Bundle.UNINSTALLED);
}
@Override
public void update(final InputStream input) {
throw new UnsupportedOperationException();
}
@Override
public void update() {
throw new UnsupportedOperationException();
}
@Override
public void uninstall() {
throw new UnsupportedOperationException();
}
@Override
public Dictionary<String, String> getHeaders() {
return headers;
}
@Override
public long getBundleId() {
return id;
}
@Override
public String getLocation() {
return includedResources != null || file == null ? "" : file.getAbsolutePath();
}
@Override
public ServiceReference<?>[] getRegisteredServices() {
return context.getServices().getServices().stream()
.filter(it -> it.getReference().getBundle() == this)
.map(ServiceRegistration::getReference)
.toArray(ServiceReference[]::new);
}
@Override
public ServiceReference<?>[] getServicesInUse() {
return new ServiceReference[0];
}
@Override
public boolean hasPermission(final Object permission) {
return true;
}
@Override
public URL getResource(final String name) {
return loader.getResource(name);
}
@Override
public Dictionary<String, String> getHeaders(final String locale) {
return headers; // ignore the locale for now
}
@Override
public String getSymbolicName() {
return symbolicName;
}
@Override
public Class<?> loadClass(final String name) throws ClassNotFoundException {
return loader.loadClass(name);
}
@Override
public Enumeration<URL> getResources(final String name) throws IOException {
return loader.getResources(name);
}
@Override
public Enumeration<String> getEntryPaths(final String path) {
if (includedResources != null) {
return enumeration(includedResources.stream()
.filter(it -> it.startsWith(path))
.collect(toList()));
}
if (file.isDirectory()) {
final Path base = file.toPath().toAbsolutePath();
final Path subPath = new File(file, path == null ? "" : (path.startsWith("/") ? path.substring(1) : path)).toPath();
final Collection<String> paths = new ArrayList<>();
try {
Files.walkFileTree(subPath, new SimpleFileVisitor<Path>() {
@Override
public FileVisitResult visitFile(final Path file, final BasicFileAttributes attrs) throws IOException {
if (file.toAbsolutePath().toString().startsWith(base.toString())) {
paths.add(base.relativize(file).toString());
}
return super.visitFile(file, attrs);
}
});
} catch (final IOException e) {
throw new IllegalStateException(e);
}
return enumeration(paths);
}
try (final JarFile jar = new JarFile(file)) {
return enumeration(list(jar.entries()).stream()
.filter(it -> it.getName().startsWith(path))
.map(ZipEntry::getName)
.collect(toList()));
} catch (final IOException e) {
throw new IllegalArgumentException(e);
}
}
@Override
public URL getEntry(final String path) {
return loader.getResource(path);
}
@Override
public long getLastModified() {
return file == null ? -1 : file.lastModified();
}
@Override
public Enumeration<URL> findEntries(final String path, final String filePattern, final boolean recurse) {
final Filter filter = filePattern == null ?
null : context.createFilter("(filename=" + filePattern + ")");
final String prefix = path == null ? "" : (path.startsWith("/") ? path.substring(1) : path);
if (includedResources != null) {
if (!recurse) {
return enumeration(includedResources.stream()
.filter(it -> doFilterEntry(filter, prefix, it))
.map(loader::getResource)
.collect(toList()));
}
}
final File baseFile = new File(file, prefix);
final Path base = baseFile.toPath();
final Path filePath = this.file.toPath();
if (file.isDirectory()) {
if (!recurse) {
return enumeration(ofNullable(baseFile.listFiles())
.map(Stream::of)
.orElseGet(Stream::empty)
.filter(file -> doFilterEntry(filter, prefix, filePath.relativize(file.toPath()).toString()))
.map(f -> {
try {
return f.getAbsoluteFile().toURI().toURL();
} catch (final MalformedURLException e) {
throw new IllegalStateException(e);
}
})
.collect(toList()));
} else {
final Collection<URL> files = new ArrayList<>();
try {
Files.walkFileTree(base, new SimpleFileVisitor<Path>() {
@Override
public FileVisitResult visitFile(final Path file, final BasicFileAttributes attrs) throws IOException {
if (doFilterEntry(filter, prefix, filePath.relativize(file).toString())) {
files.add(file.toAbsolutePath().toUri().toURL());
}
return super.visitFile(file, attrs);
}
});
} catch (final IOException e) {
throw new IllegalStateException(e);
}
return enumeration(files);
}
} else {
try (final JarFile jar = new JarFile(file)) {
return enumeration(list(jar.entries()).stream().filter(it -> it.getName().startsWith(prefix))
.map(ZipEntry::getName).filter(name -> !name.endsWith("/")) // folders
.filter(name -> doFilterEntry(filter, prefix, name)).map(name -> {
try {
return new URL("jar", null, file.toURI().toURL().toExternalForm() + "!/" + name);
} catch (final MalformedURLException e) {
throw new IllegalArgumentException(e);
}
}).collect(toList()));
} catch (final IOException e) {
throw new IllegalArgumentException(e);
}
}
}
private boolean doFilterEntry(final Filter filter, final String prefix, final String name) {
final String path = name.replace(File.separatorChar, '/');
if (prefix != null && !path.startsWith(prefix)) {
return false;
}
if (filter == null) {
return true;
}
final Hashtable<String, Object> props = new Hashtable<>();
props.put("filename", path.substring(path.lastIndexOf('/') + 1));
return filter.matches(props);
}
@Override
public BundleContext getBundleContext() {
return context;
}
@Override
public Map<X509Certificate, List<X509Certificate>> getSignerCertificates(final int signersType) {
return null;
}
@Override
public Version getVersion() {
return version;
}
@Override
public <A> A adapt(final Class<A> type) {
if (BundleWiring.class == type) {
if (bundleWiring == null) {
synchronized (this) {
if (bundleWiring == null) {
bundleWiring = new BundleWiringImpl(this, registry);
}
}
}
return type.cast(bundleWiring);
}
if (BundleRevision.class == type) {
if (bundleRevision == null) {
synchronized (this) {
if (bundleRevision == null) {
bundleRevision = new BundleRevisionImpl(this);
}
}
}
return type.cast(bundleRevision);
}
if (BundleRevisions.class == type) {
return type.cast(new BundleRevisions() {
@Override
public Bundle getBundle() {
return BundleImpl.this;
}
@Override
public List<BundleRevision> getRevisions() {
return singletonList(bundleRevision);
}
});
}
if (BundleContext.class == type) {
return type.cast(context);
}
if (ServiceReferenceDTO[].class == type) {
return type.cast(Stream.of(getRegisteredServices())
.map(s -> {
final ServiceReferenceDTO dto = new ServiceReferenceDTO();
dto.bundle = getBundleId();
dto.id = Long.class.cast(s.getProperty(Constants.SERVICE_ID));
dto.properties = list(s.getProperties().keys()).stream()
.collect(toMap(identity(), s::getProperty));
dto.usingBundles = ofNullable(s.getUsingBundles())
.map(bundles -> Stream.of(bundles).mapToLong(Bundle::getBundleId).toArray())
.orElseGet(() -> new long[0]);
return dto;
})
.toArray(ServiceReferenceDTO[]::new));
}
return null;
}
@Override
public File getDataFile(final String filename) {
final File file = new File(dataFileBase, filename);
file.getParentFile().mkdirs();
return file;
}
@Override
public int compareTo(final Bundle o) {
return (int) (id - o.getBundleId());
}
@Override
public String toString() {
return "BundleImpl{file=" + file + ", id=" + id + '}';
}
public static class HeaderClause {
public final List<String> paths;
public final Map<String, String> directives;
public final Map<String, Object> attributes;
public final Map<String, String> types;
public HeaderClause(
final List<String> paths, final Map<String, String> dirs, final Map<String, Object> attrs,
final Map<String, String> types) {
this.paths = paths;
this.directives = dirs;
this.attributes = attrs;
this.types = types;
}
}
// taken from felix
private static class Headers {
private static final char EOF = (char) -1;
private static final int CLAUSE_START = 0;
private static final int PARAMETER_START = 1;
private static final int KEY = 2;
private static final int DIRECTIVE_OR_TYPEDATTRIBUTE = 4;
private static final int ARGUMENT = 8;
private static final int VALUE = 16;
private static List<HeaderClause> parse(final String header) {
final List<HeaderClause> clauses = new ArrayList<>();
if (header == null) {
return clauses;
}
HeaderClause clause = null;
String key = null;
Map targetMap = null;
int state = CLAUSE_START;
int currentPosition = 0;
int startPosition = 0;
int length = header.length();
boolean quoted = false;
boolean escaped = false;
char currentChar;
do {
currentChar = currentPosition >= length ? EOF : header.charAt(currentPosition);
switch (state) {
case CLAUSE_START:
clause = new HeaderClause(new ArrayList<>(), new HashMap<>(), new HashMap<>(), new HashMap<>());
clauses.add(clause);
// not needed to be called but "logically" needed: state = PARAMETER_START;
case PARAMETER_START:
startPosition = currentPosition;
state = KEY;
case KEY:
switch (currentChar) {
case ':':
case '=':
key = header.substring(startPosition, currentPosition).trim();
startPosition = currentPosition + 1;
targetMap = clause.attributes;
state = currentChar == ':' ? DIRECTIVE_OR_TYPEDATTRIBUTE : ARGUMENT;
break;
case EOF:
case ',':
case ';':
clause.paths.add(header.substring(startPosition, currentPosition).trim());
state = currentChar == ',' ? CLAUSE_START : PARAMETER_START;
break;
default:
break;
}
currentPosition++;
break;
case DIRECTIVE_OR_TYPEDATTRIBUTE:
if (currentChar == '=') {
if (startPosition != currentPosition) {
clause.types.put(key, header.substring(startPosition, currentPosition).trim());
} else {
targetMap = clause.directives;
}
state = ARGUMENT;
startPosition = currentPosition + 1;
}
currentPosition++;
break;
case ARGUMENT:
if (currentChar == '\"') {
quoted = true;
currentPosition++;
} else {
quoted = false;
}
if (!Character.isWhitespace(currentChar)) {
state = VALUE;
} else {
currentPosition++;
}
break;
case VALUE:
if (escaped) {
escaped = false;
} else {
if (currentChar == '\\') {
escaped = true;
} else if (quoted && currentChar == '\"') {
quoted = false;
} else if (!quoted) {
String value;
switch (currentChar) {
case EOF:
case ';':
case ',':
value = header.substring(startPosition, currentPosition).trim();
if (value.startsWith("\"") && value.endsWith("\"")) {
value = value.substring(1, value.length() - 1);
}
if (targetMap.put(key, value) != null) {
throw new IllegalArgumentException(
"Duplicate '" + key + "' in: " + header);
}
state = currentChar == ';' ? PARAMETER_START : CLAUSE_START;
break;
default:
break;
}
}
}
currentPosition++;
break;
default:
break;
}
} while (currentChar != EOF);
if (state > PARAMETER_START) {
throw new IllegalArgumentException("Unable to parse header: " + header);
}
return clauses;
}
public static List<String> parseDelimitedString(String value, String delim, boolean trim) {
if (value == null) {
value = "";
}
List<String> list = new ArrayList<String>();
int CHAR = 1;
int DELIMITER = 2;
int STARTQUOTE = 4;
int ENDQUOTE = 8;
StringBuilder sb = new StringBuilder();
int expecting = (CHAR | DELIMITER | STARTQUOTE);
boolean isEscaped = false;
for (int i = 0; i < value.length(); i++) {
char c = value.charAt(i);
boolean isDelimiter = (delim.indexOf(c) >= 0);
if (!isEscaped && (c == '\\')) {
isEscaped = true;
continue;
}
if (isEscaped) {
sb.append(c);
} else if (isDelimiter && ((expecting & DELIMITER) > 0)) {
if (trim) {
list.add(sb.toString().trim());
} else {
list.add(sb.toString());
}
sb.delete(0, sb.length());
expecting = (CHAR | DELIMITER | STARTQUOTE);
} else if ((c == '"') && ((expecting & STARTQUOTE) > 0)) {
sb.append(c);
expecting = CHAR | ENDQUOTE;
} else if ((c == '"') && ((expecting & ENDQUOTE) > 0)) {
sb.append(c);
expecting = (CHAR | STARTQUOTE | DELIMITER);
} else if ((expecting & CHAR) > 0) {
sb.append(c);
} else {
throw new IllegalArgumentException("Invalid delimited string: " + value);
}
isEscaped = false;
}
if (sb.length() > 0) {
if (trim) {
list.add(sb.toString().trim());
} else {
list.add(sb.toString());
}
}
return list;
}
private static void coerceCapabilityClauses(final List<HeaderClause> clauses) {
clauses.forEach(clause -> clause.types.forEach((key, type) -> {
if (!type.equals("String")) {
if (type.equals("Double")) {
clause.attributes.put(
key,
new Double(clause.attributes.get(key).toString().trim()));
} else if (type.equals("Version")) {
clause.attributes.put(
key,
new Version(clause.attributes.get(key).toString().trim()));
} else if (type.equals("Long")) {
clause.attributes.put(
key,
Long.valueOf(clause.attributes.get(key).toString().trim()));
} else if (type.startsWith("List")) {
final int startIdx = type.indexOf('<');
final int endIdx = type.indexOf('>');
if ((startIdx > 0 && endIdx <= startIdx) || (startIdx < 0 && endIdx > 0)) {
throw new IllegalArgumentException("Invalid Provide-Capability attribute list type for '"
+ key + "' : " + type);
}
final String listType;
if (endIdx > startIdx) {
listType = type.substring(startIdx + 1, endIdx).trim();
} else {
listType = "String";
}
final List<String> tokens = parseDelimitedString(
clause.attributes.get(key).toString(), ",", false);
clause.attributes.put(key, tokens.stream()
.map(token -> {
switch (listType) {
case "String":
return token;
case "Double":
return Double.valueOf(token.trim());
case "Version":
return new Version(token.trim());
case "Long":
return Long.valueOf(token.trim());
default:
throw new IllegalArgumentException(
"Unknown Provide-Capability attribute list type for '"
+ key + "' : " + type);
}
}).collect(toList()));
} else {
throw new IllegalArgumentException("Unknown Provide-Capability attribute type for '" +
key + "' : " + type);
}
}
}));
}
}
}