package org.jtheque.modules.impl;
import org.jtheque.core.Core;
import org.jtheque.core.Folders;
import org.jtheque.core.utils.OSGiUtils;
import org.jtheque.errors.ErrorService;
import org.jtheque.errors.Errors;
import org.jtheque.i18n.I18NResourceFactory;
import org.jtheque.i18n.LanguageService;
import org.jtheque.modules.Module;
import org.jtheque.modules.ModuleException;
import org.jtheque.modules.ModuleException.ModuleOperation;
import org.jtheque.modules.ModuleState;
import org.jtheque.resources.Resource;
import org.jtheque.resources.ResourceService;
import org.jtheque.utils.StringUtils;
import org.jtheque.utils.ThreadUtils;
import org.jtheque.utils.annotations.Immutable;
import org.jtheque.utils.annotations.NotThreadSafe;
import org.jtheque.utils.bean.Version;
import org.jtheque.utils.collections.ArrayUtils;
import org.jtheque.utils.collections.CollectionUtils;
import org.jtheque.utils.io.FileUtils;
import org.jtheque.xml.utils.XML;
import org.jtheque.xml.utils.XMLException;
import org.jtheque.xml.utils.XMLOverReader;
import org.osgi.framework.Bundle;
import org.osgi.framework.BundleContext;
import org.osgi.framework.BundleException;
import org.springframework.osgi.context.BundleContextAware;
import java.io.File;
import java.io.FileFilter;
import java.io.IOException;
import java.io.InputStream;
import java.util.Collection;
import java.util.Dictionary;
import java.util.List;
import java.util.Locale;
import java.util.concurrent.Callable;
import java.util.concurrent.CompletionService;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorCompletionService;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.jar.JarFile;
import java.util.regex.Pattern;
import java.util.zip.ZipEntry;
/*
* Copyright JTheque (Baptiste Wicht)
*
* 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
*
* 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.
*/
/**
* A loader for the modules.
*
* @author Baptiste Wicht
*/
@NotThreadSafe
public final class ModuleLoader implements BundleContextAware {
private static final Pattern COMMA_DELIMITER_PATTERN = Pattern.compile(";");
private static final String[] EMPTY_ARRAY = new String[0];
private BundleContext bundleContext;
@javax.annotation.Resource
private ResourceService resourceService;
@javax.annotation.Resource
private LanguageService languageService;
@javax.annotation.Resource
private Core core;
private final ModuleServiceImpl moduleService;
/**
* Construct a new ModuleLoader.
*
* @param moduleService The module service.
*/
public ModuleLoader(ModuleServiceImpl moduleService) {
super();
this.moduleService = moduleService;
}
@Override
public void setBundleContext(BundleContext bundleContext) {
this.bundleContext = bundleContext;
}
/**
* Load the modules.
*
* @return All the loaded modules.
*/
public Collection<Module> loadModules() {
File[] files = Folders.getModulesFolder().listFiles(new ModuleFilter());
return isLoadingConcurrent() ? loadInParallel(files) : loadSequentially(files);
}
/**
* Indicate if we must make the loading concurrent.
*
* @return {@code true} if the loading is concurrent otherwise {@code false}.
*/
private static boolean isLoadingConcurrent() {
String property = System.getProperty("jtheque.concurrent.load");
return StringUtils.isNotEmpty(property) && "true".equalsIgnoreCase(property);
}
/**
* Load all the modules from the given files in parallel (using one thread per processor).
*
* @param files The files to load the modules from.
*
* @return A Collection containing all the loaded modules.
*/
@SuppressWarnings({"ForLoopReplaceableByForEach"})
private Collection<Module> loadInParallel(File[] files) {
ExecutorService loadersPool = Executors.newFixedThreadPool(2 * ThreadUtils.processors());
CompletionService<Module> completionService = new ExecutorCompletionService<Module>(loadersPool);
for (File file : files) {
completionService.submit(new ModuleLoaderTask(file));
}
List<Module> modules = CollectionUtils.newList(files.length);
try {
for (int i = 0; i < files.length; i++) {
modules.add(completionService.take().get());
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new RuntimeException(e);
} catch (ExecutionException e) {
throw new RuntimeException(e);
}
loadersPool.shutdown();
return modules;
}
/**
* Load all the modules from the given files sequentially.
*
* @param files The files to load the modules from.
*
* @return A Collection containing all the loaded modules.
*/
private Collection<Module> loadSequentially(File[] files) {
List<Module> modules = CollectionUtils.newList(files.length);
for (File file : files) {
try {
modules.add(installModule(file));
} catch (ModuleException e) {
//Do not rethrow the exception to try to install the others module.
OSGiUtils.getService(bundleContext, ErrorService.class).addError(Errors.newError(e));
}
}
return modules;
}
/**
* Install the module.
*
* @param file The file to the module to installFromRepository.
*
* @return The installed module.
*
* @throws org.jtheque.modules.ModuleException
* If an error occurs during module installation.
*/
public Module installModule(File file) throws ModuleException {
Builder builder = new Builder();
ModuleResources resources = null;
try {
//Read the config file of the module
resources = readConfig(file);
//Install the bundle
Bundle bundle = bundleContext.installBundle("file:" + file.getAbsolutePath());
builder.setBundle(bundle);
//Get informations from manifest
readManifestInformations(builder, bundle);
} catch (BundleException e) {
throw new ModuleException("error.module.config", e, ModuleOperation.INSTALL);
} catch (IOException e) {
throw new ModuleException("error.module.config", ModuleOperation.INSTALL);
}
Module module = builder.build();
loadI18NResources(module, resources);
moduleService.setResources(module, resources);
return module;
}
/**
* Read the config of the module.
*
* @param file The file of the module.
*
* @return The module resources.
*
* @throws IOException If an error occurs during Jar File reading.
* @throws org.jtheque.modules.ModuleException
* If the config cannot be read.
*/
private ModuleResources readConfig(File file) throws IOException, ModuleException {
JarFile jarFile = null;
try {
jarFile = new JarFile(file);
ZipEntry configEntry = jarFile.getEntry("module.xml");
if (configEntry == null) {
return new ModuleResources(
CollectionUtils.<ImageResource>emptyList(),
CollectionUtils.<I18NResource>emptyList(),
CollectionUtils.<Resource>emptyList());
}
ModuleResources resources = importConfig(jarFile.getInputStream(configEntry));
//Install necessary resources before installing the bundle
for (Resource resource : resources.getResources()) {
if (resource != null) {
resourceService.installResource(resource);
}
}
return resources;
} finally {
if (jarFile != null) {
jarFile.close();
}
}
}
/**
* Import the configuration of the module from the module config XML file.
*
* @param stream The stream to the file.
*
* @return The ModuleResources of the module.
*
* @throws org.jtheque.modules.ModuleException
* If the config cannot be read properly.
*/
private ModuleResources importConfig(InputStream stream) throws ModuleException {
XMLOverReader reader = XML.newJavaFactory().newOverReader();
try {
reader.openStream(stream);
return new ModuleResources(
importImageResources(reader),
importI18NResources(reader),
importResources(reader));
} catch (XMLException e) {
throw new ModuleException(e, ModuleOperation.LOAD);
} finally {
FileUtils.close(reader);
}
}
/**
* Read the manifest informations of the given module.
*
* @param container The module.
* @param bundle The bundle.
*/
private static void readManifestInformations(Builder container, Bundle bundle) {
@SuppressWarnings("unchecked") //We know that the bundle headers are a String<->String Map
Dictionary<String, String> headers = bundle.getHeaders();
container.setId(headers.get("Bundle-SymbolicName"));
container.setVersion(Version.get(headers.get("Bundle-Version")));
if (StringUtils.isNotEmpty(headers.get("Module-Core"))) {
container.setCoreVersion(Version.get(headers.get("Module-Core")));
} else {
container.setCoreVersion(Core.VERSION);
}
container.setUrl(headers.get("Module-Url"));
container.setUpdateUrl(headers.get("Module-UpdateUrl"));
container.setMessagesUrl(headers.get("Module-MessagesUrl"));
if (StringUtils.isNotEmpty(headers.get("Module-Collection"))) {
container.setCollection(Boolean.parseBoolean(headers.get("Module-Collection")));
}
if (StringUtils.isNotEmpty(headers.get("Module-Dependencies"))) {
container.setDependencies(COMMA_DELIMITER_PATTERN.split(headers.get("Module-Dependencies")));
} else {
container.setDependencies(EMPTY_ARRAY);
}
}
/**
* Load the i18n resources of the given module.
*
* @param module The module to load i18n resources for.
* @param resources The resources of the module.
*/
private void loadI18NResources(Module module, ModuleResources resources) {
for (I18NResource i18NResource : resources.getI18NResources()) {
List<org.jtheque.i18n.I18NResource> i18NResources = CollectionUtils.newList(i18NResource.getResources().size());
for (String resource : i18NResource.getResources()) {
if (resource.startsWith("classpath:")) {
i18NResources.add(I18NResourceFactory.fromURL(resource.substring(resource.lastIndexOf('/') + 1),
module.getBundle().getResource(resource.substring(10))));
}
}
languageService.registerResource(i18NResource.getName(), i18NResource.getVersion(),
i18NResources.toArray(new org.jtheque.i18n.I18NResource[i18NResources.size()]));
}
}
/**
* Import the i18n resources.
*
* @param reader The XML reader.
*
* @return A List containing all the I18NResource of the module.
*
* @throws XMLException If an error occurs during XML parsing.
*/
private static List<I18NResource> importI18NResources(XMLOverReader reader) throws XMLException {
List<I18NResource> i18NResources = CollectionUtils.newList();
while (reader.next("/config/i18n/resource")) {
List<String> resources = CollectionUtils.newList(5);
String name = reader.readString("@name");
Version version = Version.get(reader.readString("@version"));
while (reader.next("classpath")) {
resources.add("classpath:" + reader.readString("text()"));
}
i18NResources.add(new I18NResource(name, version, resources));
}
return i18NResources;
}
/**
* Import the image resources.
*
* @param reader The XML reader.
*
* @return A List containing all the ImageResource of the module.
*
* @throws XMLException If an exception occurs during XML parsing.
*/
private static List<ImageResource> importImageResources(XMLOverReader reader) throws XMLException {
List<ImageResource> imageResources = CollectionUtils.newList(5);
while (reader.next("/config/images/resource")) {
String name = reader.readString("@name");
String classpath = reader.readString("classpath");
imageResources.add(new ImageResource(name, "classpath:" + classpath));
}
return imageResources;
}
/**
* Import the resources.
*
* @param reader The XML reader.
*
* @return A List containing all the Resource of the module.
*
* @throws XMLException If an exception occurs during XML parsing.
*/
private List<Resource> importResources(XMLOverReader reader) throws XMLException {
List<Resource> resources = CollectionUtils.newList(5);
while (reader.next("/config/resources/resource")) {
String id = reader.readString("@id");
Version version = Version.get(reader.readString("@version"));
String url = reader.readString("@url");
resources.add(resourceService.getOrDownloadResource(id, version, url));
}
return resources;
}
/**
* Uninstall the given module.
*
* @param module The module to uninstall.
*/
public void uninstallModule(Module module) {
ModuleResources resources = moduleService.getResources(module);
if (resources != null) {
for (I18NResource i18NResource : resources.getI18NResources()) {
languageService.releaseResource(i18NResource.getName());
}
}
}
/**
* A simple task to load a module from a file.
*
* @author Baptiste Wicht
*/
private final class ModuleLoaderTask implements Callable<Module> {
private final File file;
/**
* Construct a new ModuleLoader task for the given file.
*
* @param file The file to installFromRepository.
*/
private ModuleLoaderTask(File file) {
this.file = file;
}
@Override
public Module call() {
try {
return installModule(file);
} catch (ModuleException e) {
OSGiUtils.getService(bundleContext, ErrorService.class).addError(Errors.newError(e));
}
return null;
}
}
/**
* A Builder for the SimpleModule instance.
*
* @author Baptiste Wicht
*/
private final class Builder {
private String id;
private Bundle bundle;
private Version version;
private Version coreVersion;
private String[] dependencies;
private String url;
private String updateUrl;
private String messagesUrl;
private boolean collection;
/**
* Set the id of the module.
*
* @param id The id of the module.
*/
public void setId(String id) {
this.id = id;
}
/**
* Set the version of the module.
*
* @param version The version of the module.
*/
public void setVersion(Version version) {
this.version = version;
}
/**
* Set the core version needed by the module.
*
* @param coreVersion The core version needed by the module.
*/
public void setCoreVersion(Version coreVersion) {
this.coreVersion = coreVersion;
}
/**
* Set the bundle of the module.
*
* @param bundle The bundle.
*/
public void setBundle(Bundle bundle) {
this.bundle = bundle;
}
/**
* Set the URL of the site of the module.
*
* @param url THe URL of the site of the module.
*/
public void setUrl(String url) {
this.url = url;
}
/**
* Set the the URL to the update file of the module.
*
* @param updateUrl The URL to the update file of the module.
*/
public void setUpdateUrl(String updateUrl) {
this.updateUrl = updateUrl;
}
/**
* Set the dependencies of the module.
*
* @param dependencies The dependencies of the module.
*/
public void setDependencies(String[] dependencies) {
this.dependencies = ArrayUtils.copyOf(dependencies);
}
/**
* Set the messages URL.
*
* @param messagesUrl The messages URL of the module.
*/
public void setMessagesUrl(String messagesUrl) {
this.messagesUrl = messagesUrl;
}
/**
* Set the boolean tag indicating if the module is collection-based or not.
*
* @param collection boolean tag indicating if the module is collection-based (true) or not (false).
*/
public void setCollection(boolean collection) {
this.collection = collection;
}
/**
* Build the module.
*
* @return The module to build.
*/
public Module build() {
return new SimpleModule(this);
}
}
/**
* A module implementation.
*
* @author Baptiste Wicht
*/
@Immutable
private final class SimpleModule implements Module {
private final String id;
private final Bundle bundle;
private final Version version;
private final Version coreVersion;
private final String[] dependencies;
private final String url;
private final String updateUrl;
private final String messagesUrl;
private final boolean collection;
private volatile ModuleState state;
/**
* Create a module container using the given builder informations.
*
* @param builder The builder to get the informations from.
*/
private SimpleModule(Builder builder) {
super();
id = builder.id;
version = builder.version;
coreVersion = builder.coreVersion;
bundle = builder.bundle;
dependencies = builder.dependencies;
url = builder.url;
updateUrl = builder.updateUrl;
messagesUrl = builder.messagesUrl;
collection = builder.collection;
state = ModuleState.INSTALLED;
}
@Override
public Bundle getBundle() {
return bundle;
}
@Override
public ModuleState getState() {
return state;
}
@Override
public void setState(ModuleState state) {
this.state = state;
}
@Override
public String getId() {
return id;
}
@Override
public String getName() {
return internationalize(id + ".name");
}
@Override
public String getAuthor() {
return internationalize(id + ".author");
}
@Override
public String getDescription() {
return internationalize(id + ".description");
}
@Override
public String getDisplayState() {
return internationalize(state.getKey());
}
/**
* Internationalize the given key.
*
* @param key The i18n key.
*
* @return The internationalized message.
*/
private String internationalize(String key) {
return languageService.getMessage(key);
}
@Override
public Version getVersion() {
return version;
}
@Override
public Version getCoreVersion() {
return coreVersion;
}
@Override
public String getUrl() {
return url;
}
@Override
public String getDescriptorURL() {
return updateUrl;
}
@Override
public String[] getDependencies() {
return ArrayUtils.copyOf(dependencies);
}
@Override
public String getMessagesUrl() {
return messagesUrl;
}
@Override
public String toString() {
return getName();
}
@Override
public boolean isCollection() {
return collection;
}
}
/**
* A module file filter. This filter accept only the JAR files.
*
* @author Baptiste Wicht
*/
private static final class ModuleFilter implements FileFilter {
@Override
public boolean accept(File file) {
return file.isFile() && file.getName().toLowerCase(Locale.getDefault()).endsWith(".jar");
}
}
}