/*
* ====================
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
*
* Copyright 2008-2009 Sun Microsystems, Inc. All rights reserved.
*
* The contents of this file are subject to the terms of the Common Development
* and Distribution License("CDDL") (the "License"). You may not use this file
* except in compliance with the License.
*
* You can obtain a copy of the License at
* http://opensource.org/licenses/cddl1.php
* See the License for the specific language governing permissions and limitations
* under the License.
*
* When distributing the Covered Code, include this CDDL Header Notice in each file
* and include the License file at http://opensource.org/licenses/cddl1.php.
* If applicable, add the following below this CDDL Header, with the fields
* enclosed by brackets [] replaced by your own identifying information:
* "Portions Copyrighted [year] [name of copyright owner]"
* ====================
* Portions Copyrighted 2010-2014 ForgeRock AS.
* Portions Copyrighted 2010-2014 Tirasa.
*/
package org.identityconnectors.framework.impl.api.local;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.lang.annotation.Annotation;
import java.net.URISyntaxException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Properties;
import java.util.Random;
import java.util.Set;
import java.util.StringTokenizer;
import java.util.TreeMap;
import java.util.jar.JarEntry;
import java.util.jar.JarInputStream;
import java.util.jar.Manifest;
import org.identityconnectors.common.CollectionUtil;
import org.identityconnectors.common.IOUtil;
import org.identityconnectors.common.ReflectionUtil;
import org.identityconnectors.common.logging.Log;
import org.identityconnectors.framework.api.APIConfiguration;
import org.identityconnectors.framework.api.ConnectorInfo;
import org.identityconnectors.framework.api.ConnectorInfoManager;
import org.identityconnectors.framework.api.ConnectorKey;
import org.identityconnectors.framework.common.FrameworkUtil;
import org.identityconnectors.framework.common.exceptions.ConfigurationException;
import org.identityconnectors.framework.common.exceptions.ConnectorException;
import org.identityconnectors.framework.impl.api.APIConfigurationImpl;
import org.identityconnectors.framework.impl.api.ConnectorMessagesImpl;
import org.identityconnectors.framework.spi.Configuration;
import org.identityconnectors.framework.spi.Connector;
import org.identityconnectors.framework.spi.ConnectorClass;
import org.identityconnectors.framework.spi.PoolableConnector;
public class LocalConnectorInfoManagerImpl implements ConnectorInfoManager {
private static final Log LOG = Log.getLog(LocalConnectorInfoManagerImpl.class);
private List<ConnectorInfo> connectorInfos;
public LocalConnectorInfoManagerImpl(final List<URL> bundleURLs,
final ClassLoader bundleParentClassLoader) throws ConfigurationException {
final List<WorkingBundleInfo> workingInfo = expandBundles(bundleURLs);
WorkingBundleInfo.resolve(workingInfo);
connectorInfos = createConnectorInfo(workingInfo, bundleParentClassLoader);
}
/**
* First pass - expand bundles as needed. populates originalURL, parsedManifest, libContents, and topLevelContents
*/
private static List<WorkingBundleInfo> expandBundles(final List<URL> bundleURLs)
throws ConfigurationException {
final List<WorkingBundleInfo> rv = new ArrayList<WorkingBundleInfo>();
for (URL url : bundleURLs) {
WorkingBundleInfo info = null;
try {
if ("file".equals(url.getProtocol())) {
final File file = new File(url.toURI());
if (file.isDirectory()) {
info = processDirectory(file);
}
}
if (info == null) {
info = processURL(url, true);
}
} catch (URISyntaxException e) {
throw new ConfigurationException("Invalid bundleURL: " + url.toExternalForm(), e);
}
rv.add(info);
}
return rv;
}
private static WorkingBundleInfo processDirectory(final File dir) throws ConfigurationException {
final WorkingBundleInfo info = new WorkingBundleInfo(dir.getAbsolutePath());
try {
// easy case - nothing needs to be copied
final File manifest = new File(dir, "META-INF/MANIFEST.MF");
InputStream in = null;
try {
in = new FileInputStream(manifest);
Manifest rawManifest = new Manifest(in);
ConnectorBundleManifestParser parser =
new ConnectorBundleManifestParser(info.getOriginalLocation(), rawManifest);
info.setManifest(parser.parse());
} finally {
IOUtil.quietClose(in);
}
info.getImmediateClassPath().add(dir.toURI().toURL());
final List<String> bundleContents = listBundleContents(dir);
info.getImmediateBundleContents().addAll(bundleContents);
final File libDir = new File(dir, "lib");
if (libDir.exists()) {
final List<URL> libURLs = BundleLibSorter.getSortedURLs(libDir);
for (URL lib : libURLs) {
info.getEmbeddedBundles().add(processURL(lib, false));
}
}
final File nativeDir = new File(dir, "native");
if (nativeDir.exists()) {
for (File file : BundleLibSorter.getSortedFiles(nativeDir)) {
if (file.isFile()) {
info.getImmediateNativeLibraries().put(file.getName(),
file.getAbsolutePath());
}
}
}
} catch (IOException e) {
throw new ConfigurationException(e);
}
return info;
}
/**
* Lists the contents of a directory and its contents. Result will be given as a list of forward-slash separated
* relative paths
*/
private static List<String> listBundleContents(final File dir) {
final List<String> rv = new ArrayList<String>();
for (File file : dir.listFiles()) {
listBundleContents2("", file, rv);
}
return rv;
}
private static void listBundleContents2(final String prefix, final File file,
final List<String> result) {
String path = prefix + file.getName();
result.add(path);
if (file.isDirectory()) {
for (File sub : file.listFiles()) {
listBundleContents2(path + "/", sub, result);
}
}
}
private static WorkingBundleInfo processURL(final URL url, final boolean topLevel)
throws ConfigurationException {
final WorkingBundleInfo info = new WorkingBundleInfo(url.toString());
final BundleTempDirectory tempDir = new BundleTempDirectory();
try {
JarInputStream stream = null;
if ("file".equals(url.getProtocol())) {
info.getImmediateClassPath().add(url);
} else {
// if we're in a WAR, this might not be the kind of URL
// that URLClassLoader can handle, so copy it as well
InputStream stream2 = null;
try {
stream2 = url.openStream();
info.getImmediateClassPath().add(
tempDir.copyStreamToFile(stream2).toURI().toURL());
} finally {
IOUtil.quietClose(stream2);
}
}
final TreeMap<String, URL> libURLs = new TreeMap<String, URL>();
try {
stream = new JarInputStream(url.openStream());
// only parse the manifest for top-level bundles
// other bundles may not be bundles - they might be
// jars instead
if (topLevel) {
final Manifest rawManifest = stream.getManifest();
final ConnectorBundleManifestParser parser =
new ConnectorBundleManifestParser(info.getOriginalLocation(),
rawManifest);
info.setManifest(parser.parse());
}
JarEntry entry = null;
while ((entry = stream.getNextJarEntry()) != null) {
final String name = entry.getName();
info.getImmediateBundleContents().add(name);
if (name.startsWith("lib/") && !entry.isDirectory()) {
final String localName = name.substring("lib/".length());
final URL tempurl = tempDir.copyStreamToFile(stream, name).toURI().toURL();
libURLs.put(localName, tempurl);
}
if (name.startsWith("native/") && !entry.isDirectory()) {
final String localName = name.substring("native/".length());
// It is important that the name of the native library
// be preserved!
final File tempFile = tempDir.copyStreamToFile(stream, name);
info.getImmediateNativeLibraries().put(localName,
tempFile.getAbsolutePath());
}
}
} finally {
IOUtil.quietClose(stream);
}
for (URL lib : libURLs.values()) {
info.getEmbeddedBundles().add(processURL(lib, false));
}
} catch (IOException e) {
throw new ConfigurationException(e);
}
return info;
}
/**
* Final pass - create connector infos
*/
private static List<ConnectorInfo> createConnectorInfo(
final Collection<WorkingBundleInfo> parsed, final ClassLoader bundleParentClassLoader)
throws ConfigurationException {
final List<ConnectorInfo> rv = new ArrayList<ConnectorInfo>();
for (WorkingBundleInfo bundleInfo : parsed) {
final ClassLoader loader =
new BundleClassLoader(bundleInfo.getEffectiveClassPath(), bundleInfo
.getEffectiveNativeLibraries(), bundleParentClassLoader);
for (String name : bundleInfo.getImmediateBundleContents()) {
Class<?> connectorClass = null;
ConnectorClass options = null;
if (name.endsWith(".class")) {
String className = name.substring(0, name.length() - ".class".length());
className = className.replace('/', '.');
try {
connectorClass = loader.loadClass(className);
options = connectorClass.getAnnotation(ConnectorClass.class);
} catch (Throwable e) {
// probe for the class. this might not be an error since
// it might be from a bundle
// fragment ( a bundle only included by other bundles ).
// However, we should definitely warn
LOG.info(LOG.isOk() ?
e : null,
"Unable to load class {0} from bundle {1}. Class will be ignored and will not be listed in list of connectors.",
className, bundleInfo.getOriginalLocation());
}
if (connectorClass != null && options == null) {
for (Annotation annotation: connectorClass.getAnnotations()) {
if (ConnectorClass.class.getName().equals(annotation.annotationType().getName())) {
// Same class name as the annotation we are looking for. But the previous code haven't found it.
// So it looks like the annotation on this class is actually the correct one but it is loaded
// by wrong classloader.
// Note: This error is very difficult to diagnose. Therefore we are explicitly checking for it here.
throw new ConfigurationException("Class "+connectorClass.getName()+" has ConnectorClass annotation but it looks like it is " +
"loaded by a wrong classloader. Maybe the connector bundle contains the connector frameworks JAR? (it should NOT contain it).");
}
}
}
}
if (connectorClass != null && options != null) {
if (!Connector.class.isAssignableFrom(connectorClass)) {
throw new ConfigurationException("Class " + connectorClass
+ " does not implement " + Connector.class.getName());
}
final LocalConnectorInfoImpl info = new LocalConnectorInfoImpl();
info.setConnectorClass(connectorClass.asSubclass(Connector.class));
try {
info.setConnectorConfigurationClass(options.configurationClass());
info.setConnectorDisplayNameKey(options.displayNameKey());
info.setConnectorCategoryKey(options.categoryKey());
info.setConnectorKey(new ConnectorKey(bundleInfo.getManifest().getBundleName(),
bundleInfo.getManifest().getBundleVersion(), connectorClass.getName()));
final ConnectorMessagesImpl messages =
loadMessageCatalog(bundleInfo.getEffectiveContents(), loader, info
.getConnectorClass());
info.setMessages(messages);
info.setDefaultAPIConfiguration(createDefaultAPIConfiguration(info));
rv.add(info);
LOG.info("Add ConnectorInfo {0} to Local Connector Info Manager from {1}",
info.getConnectorKey(), bundleInfo.getOriginalLocation());
} catch (final NoClassDefFoundError e) {
LOG.info(LOG.isOk() ?
e : null,
"Unable to load configuration class of connector {0} from bundle {1}. Class will be ignored and will not be listed in list of connectors.",
connectorClass, bundleInfo.getOriginalLocation());
} catch (final TypeNotPresentException e) {
LOG.info(LOG.isOk() ?
e : null,
"Unable to load configuration class of connector {0} from bundle {1}. Class will be ignored and will not be listed in list of connectors.",
connectorClass, bundleInfo.getOriginalLocation());
}
}
}
}
return rv;
}
/**
* Create an instance of the {@link APIConfiguration} object to setup the framework etc..
*/
public static APIConfigurationImpl createDefaultAPIConfiguration(
final LocalConnectorInfoImpl localInfo) {
// setup classloader since we are going to construct the config bean
ThreadClassLoaderManager.getInstance().pushClassLoader(
localInfo.getConnectorClass().getClassLoader());
try {
final Class<? extends Connector> connectorClass = localInfo.getConnectorClass();
final APIConfigurationImpl rv = new APIConfigurationImpl();
final Configuration config = localInfo.getConnectorConfigurationClass().newInstance();
final boolean pooling = PoolableConnector.class.isAssignableFrom(connectorClass);
rv.setConnectorPoolingSupported(pooling);
rv.setConfigurationProperties(JavaClassProperties.createConfigurationProperties(config));
rv.setConnectorInfo(localInfo);
rv.setSupportedOperations(FrameworkUtil.getDefaultSupportedOperations(connectorClass));
return rv;
} catch (Exception e) {
throw ConnectorException.wrap(e);
} finally {
ThreadClassLoaderManager.getInstance().popClassLoader();
}
}
public static ConnectorMessagesImpl loadMessageCatalog(final Set<String> bundleContents,
final ClassLoader loader,
final Class<? extends Connector> connector)
throws ConfigurationException {
try {
final String[] prefixes = getBundleNamePrefixes(connector);
final String suffix = ".properties";
final ConnectorMessagesImpl rv = new ConnectorMessagesImpl();
// iterate last to first so that first one wins
for (int i = prefixes.length - 1; i >= 0; i--) {
String prefix = prefixes[i];
for (String path : bundleContents) {
if (path.startsWith(prefix)) {
String localeStr = path.substring(prefix.length());
if (localeStr.endsWith(suffix)) {
localeStr =
localeStr.substring(0, localeStr.length() - suffix.length());
final Locale locale = parseLocale(localeStr);
Properties properties = IOUtil.getResourceAsProperties(loader, path);
// get or create map
Map<String, String> map = rv.getCatalogs().get(locale);
if (map == null) {
map = new HashMap<String, String>();
rv.getCatalogs().put(locale, map);
}
// merge properties into map, overwriting
// any that already exist
map.putAll(CollectionUtil.newMap(properties));
}
}
}
}
return rv;
} catch (RuntimeException e) {
throw e;
} catch (Exception e) {
throw new ConfigurationException(e);
}
}
private static Locale parseLocale(final String str) {
String lang = null;
String country = null;
String variant = null;
final StringTokenizer tok = new StringTokenizer(str, "_", false);
if (tok.hasMoreTokens()) {
lang = tok.nextToken();
}
if (tok.hasMoreTokens()) {
country = tok.nextToken();
}
if (tok.hasMoreTokens()) {
variant = tok.nextToken();
}
if (variant != null) {
return new Locale(lang, country, variant);
} else if (country != null) {
return new Locale(lang, country);
} else if (lang != null) {
return new Locale(lang);
} else {
return new Locale("");
}
}
private static String[] getBundleNamePrefixes(final Class<? extends Connector> connector) {
// figure out the message catalog..
final ConnectorClass configOpts = connector.getAnnotation(ConnectorClass.class);
String[] paths = null;
if (configOpts != null) {
paths = configOpts.messageCatalogPaths();
}
if (paths == null || paths.length == 0) {
final String pkage = ReflectionUtil.getPackage(connector);
final String messageCatalog = pkage + ".Messages";
paths = new String[]{messageCatalog};
}
for (int i = 0; i < paths.length; i++) {
paths[i] = paths[i].replace('.', '/');
}
return paths;
}
public ConnectorInfo findConnectorInfo(final ConnectorKey key) {
for (ConnectorInfo info : connectorInfos) {
if (info.getConnectorKey().equals(key)) {
return info;
}
}
return null;
}
public List<ConnectorInfo> getConnectorInfos() {
return Collections.unmodifiableList(connectorInfos);
}
private static final class BundleTempDirectory {
private final Random _random = new Random(System.currentTimeMillis());
private File _bundleTempDir;
public File copyStreamToFile(final InputStream stream) throws IOException {
final File bundleDir = getBundleTempDir();
File candidate;
do {
candidate = new File(bundleDir, "file-" + nextRandom());
} while (!candidate.createNewFile());
candidate.deleteOnExit();
copyStream(stream, candidate);
return candidate;
}
public File copyStreamToFile(final InputStream stream, final String name)
throws IOException {
final File bundleDir = getBundleTempDir();
final File newFile = new File(bundleDir, name);
if (newFile.exists()) {
throw new IOException("File " + newFile + " already exists");
}
File parent = newFile.getParentFile();
if (!parent.exists() && !parent.mkdirs()) {
throw new IOException("Could not create directory " + parent);
}
while (!parent.equals(bundleDir)) {
parent.deleteOnExit();
parent = parent.getParentFile();
}
newFile.deleteOnExit();
copyStream(stream, newFile);
return newFile;
}
private void copyStream(final InputStream stream, final File toFile) throws IOException {
final FileOutputStream out = new FileOutputStream(toFile);
try {
IOUtil.copyFile(stream, out);
} finally {
out.close();
}
}
private File getBundleTempDir() throws IOException {
if (_bundleTempDir != null) {
return _bundleTempDir;
}
final File tempDir = new File(System.getProperty("java.io.tmpdir"));
if (!tempDir.exists()) {
throw new IOException("Temporary directory " + tempDir + " does not exist");
}
File candidate;
do {
candidate = new File(tempDir, "bundle-" + nextRandom());
} while (!candidate.mkdir());
candidate.deleteOnExit();
_bundleTempDir = candidate;
return candidate;
}
private int nextRandom() {
return _random.nextInt() & 0x7fffffff; // Want only positive numbers.
}
}
}