View Javadoc
1   package com.atlassian.plugin.metadata;
2   
3   import com.atlassian.plugin.ModuleDescriptor;
4   import com.atlassian.plugin.Plugin;
5   import com.google.common.collect.ImmutableSet;
6   import org.apache.commons.io.IOUtils;
7   import org.apache.commons.lang3.StringUtils;
8   
9   import java.io.IOException;
10  import java.io.InputStream;
11  import java.net.URL;
12  import java.util.ArrayList;
13  import java.util.Collection;
14  import java.util.Enumeration;
15  import java.util.List;
16  import java.util.Set;
17  
18  import static org.apache.commons.io.IOUtils.readLines;
19  
20  /**
21   * Looks on the classpath for three files named: <ul> <li>application-provided-plugins.txt - used to list the plugin
22   * keys of all plugins that are provided by the host application</li> <li>application-required-plugins.txt - used to
23   * list the plugin keys that are considered required for the application to function correctly</li>
24   * <li>application-required-modules.txt - used to list the module keys that are considered required for the application
25   * to function correctly</li> </ul> Note that all files in that package space with those names will be included.
26   * <p>
27   * All files contents will be used to inform this implementation of plugin keys. This will read the contents all
28   * instances of those files into the structures of this class.
29   * <p>
30   * The values will determine the plugin metadata for this implementation.
31   *
32   * @since 2.6
33   */
34  public class ClasspathFilePluginMetadata implements PluginMetadata, RequiredPluginProvider {
35      final static String APPLICATION_PROVIDED_PLUGINS_FILENAME = "application-provided-plugins.txt";
36      final static String APPLICATION_REQUIRED_PLUGINS_FILENAME = "application-required-plugins.txt";
37      final static String APPLICATION_REQUIRED_MODULES_FILENAME = "application-required-modules.txt";
38  
39      private final Set<String> providedPluginKeys;
40      private final Set<String> requiredPluginKeys;
41      private final Set<String> requiredModuleKeys;
42      private final ClassLoader classLoader;
43  
44      public ClasspathFilePluginMetadata() {
45          this(ClasspathFilePluginMetadata.class.getClassLoader());
46      }
47  
48      ClasspathFilePluginMetadata(ClassLoader classLoader) {
49          this.classLoader = classLoader;
50          this.providedPluginKeys = getStringsFromFile(APPLICATION_PROVIDED_PLUGINS_FILENAME);
51          this.requiredPluginKeys = getStringsFromFile(APPLICATION_REQUIRED_PLUGINS_FILENAME);
52          this.requiredModuleKeys = getStringsFromFile(APPLICATION_REQUIRED_MODULES_FILENAME);
53      }
54  
55      public boolean applicationProvided(final Plugin plugin) {
56          return providedPluginKeys.contains(plugin.getKey());
57      }
58  
59      public boolean required(final Plugin plugin) {
60          return requiredPluginKeys.contains(plugin.getKey());
61      }
62  
63      public boolean required(final ModuleDescriptor<?> module) {
64          return requiredModuleKeys.contains(module.getCompleteKey());
65      }
66  
67      public Set<String> getRequiredPluginKeys() {
68          return requiredPluginKeys;
69      }
70  
71      public Set<String> getRequiredModuleKeys() {
72          return requiredModuleKeys;
73      }
74  
75      private Set<String> getStringsFromFile(final String fileName) {
76          final ImmutableSet.Builder<String> stringsFromFiles = ImmutableSet.builder();
77  
78          final Collection<InputStream> fileInputStreams = getInputStreamsForFilename(fileName);
79          try {
80              for (InputStream fileInputStream : fileInputStreams) {
81                  if (fileInputStream != null) {
82                      try {
83                          @SuppressWarnings("unchecked")
84                          final List<String> lines = readLines(fileInputStream);
85  
86                          // Make sure that we trim the strings that we read from the file and filter out comments and blank lines
87                          // NOTE: You could use a filter and a transformation but then you need either a private class
88                          // or a single enum which causes WAY TOO MUCH debate.
89                          for (String line : lines) {
90                              final String processedLine = processedLine(line);
91                              if (processedLine != null) {
92                                  stringsFromFiles.add(processedLine.intern());
93                              }
94                          }
95                      } catch (final IOException e) {
96                          throw new RuntimeException(e);
97                      }
98                  }
99              }
100         } finally {
101             for (InputStream fileInputStream : fileInputStreams) {
102                 IOUtils.closeQuietly(fileInputStream);
103             }
104         }
105         return stringsFromFiles.build();
106     }
107 
108     /**
109      * Checks the input to see if it meets the rules we want to enforce about valid lines. If it does not meet the
110      * criteria then null is returned, otherwise a trimmed version of the passed in string is returned.
111      *
112      * @param rawLine the string of data we are processing
113      * @return if rawLine does not meet the criteria then null is returned, otherwise a trimmed version of rawLine is
114      * returned.
115      */
116     private String processedLine(String rawLine) {
117         if (rawLine == null) {
118             return null;
119         }
120 
121         final String trimmedLine = rawLine.trim();
122         // Lets not include blank lines
123         if (StringUtils.isBlank(trimmedLine)) {
124             return null;
125         }
126 
127         // Lets not include comments
128         if (trimmedLine.startsWith("#")) {
129             return null;
130         }
131         return trimmedLine;
132     }
133 
134     Collection<InputStream> getInputStreamsForFilename(final String fileName) {
135         final Collection<InputStream> inputStreams = new ArrayList<>();
136         final Class<ClasspathFilePluginMetadata> clazz = ClasspathFilePluginMetadata.class;
137         final String resourceName = clazz.getPackage().getName().replace(".", "/") + "/" + fileName;
138         try {
139             final Enumeration<URL> urlEnumeration = classLoader.getResources(resourceName);
140             while (urlEnumeration.hasMoreElements()) {
141                 inputStreams.add(urlEnumeration.nextElement().openStream());
142             }
143         } catch (final IOException e) {
144             // Close what we had opened before, one bad apple ruins the batch
145             for (InputStream inputStream : inputStreams) {
146                 IOUtils.closeQuietly(inputStream);
147             }
148             throw new RuntimeException(e);
149         }
150         return inputStreams;
151     }
152 
153 }