/*
* Licensed to 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 org.apache.openejb.arquillian.openejb;
import org.apache.openejb.loader.SystemInstance;
import org.apache.openejb.util.Enumerator;
import org.apache.openejb.util.URLs;
import org.apache.openejb.util.reflection.Reflections;
import org.jboss.shrinkwrap.api.Archive;
import org.jboss.shrinkwrap.api.ArchivePaths;
import org.jboss.shrinkwrap.api.Node;
import org.jboss.shrinkwrap.api.ShrinkWrap;
import org.jboss.shrinkwrap.api.asset.Asset;
import org.jboss.shrinkwrap.api.asset.ClassLoaderAsset;
import org.jboss.shrinkwrap.api.asset.FileAsset;
import org.jboss.shrinkwrap.api.asset.UrlAsset;
import org.jboss.shrinkwrap.api.spec.JavaArchive;
import org.jboss.shrinkwrap.api.spec.WebArchive;
import org.jboss.shrinkwrap.impl.base.filter.IncludeRegExpPaths;
import javax.enterprise.inject.spi.Extension;
import java.io.Closeable;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Field;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLConnection;
import java.net.URLStreamHandler;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Hashtable;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import static java.util.Arrays.asList;
import static org.apache.openejb.arquillian.openejb.reflection.Assets.get;
public class SWClassLoader extends ClassLoader implements Closeable {
static {
try {
final Field handler = URL.class.getDeclaredField("handlers");
handler.setAccessible(true);
((Hashtable<String, URLStreamHandler>) handler.get(null)).put("archive", new ArchiveStreamHandler());
} catch (final Exception e) {
// no-op
}
}
private final Collection<Archive<?>> archives;
private final Collection<Closeable> closeables = new ArrayList<Closeable>();
public SWClassLoader(final ClassLoader parent, final Archive<?>... ar) {
super(parent);
this.archives = new ArrayList<>(asList(ar));
for (final Archive<?> a : ar) {
ArchiveStreamHandler.set(a, closeables);
final boolean isWar = WebArchive.class.isInstance(a);
if (isWar) { // add dependencies - file and url - to be able to lookup them. ArchiveAssets are provided normally
for (final Node n : a.getContent(new IncludeRegExpPaths("/WEB-INF/lib/.*\\.jar")).values()) {
final Asset asset = n.getAsset();
if (FileAsset.class.isInstance(asset)) {
final JavaArchive jar = ShrinkWrap.createFromZipFile(JavaArchive.class, get(File.class, "file", asset));
this.archives.add(jar);
ArchiveStreamHandler.set(jar, closeables);
} else if (UrlAsset.class.isInstance(asset)) {
final JavaArchive jar = ShrinkWrap.createFromZipFile(JavaArchive.class, URLs.toFile(get(URL.class, "url", asset)));
this.archives.add(jar);
ArchiveStreamHandler.set(jar, closeables);
}
}
}
}
}
@Override
public Enumeration<URL> getResources(final String name) throws IOException {
if (name == null) {
return super.getResources(null);
}
final boolean cdiExtensions = name.startsWith("META-INF/services/" + Extension.class.getName());
if (cdiExtensions || !name.contains("META-INF/services/javax")) {
final List<Archive<?>> node = findNodes(name);
if (!node.isEmpty()) {
final List<URL> urls = new ArrayList<>();
for (final Archive<?> i : node) {
urls.add(new URL(null, "archive:" + i.getName() + (!name.startsWith("/") ? "/" : "") + name, new ArchiveStreamHandler()));
}
if (cdiExtensions && !"true".equalsIgnoreCase(SystemInstance.get().getProperty("openejb.arquillian.cdi.extension.skip-externals", "false"))) {
addContainerExtensions(name, urls);
}
return enumerator(urls);
}
if (cdiExtensions) {
if ("true".equalsIgnoreCase(SystemInstance.get().getProperty("openejb.arquillian.cdi.extension.skip-externals", "false"))) {
return enumerator(Collections.<URL>emptyList());
}
return enumerator(addContainerExtensions(name, new ArrayList<URL>(2)));
}
}
return super.getResources(name);
}
private List<URL> addContainerExtensions(final String name, final List<URL> urls) throws IOException {
final Collection<URL> containerExtensions = Collections.list(getParent().getResources(name));
for (final URL u : containerExtensions) {
final String externalForm = u.toExternalForm();
if (externalForm.contains("myfaces-impl") || externalForm.contains("bval-jsr")) {
urls.add(u);
}
}
return urls;
}
@Override
protected Enumeration<URL> findResources(final String name) throws IOException {
final List<Archive<?>> node = findNodes(name);
if (!node.isEmpty()) {
final List<URL> urls = new ArrayList<>();
for (final Archive<?> i : node) {
urls.add(new URL(null, "archive:" + i.getName() + (!name.startsWith("/") ? "/" : "") + name, new ArchiveStreamHandler()));
}
return enumerator(urls);
}
return super.findResources(name);
}
public URL getWebResource(final String name) {
for (final Archive<?> a : archives) {
if (!WebArchive.class.isInstance(a)) {
continue;
}
final Node node = a.get(name);
if (node != null) {
try {
return new URL(null, "archive:" + a.getName() + (!name.startsWith("/") ? "/" : "") + name, new ArchiveStreamHandler());
} catch (final MalformedURLException e) {
// no-op
}
}
}
return null;
}
public LinkedList<Archive<?>> findNodes(final String name) {
final LinkedList<Archive<?>> items = new LinkedList<>();
for (final Archive<?> a : archives) {
final boolean isWar = WebArchive.class.isInstance(a);
final Node node = a.get(ArchivePaths.create((isWar ? "/WEB-INF/classes/" : "") + name));
if (node != null) {
items.add(a);
}
}
return items;
}
private static Enumeration<URL> enumerator(final List<URL> urls) {
return new Enumerator(urls);
}
@Override
protected URL findResource(final String name) {
final LinkedList<Archive<?>> node = findNodes(name);
if (!node.isEmpty()) {
final Archive<?> i = node.getLast();
try {
return new URL(null, "archive:" + i.getName() + (!name.startsWith("/") ? "/" : "") + name, new ArchiveStreamHandler());
} catch (final MalformedURLException e) {
throw new IllegalArgumentException(e);
}
}
return super.findResource(name);
}
private static class ArchiveStreamHandler extends URLStreamHandler {
public static final Map<String, Archive<?>> archives = new HashMap<String, Archive<?>>();
public static final Map<String, Collection<Closeable>> closeables = new HashMap<String, Collection<Closeable>>();
public static void set(final Archive<?> ar, final Collection<Closeable> c) {
final String archiveName = ar.getName();
archives.put(archiveName, ar);
closeables.put(archiveName, c);
}
public static void reset(final String archiveName) {
archives.remove(archiveName);
closeables.remove(archiveName);
}
@Override
protected URLConnection openConnection(final URL u) throws IOException {
final String arName = key(u);
final Archive<?> archive = archives.get(arName);
final String path = path(archive.getName(), WebArchive.class.isInstance(archive) ? "/WEB-INF/classes/" : "", u);
Node node = archive.get(path);
if (node == null) {
node = archive.get(path(archive.getName(), "", u)); // web resources
if (node == null) {
throw new IOException(u.toExternalForm() + " not found");
}
}
final Asset asset = node.getAsset();
if (UrlAsset.class.isInstance(asset)) {
return URL.class.cast(Reflections.get(asset, "url")).openConnection();
} else if (FileAsset.class.isInstance(asset)) {
return File.class.cast(Reflections.get(asset, "file")).toURI().toURL().openConnection();
} else if (ClassLoaderAsset.class.isInstance(asset)) {
return ClassLoader.class.cast(Reflections.get(asset, "classLoader")).getResource(String.class.cast(Reflections.get(asset, "resourceName"))).openConnection();
}
return new URLConnection(u) {
@Override
public void connect() throws IOException {
// no-op
}
@Override
public InputStream getInputStream() throws IOException {
final InputStream input = asset.openStream();
final Collection<Closeable> c = closeables.get(arName);
c.add(input);
return input;
}
};
}
private static String path(final String arName, final String prefix, final URL url) {
final String p = url.getPath();
final String out = p.substring(arName.length(), p.length());
if (prefix.endsWith("/") && out.startsWith("/")) {
return prefix + out.substring(1);
}
return prefix + out;
}
private static String key(final URL url) {
final String p = url.getPath();
if (p == null) {
return null;
}
final int endIndex = p.indexOf('/');
if (endIndex >= 0) {
return p.substring(0, endIndex);
}
return p;
}
}
@Override
public void close() throws IOException {
for (final Archive<?> a : archives) {
ArchiveStreamHandler.reset(a.getName());
}
for (final Closeable cl : closeables) {
try {
cl.close();
} catch (final IOException e) {
// no-op
}
}
}
// to let frameworks using TCCL use the archive directly
public Collection<Archive<?>> getArchives() {
return archives;
}
}