View Javadoc
1   package com.atlassian.plugin.loaders;
2   
3   import com.atlassian.plugin.DefaultPluginArtifactFactory;
4   import com.atlassian.plugin.Plugin;
5   import com.atlassian.plugin.PluginException;
6   import com.atlassian.plugin.PluginInternal;
7   import com.atlassian.plugin.event.PluginEventManager;
8   import com.atlassian.plugin.factories.PluginFactory;
9   import com.atlassian.plugin.loaders.classloading.DeploymentUnit;
10  import com.atlassian.plugin.loaders.classloading.EmptyScanner;
11  import com.atlassian.plugin.loaders.classloading.ForwardingScanner;
12  import com.atlassian.plugin.loaders.classloading.Scanner;
13  import org.apache.commons.io.FileUtils;
14  import org.slf4j.Logger;
15  import org.slf4j.LoggerFactory;
16  
17  import java.io.File;
18  import java.io.IOException;
19  import java.net.URL;
20  import java.nio.file.Files;
21  import java.util.ArrayList;
22  import java.util.List;
23  
24  import static com.atlassian.plugin.ReferenceMode.PERMIT_REFERENCE;
25  import static com.google.common.base.Preconditions.checkArgument;
26  import static com.google.common.base.Preconditions.checkNotNull;
27  
28  /**
29   * A Plugin loader that manages a set of bundled plugins, meaning that they can can be upgraded, but
30   * not deleted.
31   * <p>
32   * This loader can source plugins from:
33   * <p>
34   * <ul>
35   * <li>A directory containing Plugin artifacts.
36   * <li>A file with a {@link #getListSuffix()} suffix, where each line of that
37   * file gives the path to a Plugin artifact.
38   * <li>A URL identifying a zip file, and a path to explode the zip into, and the exploded
39   * contents of the zip are used as the plugin artifacts.
40   * </ul>
41   */
42  public class BundledPluginLoader extends ScanningPluginLoader {
43      private static final Logger log = LoggerFactory.getLogger(BundledPluginLoader.class);
44  
45      /**
46       * The suffix used for bundled plugin list files.
47       */
48      public static String getListSuffix() {
49          return ".list";
50      }
51  
52      private BundledPluginLoader(
53              final Scanner scanner,
54              final List<PluginFactory> pluginFactories,
55              final PluginEventManager eventManager) {
56          super(new NonRemovingScanner(scanner), pluginFactories, new DefaultPluginArtifactFactory(PERMIT_REFERENCE),
57                  eventManager);
58      }
59  
60      /**
61       * Construct a bundled plugin loader for a directory or list file source.
62       *
63       * @param source          a directory of containing plugin artifacts, or a file with suffix
64       *                        {@link #getListSuffix()} containing a list of paths to plugin artifacts.
65       * @param pluginFactories as per {@link ScanningPluginLoader}.
66       * @param eventManager    as per {@link ScanningPluginLoader}.
67       * @since 3.0.16
68       */
69      public BundledPluginLoader(
70              final File source,
71              final List<PluginFactory> pluginFactories,
72              final PluginEventManager eventManager) {
73          this(buildSourceScanner(source), pluginFactories, eventManager);
74      }
75  
76      /**
77       * Construct a bundled plugin loader for a zip source.
78       * <p>
79       * For backwards compatibility, if the zipUrl is in fact a file url (acccording to
80       * {@link FileUtils#toFile}), this constructor has the same semantics as
81       * {@link #BundledPluginLoader(File, List, PluginEventManager)} where the first argument
82       * is the file referred to by zipUrl, and the pluginPath argument is ignored. That
83       * constructor should be used directly in such cases.
84       * <p>
85       *
86       * @param zipUrl          a url to a zipFile containing plugin artifacts, or a file url to a directory or
87       *                        list file.
88       * @param pluginPath      path to the directory to expand zipUrl.
89       * @param pluginFactories as per {@link ScanningPluginLoader}.
90       * @param eventManager    as per {@link ScanningPluginLoader}.
91       */
92      public BundledPluginLoader(
93              final URL zipUrl,
94              final File pluginPath,
95              final List<PluginFactory> pluginFactories,
96              final PluginEventManager eventManager) {
97          this(buildZipScanner(zipUrl, pluginPath), pluginFactories, eventManager);
98      }
99  
100     @Override
101     protected Plugin postProcess(final Plugin plugin) {
102         if (plugin instanceof PluginInternal) {
103             ((PluginInternal) plugin).setBundledPlugin(true);
104         } else {
105             log.warn("unable to set bundled attribute on plugin '" + plugin + "' as it is of class " + plugin.getClass().getCanonicalName());
106         }
107         return plugin;
108     }
109 
110     private static Scanner buildScannerCommon(final File file) {
111         if (file.isDirectory()) {
112             // file points directly to a directory of jars
113             return new DirectoryScanner(file);
114         } else if (file.isFile() && file.getName().endsWith(getListSuffix())) {
115             // file contains a list of jars.
116             final List<File> files = readListFile(file);
117             return new FileListScanner(files);
118         } else {
119             // unknown - let caller figure out fallback
120             return null;
121         }
122     }
123 
124     private static Scanner buildSourceScanner(final File source) {
125         checkNotNull(source, "Source must not be null");
126         final Scanner scanner = buildScannerCommon(source);
127         if (null == scanner) {
128             log.error("Cannot build a scanner for source '{}'", source);
129             // This is approximately what the code used to do - produce a scanner which would have
130             // no entries (unless it accidentally had a random collection from a prior boot).
131             return new EmptyScanner();
132         } else {
133             return scanner;
134         }
135     }
136 
137     private static Scanner buildZipScanner(final URL url, final File pluginPath) {
138         // checkArgument used to preserve historical behaviour of throwing IllegalArgumentException
139         checkArgument(null != url, "Bundled plugins url cannot be null");
140 
141 
142         Scanner scanner = null;
143 
144         // Legacy behaviour - treat file:// urls as per buildSourceScanner, but we don't
145         // want the error or the empty scanner.
146         final File file = FileUtils.toFile(url);
147         if (null != file) {
148             scanner = buildScannerCommon(file);
149         }
150 
151         if (null == scanner) {
152             // Not handled by file:// urls, so it's a zip url
153             com.atlassian.plugin.util.FileUtils.conditionallyExtractZipFile(url, pluginPath);
154             scanner = new DirectoryScanner(pluginPath);
155         }
156 
157         return scanner;
158     }
159 
160     private static List<File> readListFile(final File file) {
161         try {
162             final List<String> fnames = Files.readAllLines(file.toPath());
163             final List<File> files = new ArrayList<>();
164             for (final String fname : fnames) {
165                 files.add(new File(fname));
166             }
167             return files;
168         } catch (IOException e) {
169             throw new IllegalStateException("Unable to read list from " + file, e);
170         }
171     }
172 
173     /**
174      * A forwarding scanner which suppresses remove operation.
175      * <p>
176      * Bundled plugins are never actually removed from the system, so we wrap the scanner to
177      * suppress removal.
178      */
179     private static class NonRemovingScanner extends ForwardingScanner {
180         NonRemovingScanner(final Scanner scanner) {
181             super(scanner);
182         }
183 
184         @Override
185         public void remove(final DeploymentUnit unit) throws PluginException {
186             // Suppressed - see class documentation.
187         }
188     }
189 }