| // Copyright (C) 2012 The Android Open Source Project |
| // |
| // 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. |
| |
| package com.google.gerrit.server.plugins; |
| |
| import com.google.common.base.CharMatcher; |
| import com.google.common.base.Joiner; |
| import com.google.common.base.MoreObjects; |
| import com.google.common.base.Predicate; |
| import com.google.common.collect.Iterables; |
| import com.google.common.collect.LinkedHashMultimap; |
| import com.google.common.collect.Lists; |
| import com.google.common.collect.Maps; |
| import com.google.common.collect.Multimap; |
| import com.google.common.collect.Queues; |
| import com.google.common.collect.Sets; |
| import com.google.gerrit.extensions.annotations.PluginName; |
| import com.google.gerrit.extensions.events.LifecycleListener; |
| import com.google.gerrit.extensions.systemstatus.ServerInformation; |
| import com.google.gerrit.extensions.webui.JavaScriptPlugin; |
| import com.google.gerrit.server.PluginUser; |
| import com.google.gerrit.server.cache.PersistentCacheFactory; |
| import com.google.gerrit.server.config.CanonicalWebUrl; |
| import com.google.gerrit.server.config.ConfigUtil; |
| import com.google.gerrit.server.config.GerritServerConfig; |
| import com.google.gerrit.server.config.SitePaths; |
| import com.google.gerrit.server.plugins.ServerPluginProvider.PluginDescription; |
| import com.google.inject.Inject; |
| import com.google.inject.Provider; |
| import com.google.inject.Singleton; |
| |
| import org.eclipse.jgit.internal.storage.file.FileSnapshot; |
| import org.eclipse.jgit.lib.Config; |
| import org.slf4j.Logger; |
| import org.slf4j.LoggerFactory; |
| |
| import java.io.File; |
| import java.io.FileFilter; |
| import java.io.FileOutputStream; |
| import java.io.IOException; |
| import java.io.InputStream; |
| import java.util.AbstractMap; |
| import java.util.ArrayList; |
| import java.util.Arrays; |
| import java.util.Collection; |
| import java.util.Collections; |
| import java.util.Comparator; |
| import java.util.Iterator; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.Map.Entry; |
| import java.util.Queue; |
| import java.util.Set; |
| import java.util.TreeSet; |
| import java.util.concurrent.ConcurrentMap; |
| import java.util.concurrent.TimeUnit; |
| |
| @Singleton |
| public class PluginLoader implements LifecycleListener { |
| static final String PLUGIN_TMP_PREFIX = "plugin_"; |
| static final Logger log = LoggerFactory.getLogger(PluginLoader.class); |
| |
| public String getPluginName(File srcFile) { |
| return MoreObjects.firstNonNull(getGerritPluginName(srcFile), |
| nameOf(srcFile)); |
| } |
| |
| private final File pluginsDir; |
| private final File dataDir; |
| private final PluginGuiceEnvironment env; |
| private final ServerInformationImpl srvInfoImpl; |
| private final PluginUser.Factory pluginUserFactory; |
| private final ConcurrentMap<String, Plugin> running; |
| private final ConcurrentMap<String, Plugin> disabled; |
| private final Map<String, FileSnapshot> broken; |
| private final Map<Plugin, CleanupHandle> cleanupHandles; |
| private final Queue<Plugin> toCleanup; |
| private final Provider<PluginCleanerTask> cleaner; |
| private final PluginScannerThread scanner; |
| private final Provider<String> urlProvider; |
| private final PersistentCacheFactory persistentCacheFactory; |
| private final boolean remoteAdmin; |
| private final UniversalServerPluginProvider serverPluginFactory; |
| |
| @Inject |
| public PluginLoader(SitePaths sitePaths, |
| PluginGuiceEnvironment pe, |
| ServerInformationImpl sii, |
| PluginUser.Factory puf, |
| Provider<PluginCleanerTask> pct, |
| @GerritServerConfig Config cfg, |
| @CanonicalWebUrl Provider<String> provider, |
| PersistentCacheFactory cacheFactory, |
| UniversalServerPluginProvider pluginFactory) { |
| pluginsDir = sitePaths.plugins_dir; |
| dataDir = sitePaths.data_dir; |
| env = pe; |
| srvInfoImpl = sii; |
| pluginUserFactory = puf; |
| running = Maps.newConcurrentMap(); |
| disabled = Maps.newConcurrentMap(); |
| broken = Maps.newHashMap(); |
| toCleanup = Queues.newArrayDeque(); |
| cleanupHandles = Maps.newConcurrentMap(); |
| cleaner = pct; |
| urlProvider = provider; |
| persistentCacheFactory = cacheFactory; |
| serverPluginFactory = pluginFactory; |
| |
| remoteAdmin = |
| cfg.getBoolean("plugins", null, "allowRemoteAdmin", false); |
| |
| long checkFrequency = ConfigUtil.getTimeUnit(cfg, |
| "plugins", null, "checkFrequency", |
| TimeUnit.MINUTES.toMillis(1), TimeUnit.MILLISECONDS); |
| if (checkFrequency > 0) { |
| scanner = new PluginScannerThread(this, checkFrequency); |
| } else { |
| scanner = null; |
| } |
| } |
| |
| public boolean isRemoteAdminEnabled() { |
| return remoteAdmin; |
| } |
| |
| public Plugin get(String name) { |
| Plugin p = running.get(name); |
| if (p != null) { |
| return p; |
| } |
| return disabled.get(name); |
| } |
| |
| public Iterable<Plugin> getPlugins(boolean all) { |
| if (!all) { |
| return running.values(); |
| } else { |
| List<Plugin> plugins = new ArrayList<>(running.values()); |
| plugins.addAll(disabled.values()); |
| return plugins; |
| } |
| } |
| |
| public void installPluginFromStream(String originalName, InputStream in) |
| throws IOException, PluginInstallException { |
| checkRemoteInstall(); |
| |
| String fileName = originalName; |
| File tmp = asTemp(in, ".next_" + fileName + "_", ".tmp", pluginsDir); |
| String name = MoreObjects.firstNonNull(getGerritPluginName(tmp), |
| nameOf(fileName)); |
| if (!originalName.equals(name)) { |
| log.warn(String.format("Plugin provides its own name: <%s>," |
| + " use it instead of the input name: <%s>", |
| name, originalName)); |
| } |
| |
| String fileExtension = getExtension(fileName); |
| File dst = new File(pluginsDir, name + fileExtension); |
| synchronized (this) { |
| Plugin active = running.get(name); |
| if (active != null) { |
| fileName = active.getSrcFile().getName(); |
| log.info(String.format("Replacing plugin %s", active.getName())); |
| File old = new File(pluginsDir, ".last_" + fileName); |
| old.delete(); |
| active.getSrcFile().renameTo(old); |
| } |
| |
| new File(pluginsDir, fileName + ".disabled").delete(); |
| tmp.renameTo(dst); |
| try { |
| Plugin plugin = runPlugin(name, dst, active); |
| if (active == null) { |
| log.info(String.format("Installed plugin %s", plugin.getName())); |
| } |
| } catch (PluginInstallException e) { |
| dst.delete(); |
| throw e; |
| } |
| |
| cleanInBackground(); |
| } |
| } |
| |
| static File asTemp(InputStream in, String prefix, String suffix, File dir) |
| throws IOException { |
| File tmp = File.createTempFile(prefix, suffix, dir); |
| boolean keep = false; |
| try (FileOutputStream out = new FileOutputStream(tmp)) { |
| byte[] data = new byte[8192]; |
| int n; |
| while ((n = in.read(data)) > 0) { |
| out.write(data, 0, n); |
| } |
| keep = true; |
| return tmp; |
| } finally { |
| if (!keep) { |
| tmp.delete(); |
| } |
| } |
| } |
| |
| private synchronized void unloadPlugin(Plugin plugin) { |
| persistentCacheFactory.onStop(plugin); |
| String name = plugin.getName(); |
| log.info(String.format("Unloading plugin %s", name)); |
| plugin.stop(env); |
| env.onStopPlugin(plugin); |
| running.remove(name); |
| disabled.remove(name); |
| toCleanup.add(plugin); |
| } |
| |
| public void disablePlugins(Set<String> names) { |
| if (!isRemoteAdminEnabled()) { |
| log.warn("Remote plugin administration is disabled," |
| + " ignoring disablePlugins(" + names + ")"); |
| return; |
| } |
| |
| synchronized (this) { |
| for (String name : names) { |
| Plugin active = running.get(name); |
| if (active == null) { |
| continue; |
| } |
| |
| log.info(String.format("Disabling plugin %s", active.getName())); |
| File off = new File(active.getSrcFile() + ".disabled"); |
| active.getSrcFile().renameTo(off); |
| |
| unloadPlugin(active); |
| try { |
| FileSnapshot snapshot = FileSnapshot.save(off); |
| Plugin offPlugin = loadPlugin(name, off, snapshot); |
| disabled.put(name, offPlugin); |
| } catch (Throwable e) { |
| // This shouldn't happen, as the plugin was loaded earlier. |
| log.warn(String.format( |
| "Cannot load disabled plugin %s", active.getName()), |
| e.getCause()); |
| } |
| } |
| cleanInBackground(); |
| } |
| } |
| |
| public void enablePlugins(Set<String> names) throws PluginInstallException { |
| if (!isRemoteAdminEnabled()) { |
| log.warn("Remote plugin administration is disabled," |
| + " ignoring enablePlugins(" + names + ")"); |
| return; |
| } |
| |
| synchronized (this) { |
| for (String name : names) { |
| Plugin off = disabled.get(name); |
| if (off == null) { |
| continue; |
| } |
| |
| log.info(String.format("Enabling plugin %s", name)); |
| String n = off.getSrcFile().getName(); |
| if (n.endsWith(".disabled")) { |
| n = n.substring(0, n.lastIndexOf('.')); |
| } |
| File on = new File(pluginsDir, n); |
| off.getSrcFile().renameTo(on); |
| |
| disabled.remove(name); |
| runPlugin(name, on, null); |
| } |
| cleanInBackground(); |
| } |
| } |
| |
| @Override |
| public synchronized void start() { |
| log.info("Loading plugins from " + pluginsDir.getAbsolutePath()); |
| srvInfoImpl.state = ServerInformation.State.STARTUP; |
| rescan(); |
| srvInfoImpl.state = ServerInformation.State.RUNNING; |
| if (scanner != null) { |
| scanner.start(); |
| } |
| } |
| |
| @Override |
| public void stop() { |
| if (scanner != null) { |
| scanner.end(); |
| } |
| srvInfoImpl.state = ServerInformation.State.SHUTDOWN; |
| synchronized (this) { |
| for (Plugin p : running.values()) { |
| unloadPlugin(p); |
| } |
| running.clear(); |
| disabled.clear(); |
| broken.clear(); |
| if (!toCleanup.isEmpty()) { |
| System.gc(); |
| processPendingCleanups(); |
| } |
| } |
| } |
| |
| public void reload(List<String> names) |
| throws InvalidPluginException, PluginInstallException { |
| synchronized (this) { |
| List<Plugin> reload = Lists.newArrayListWithCapacity(names.size()); |
| List<String> bad = Lists.newArrayListWithExpectedSize(4); |
| for (String name : names) { |
| Plugin active = running.get(name); |
| if (active != null) { |
| reload.add(active); |
| } else { |
| bad.add(name); |
| } |
| } |
| if (!bad.isEmpty()) { |
| throw new InvalidPluginException(String.format( |
| "Plugin(s) \"%s\" not running", |
| Joiner.on("\", \"").join(bad))); |
| } |
| |
| for (Plugin active : reload) { |
| String name = active.getName(); |
| try { |
| log.info(String.format("Reloading plugin %s", name)); |
| runPlugin(name, active.getSrcFile(), active); |
| } catch (PluginInstallException e) { |
| log.warn(String.format("Cannot reload plugin %s", name), e.getCause()); |
| throw e; |
| } |
| } |
| |
| cleanInBackground(); |
| } |
| } |
| |
| public synchronized void rescan() { |
| Multimap<String, File> pluginsFiles = prunePlugins(pluginsDir); |
| if (pluginsFiles.isEmpty()) { |
| return; |
| } |
| |
| syncDisabledPlugins(pluginsFiles); |
| |
| Map<String, File> activePlugins = filterDisabled(pluginsFiles); |
| for (Map.Entry<String, File> entry : jarsFirstSortedPluginsSet(activePlugins)) { |
| String name = entry.getKey(); |
| File file = entry.getValue(); |
| String fileName = file.getName(); |
| if (!isJsPlugin(fileName) && !serverPluginFactory.handles(file)) { |
| log.warn("No Plugin provider was found that handles this file format: {}", fileName); |
| continue; |
| } |
| |
| FileSnapshot brokenTime = broken.get(name); |
| if (brokenTime != null && !brokenTime.isModified(file)) { |
| continue; |
| } |
| |
| Plugin active = running.get(name); |
| if (active != null && !active.isModified(file)) { |
| continue; |
| } |
| |
| if (active != null) { |
| log.info(String.format("Reloading plugin %s, version %s", |
| active.getName(), active.getVersion())); |
| } |
| |
| try { |
| Plugin loadedPlugin = runPlugin(name, file, active); |
| if (active == null && !loadedPlugin.isDisabled()) { |
| log.info(String.format("Loaded plugin %s, version %s", |
| loadedPlugin.getName(), loadedPlugin.getVersion())); |
| } |
| } catch (PluginInstallException e) { |
| log.warn(String.format("Cannot load plugin %s", name), e.getCause()); |
| } |
| } |
| |
| cleanInBackground(); |
| } |
| |
| private void addAllEntries(Map<String, File> from, |
| TreeSet<Entry<String, File>> to) { |
| Iterator<Entry<String, File>> it = from.entrySet().iterator(); |
| while (it.hasNext()) { |
| Entry<String,File> entry = it.next(); |
| to.add(new AbstractMap.SimpleImmutableEntry<>( |
| entry.getKey(), entry.getValue())); |
| } |
| } |
| |
| private TreeSet<Entry<String, File>> jarsFirstSortedPluginsSet( |
| Map<String, File> activePlugins) { |
| TreeSet<Entry<String, File>> sortedPlugins = |
| Sets.newTreeSet(new Comparator<Entry<String, File>>() { |
| @Override |
| public int compare(Entry<String, File> entry1, |
| Entry<String, File> entry2) { |
| String file1 = entry1.getValue().getName(); |
| String file2 = entry2.getValue().getName(); |
| int cmp = file1.compareTo(file2); |
| if (file1.endsWith(".jar")) { |
| return (file2.endsWith(".jar") ? cmp : -1); |
| } else { |
| return (file2.endsWith(".jar") ? +1 : cmp); |
| } |
| } |
| }); |
| |
| addAllEntries(activePlugins, sortedPlugins); |
| return sortedPlugins; |
| } |
| |
| private void syncDisabledPlugins(Multimap<String, File> jars) { |
| stopRemovedPlugins(jars); |
| dropRemovedDisabledPlugins(jars); |
| } |
| |
| private Plugin runPlugin(String name, File plugin, Plugin oldPlugin) |
| throws PluginInstallException { |
| FileSnapshot snapshot = FileSnapshot.save(plugin); |
| try { |
| Plugin newPlugin = loadPlugin(name, plugin, snapshot); |
| if (newPlugin.getCleanupHandle() != null) { |
| cleanupHandles.put(newPlugin, newPlugin.getCleanupHandle()); |
| } |
| /* |
| * Pluggable plugin provider may have assigned a plugin name that could be |
| * actually different from the initial one assigned during scan. It is |
| * safer then to reassign it. |
| */ |
| name = newPlugin.getName(); |
| boolean reload = oldPlugin != null |
| && oldPlugin.canReload() |
| && newPlugin.canReload(); |
| if (!reload && oldPlugin != null) { |
| unloadPlugin(oldPlugin); |
| } |
| if (!newPlugin.isDisabled()) { |
| newPlugin.start(env); |
| } |
| if (reload) { |
| env.onReloadPlugin(oldPlugin, newPlugin); |
| unloadPlugin(oldPlugin); |
| } else if (!newPlugin.isDisabled()) { |
| env.onStartPlugin(newPlugin); |
| } |
| if (!newPlugin.isDisabled()) { |
| running.put(name, newPlugin); |
| } else { |
| disabled.put(name, newPlugin); |
| } |
| broken.remove(name); |
| return newPlugin; |
| } catch (Throwable err) { |
| broken.put(name, snapshot); |
| throw new PluginInstallException(err); |
| } |
| } |
| |
| private void stopRemovedPlugins(Multimap<String, File> jars) { |
| Set<String> unload = Sets.newHashSet(running.keySet()); |
| for (Map.Entry<String, Collection<File>> entry : jars.asMap().entrySet()) { |
| for (File file : entry.getValue()) { |
| if (!file.getName().endsWith(".disabled")) { |
| unload.remove(entry.getKey()); |
| } |
| } |
| } |
| for (String name : unload) { |
| unloadPlugin(running.get(name)); |
| } |
| } |
| |
| private void dropRemovedDisabledPlugins(Multimap<String, File> jars) { |
| Set<String> unload = Sets.newHashSet(disabled.keySet()); |
| for (Map.Entry<String, Collection<File>> entry : jars.asMap().entrySet()) { |
| for (File file : entry.getValue()) { |
| if (file.getName().endsWith(".disabled")) { |
| unload.remove(entry.getKey()); |
| } |
| } |
| } |
| for (String name : unload) { |
| disabled.remove(name); |
| } |
| } |
| |
| synchronized int processPendingCleanups() { |
| Iterator<Plugin> iterator = toCleanup.iterator(); |
| while (iterator.hasNext()) { |
| Plugin plugin = iterator.next(); |
| iterator.remove(); |
| |
| CleanupHandle cleanupHandle = cleanupHandles.remove(plugin); |
| if (cleanupHandle != null) { |
| cleanupHandle.cleanup(); |
| } |
| } |
| return toCleanup.size(); |
| } |
| |
| private void cleanInBackground() { |
| int cnt = toCleanup.size(); |
| if (0 < cnt) { |
| cleaner.get().clean(cnt); |
| } |
| } |
| |
| public static String nameOf(File plugin) { |
| return nameOf(plugin.getName()); |
| } |
| |
| private static String nameOf(String name) { |
| if (name.endsWith(".disabled")) { |
| name = name.substring(0, name.lastIndexOf('.')); |
| } |
| int ext = name.lastIndexOf('.'); |
| return 0 < ext ? name.substring(0, ext) : name; |
| } |
| |
| private static String getExtension(String name) { |
| int ext = name.lastIndexOf('.'); |
| return 0 < ext ? name.substring(ext) : ""; |
| } |
| |
| private Plugin loadPlugin(String name, File srcPlugin, FileSnapshot snapshot) |
| throws InvalidPluginException { |
| String pluginName = srcPlugin.getName(); |
| if (isJsPlugin(pluginName)) { |
| return loadJsPlugin(name, srcPlugin, snapshot); |
| } else if (serverPluginFactory.handles(srcPlugin)) { |
| return loadServerPlugin(srcPlugin, snapshot); |
| } else { |
| throw new InvalidPluginException(String.format( |
| "Unsupported plugin type: %s", srcPlugin.getName())); |
| } |
| } |
| |
| private File getPluginDataDir(String name) { |
| return new File(dataDir, name); |
| } |
| |
| private String getPluginCanonicalWebUrl(String name) { |
| String url = String.format("%s/plugins/%s/", |
| CharMatcher.is('/').trimTrailingFrom(urlProvider.get()), |
| name); |
| return url; |
| } |
| |
| private Plugin loadJsPlugin(String name, File srcJar, FileSnapshot snapshot) { |
| return new JsPlugin(name, srcJar, pluginUserFactory.create(name), snapshot); |
| } |
| |
| private ServerPlugin loadServerPlugin(File scriptFile, |
| FileSnapshot snapshot) throws InvalidPluginException { |
| String name = serverPluginFactory.getPluginName(scriptFile); |
| return serverPluginFactory.get(scriptFile, snapshot, new PluginDescription( |
| pluginUserFactory.create(name), getPluginCanonicalWebUrl(name), |
| getPluginDataDir(name))); |
| } |
| |
| static ClassLoader parentFor(Plugin.ApiType type) |
| throws InvalidPluginException { |
| switch (type) { |
| case EXTENSION: |
| return PluginName.class.getClassLoader(); |
| case PLUGIN: |
| return PluginLoader.class.getClassLoader(); |
| case JS: |
| return JavaScriptPlugin.class.getClassLoader(); |
| default: |
| throw new InvalidPluginException("Unsupported ApiType " + type); |
| } |
| } |
| |
| // Only one active plugin per plugin name can exist for each plugin name. |
| // Filter out disabled plugins and transform the multimap to a map |
| private static Map<String, File> filterDisabled( |
| Multimap<String, File> pluginFiles) { |
| Map<String, File> activePlugins = Maps.newHashMapWithExpectedSize( |
| pluginFiles.keys().size()); |
| for (String name : pluginFiles.keys()) { |
| for (File pluginFile : pluginFiles.asMap().get(name)) { |
| if (!pluginFile.getName().endsWith(".disabled")) { |
| assert(!activePlugins.containsKey(name)); |
| activePlugins.put(name, pluginFile); |
| } |
| } |
| } |
| return activePlugins; |
| } |
| |
| // Scan the $site_path/plugins directory and fetch all files and directories. |
| // The Key in returned multimap is the plugin name initially assigned from its filename. |
| // Values are the files. Plugins can optionally provide their name in MANIFEST file. |
| // If multiple plugin files provide the same plugin name, then only |
| // the first plugin remains active and all other plugins with the same |
| // name are disabled. |
| // |
| // NOTE: Bear in mind that the plugin name can be reassigned after load by the |
| // Server plugin provider. |
| public Multimap<String, File> prunePlugins(File pluginsDir) { |
| List<File> pluginFiles = scanFilesInPluginsDirectory(pluginsDir); |
| Multimap<String, File> map; |
| map = asMultimap(pluginFiles); |
| for (String plugin : map.keySet()) { |
| Collection<File> files = map.asMap().get(plugin); |
| if (files.size() == 1) { |
| continue; |
| } |
| // retrieve enabled plugins |
| Iterable<File> enabled = filterDisabledPlugins( |
| files); |
| // If we have only one (the winner) plugin, nothing to do |
| if (!Iterables.skip(enabled, 1).iterator().hasNext()) { |
| continue; |
| } |
| File winner = Iterables.getFirst(enabled, null); |
| assert(winner != null); |
| // Disable all loser plugins by renaming their file names to |
| // "file.disabled" and replace the disabled files in the multimap. |
| Collection<File> elementsToRemove = Lists.newArrayList(); |
| Collection<File> elementsToAdd = Lists.newArrayList(); |
| for (File loser : Iterables.skip(enabled, 1)) { |
| log.warn(String.format("Plugin <%s> was disabled, because" |
| + " another plugin <%s>" |
| + " with the same name <%s> already exists", |
| loser, winner, plugin)); |
| File disabledPlugin = new File(loser + ".disabled"); |
| elementsToAdd.add(disabledPlugin); |
| elementsToRemove.add(loser); |
| loser.renameTo(disabledPlugin); |
| } |
| Iterables.removeAll(files, elementsToRemove); |
| Iterables.addAll(files, elementsToAdd); |
| } |
| return map; |
| } |
| |
| private List<File> scanFilesInPluginsDirectory(File pluginsDir) { |
| if (pluginsDir == null || !pluginsDir.exists()) { |
| return Collections.emptyList(); |
| } |
| File[] matches = pluginsDir.listFiles(new FileFilter() { |
| @Override |
| public boolean accept(File pathname) { |
| String n = pathname.getName(); |
| return !n.startsWith(".last_") |
| && !n.startsWith(".next_"); |
| } |
| }); |
| if (matches == null) { |
| log.error("Cannot list " + pluginsDir.getAbsolutePath()); |
| return Collections.emptyList(); |
| } |
| return Arrays.asList(matches); |
| } |
| |
| private static Iterable<File> filterDisabledPlugins( |
| Collection<File> files) { |
| return Iterables.filter(files, new Predicate<File>() { |
| @Override |
| public boolean apply(File file) { |
| return !file.getName().endsWith(".disabled"); |
| } |
| }); |
| } |
| |
| public String getGerritPluginName(File srcFile) { |
| String fileName = srcFile.getName(); |
| if (isJsPlugin(fileName)) { |
| return fileName.substring(0, fileName.length() - 3); |
| } |
| if (serverPluginFactory.handles(srcFile)) { |
| return serverPluginFactory.getPluginName(srcFile); |
| } |
| return null; |
| } |
| |
| private Multimap<String, File> asMultimap(List<File> plugins) { |
| Multimap<String, File> map = LinkedHashMultimap.create(); |
| for (File srcFile : plugins) { |
| map.put(getPluginName(srcFile), srcFile); |
| } |
| return map; |
| } |
| |
| private static boolean isJsPlugin(String name) { |
| return isPlugin(name, "js"); |
| } |
| |
| private static boolean isPlugin(String fileName, String ext) { |
| String fullExt = "." + ext; |
| return fileName.endsWith(fullExt) || fileName.endsWith(fullExt + ".disabled"); |
| } |
| |
| private void checkRemoteInstall() throws PluginInstallException { |
| if (!isRemoteAdminEnabled()) { |
| throw new PluginInstallException("remote installation is disabled"); |
| } |
| } |
| } |