package org.jtheque.modules.impl; import org.jtheque.core.Core; import org.jtheque.core.Folders; import org.jtheque.errors.ErrorService; import org.jtheque.errors.Errors; import org.jtheque.modules.Module; import org.jtheque.modules.ModuleException; import org.jtheque.modules.ModuleException.ModuleOperation; import org.jtheque.ui.UIUtils; import org.jtheque.utils.StringUtils; import org.jtheque.utils.ThreadUtils; import org.jtheque.utils.annotations.NotThreadSafe; import org.jtheque.utils.collections.ArrayUtils; import org.jtheque.utils.collections.CollectionUtils; import org.jtheque.utils.io.CopyException; import org.jtheque.utils.io.FileUtils; import org.osgi.framework.BundleException; import org.slf4j.LoggerFactory; import javax.annotation.Resource; import java.io.File; import java.util.Collection; import java.util.Iterator; import java.util.List; import java.util.Set; import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Semaphore; import static org.jtheque.modules.ModuleState.DISABLED; import static org.jtheque.modules.ModuleState.STARTED; /* * 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 manager for the modules. It is only used by the module service. * * @author Baptiste Wicht */ @NotThreadSafe public final class ModuleManager { private final List<Module> modules = CollectionUtils.newConcurrentList(); @Resource private Core core; @Resource private UIUtils uiUtils; @Resource private ErrorService errorService; @Resource private ModuleLoader moduleLoader; /** * Return all the modules. * * @return A Collection containing all the modules. */ Collection<Module> getModules() { return modules; } /** * Return the module with the given ID. * * @param id The id of the module to search. * * @return The module with the specified id or {@code null} if there is no module with this id. */ Module getModuleById(String id) { for (Module module : modules) { if (id.equals(module.getId())) { return module; } } return null; } /** * Indicate if a module with the given id exists or not. * * @param id The id to search for. * * @return true if a module exists with this id otherwise false. */ boolean exists(String id) { return getModuleById(id) != null; } /** * Start the given module. * * @param module The module to start. * * @throws org.jtheque.modules.ModuleException * If there is an OSGi error during start. */ void startModule(Module module) throws ModuleException { try { module.getBundle().start(); } catch (BundleException e) { throw new ModuleException(e, ModuleOperation.START); } } /** * Stop the given module. * * @param module The module to stop. * * @throws org.jtheque.modules.ModuleException * If there is an OSGi error during start. */ void stopModule(Module module) throws ModuleException { try { module.getBundle().stop(); } catch (BundleException e) { throw new ModuleException(e, ModuleOperation.STOP); } } /** * Start all the modules using the given launcher. * * @param moduleLauncher The launcher to use. */ void startAll(ModuleLauncher moduleLauncher) { if (isStartingConcurrent()) { ModuleStarter starter = new ModuleStarter(moduleLauncher); for (Module module : modules) { if (canBeLoaded(module) && areAllDependenciesSatisfied(module)) { starter.addModule(module); } } starter.startAll(); } else { for (Module module : modules) { if (canBeLoaded(module) && areAllDependenciesSatisfied(module)) { try { moduleLauncher.startModule(module); } catch (ModuleException e) { errorService.addError(Errors.newError(e)); } } } } } /** * Indicate if the starting must be made in concurrent. * * @return {@code true} if the starting is made in parallel or sequentially ({@code false}). */ private static boolean isStartingConcurrent() { String property = System.getProperty("jtheque.concurrent.start"); return StringUtils.isNotEmpty(property) && "true".equalsIgnoreCase(property); } /** * Test if the module can be loaded. * * @param module The module to test. * * @return true if the module can be loaded else false. */ static boolean canBeLoaded(Module module) { return module.getState() != DISABLED; } /** * Test if there is a dependency on the given module. * * @param module The module to test for dependencies. * * @return true if there is a dependency on the given module. */ boolean isThereIsActiveDependenciesOn(Module module) { for (Module other : modules) { if (other != module && other.getState() == STARTED && ArrayUtils.contains(other.getDependencies(), module.getId())) { return true; } } return false; } /** * Load all the modules. */ void loadModules() { //Load all modules modules.addAll(moduleLoader.loadModules()); CollectionUtils.filter(modules, new CoreVersionFilter(core, uiUtils)); //Must cast to perform the good sort CollectionUtils.sort((CopyOnWriteArrayList<Module>) modules, new ModuleComparator()); } /** * Install the module from the given file. * * @param file The file to install. * * @return The installed module or {@code null} if the module file cannot be installed. * * @throws org.jtheque.modules.ModuleException * If there is problem installing the file. */ Module installModule(File file) throws ModuleException { File moduleFile = installModuleFile(file); try { Module module = moduleLoader.installModule(moduleFile); if (exists(module.getId())) { throw new ModuleException("error.module.install.already.exists", ModuleOperation.INSTALL); } else { modules.add(module); return module; } } catch (ModuleException e){ FileUtils.delete(moduleFile); throw e; } } /** * Install the module file. It seems copy it into the application directory and make verifications for the existence * of the file. * * @param file The file of the module. * * @return The file were the module has been installed. * * @throws org.jtheque.modules.ModuleException * If there is a problem to copy the file in the good folder. */ private static File installModuleFile(File file) throws ModuleException { File target = file; if (!FileUtils.isFileInDirectory(file, Folders.getModulesFolder())) { target = new File(Folders.getModulesFolder(), file.getName()); if (target.exists()) { throw new ModuleException("errors.module.installFromRepository.already.exists", ModuleOperation.INSTALL); } else { try { FileUtils.copy(file, target); } catch (CopyException e) { throw new ModuleException(e, ModuleOperation.INSTALL); } } } return target; } /** * Indicate if all the dependencies of the module are satisfied. * * @param module The module to test. * * @return <code>true</code> if all the dependencies are satisfied else <code>false</code>. */ private boolean areAllDependenciesSatisfied(Module module) { if (StringUtils.isEmpty(module.getDependencies())) { return true; } for (String dependency : module.getDependencies()) { Module resolvedDependency = getModuleById(dependency); if (resolvedDependency == null || !canBeLoaded(resolvedDependency)) { return false; } } return true; } /** * Indicate if all the dependencies of the given module are satisfied. * * @param module The module to test. * * @return true if the all the dependencies of the module are satisfied else false. */ boolean areAllDependenciesSatisfiedAndActive(Module module) { if (StringUtils.isEmpty(module.getDependencies())) { return true; } for (String dependencyId : module.getDependencies()) { Module dependency = getModuleById(dependencyId); if (dependency == null || dependency.getState() != STARTED) { return false; } } return true; } /** * Install the module from the repository from the file. * * @param file The file of the module. * * @return The installed Module. * * @throws org.jtheque.modules.ModuleException * If an exception occurs during install. */ Module installModuleFromRepository(File file) throws ModuleException { Module module = moduleLoader.installModule(file); uiUtils.displayI18nText("message.module.repository.installed"); return module; } /** * Uninstall the module. * * @param module The module to uninstall. * * @throws org.jtheque.modules.ModuleException * If an error occurs during the uninstallation of the bundle. */ void uninstallModule(Module module) throws ModuleException { try { module.getBundle().uninstall(); } catch (BundleException e) { throw new ModuleException(e, ModuleOperation.UNINSTALL); } moduleLoader.uninstallModule(module); modules.remove(module); //Delete the bundle file FileUtils.delete(StringUtils.delete(module.getBundle().getLocation(), "file:")); } /** * A starter for the modules. This started load the modules with several threads. * * @author Baptiste Wicht */ private final class ModuleStarter { private final Set<Module> startList = CollectionUtils.newSet(5); private final ModuleLauncher moduleLauncher; private final ExecutorService startersPool = Executors.newFixedThreadPool(ThreadUtils.processors()); private final Semaphore semaphore = new Semaphore(0, true); private CountDownLatch countDown; /** * Construct a new ModuleStarter. * * @param moduleLauncher The module launcher to use. */ private ModuleStarter(ModuleLauncher moduleLauncher) { super(); this.moduleLauncher = moduleLauncher; } /** * Add a module to start. * * @param module The module to start. */ public void addModule(Module module) { startList.add(module); } /** * Start all the modules of the starter. */ public void startAll() { if (startList.isEmpty()) { return; } countDown = new CountDownLatch(startList.size()); //startReadyModules(); while (true) { try { semaphore.acquire(); startReadyModules(); if (startList.isEmpty()) { break; } } catch (InterruptedException e) { LoggerFactory.getLogger(getClass()).error(e.getMessage(), e); Thread.currentThread().interrupt(); break; } } try { countDown.await(); } catch (InterruptedException e) { LoggerFactory.getLogger(getClass()).error(e.getMessage(), e); Thread.currentThread().interrupt(); } startersPool.shutdown(); } /** * Start the currently ready modules. */ private void startReadyModules() { for (Iterator<Module> iterator = startList.iterator(); iterator.hasNext();) { Module module = iterator.next(); if (canBeStarted(module)) { startersPool.submit(new ModuleStarterRunnable(this, module, moduleLauncher)); iterator.remove(); } else if (!(canBeLoaded(module) && areAllDependenciesSatisfied(module))) { //Perhaps the start of a module has not been successful and this module cannot be launched anymore countDown.countDown(); iterator.remove(); } } } /** * Indicate if the module can be started. * * @param module The module to test. * * @return {@code true} if the module can be started else {@code false}. */ public boolean canBeStarted(Module module) { if (module.getCoreVersion() != null && module.getCoreVersion().isGreaterThan(Core.VERSION)) { return false; } return areAllDependenciesSatisfiedAndActive(module); } } /** * A simple runnable to start a module. * * @author Baptiste Wicht */ private final class ModuleStarterRunnable implements Runnable { private final ModuleStarter starter; private final Module module; private final ModuleLauncher moduleLauncher; /** * Construct a ModuleStarterRunnable for the given module. * * @param starter The starter. * @param module The module to start. * @param moduleLauncher The module launcher to start the module. */ private ModuleStarterRunnable(ModuleStarter starter, Module module, ModuleLauncher moduleLauncher) { super(); this.starter = starter; this.module = module; this.moduleLauncher = moduleLauncher; } @Override public void run() { try { moduleLauncher.startModule(module); } catch (ModuleException e) { errorService.addError(Errors.newError(e)); } starter.semaphore.release(); starter.countDown.countDown(); } } }