View Javadoc
1   package com.atlassian.plugin;
2   
3   import com.google.common.collect.Sets;
4   import org.apache.commons.io.IOUtils;
5   import org.slf4j.Logger;
6   import org.slf4j.LoggerFactory;
7   
8   import java.io.BufferedInputStream;
9   import java.io.File;
10  import java.io.FileInputStream;
11  import java.io.FileNotFoundException;
12  import java.io.IOException;
13  import java.io.InputStream;
14  import java.util.Set;
15  import java.util.jar.JarFile;
16  import java.util.jar.Manifest;
17  import java.util.regex.Matcher;
18  import java.util.regex.Pattern;
19  import java.util.zip.ZipEntry;
20  
21  import static com.google.common.base.Preconditions.checkNotNull;
22  import static com.google.common.collect.Iterators.any;
23  import static com.google.common.collect.Iterators.filter;
24  import static com.google.common.collect.Iterators.forEnumeration;
25  import static com.google.common.collect.Iterators.transform;
26  
27  /**
28   * The implementation of PluginArtifact that is backed by a jar file.
29   *
30   * @see PluginArtifact
31   * @since 2.0.0
32   */
33  public final class JarPluginArtifact implements PluginArtifact, PluginArtifact.HasExtraModuleDescriptors {
34      private static final Logger log = LoggerFactory.getLogger(JarPluginArtifact.class);
35  
36      private final File jarFile;
37      final com.atlassian.plugin.ReferenceMode referenceMode;
38  
39      /**
40       * Construct a PluginArtifact for a jar file which does not allow reference installation.
41       *
42       * @param jarFile the jar file comprising the artifact.
43       */
44      public JarPluginArtifact(File jarFile) {
45          this(jarFile, com.atlassian.plugin.ReferenceMode.FORBID_REFERENCE);
46      }
47  
48      /**
49       * Construct a PluginArtifact for a jar file and specify whether reference installation is supported.
50       *
51       * @param jarFile       the jar file comprising the artifact.
52       * @param referenceMode specifies whether this artifact may be installed by reference.
53       */
54      public JarPluginArtifact(File jarFile, com.atlassian.plugin.ReferenceMode referenceMode) {
55          this.jarFile = checkNotNull(jarFile);
56          this.referenceMode = referenceMode;
57      }
58  
59      public boolean doesResourceExist(String name) {
60          InputStream in = null;
61          try {
62              in = getResourceAsStream(name);
63              return (in != null);
64          } finally {
65              IOUtils.closeQuietly(in);
66          }
67      }
68  
69      /**
70       * @return an input stream for the this file in the jar. Closing this stream also closes the jar file this stream comes from.
71       */
72      public InputStream getResourceAsStream(String fileName) throws PluginParseException {
73          checkNotNull(fileName, "The file name must not be null");
74  
75          final JarFile jar = open();
76          final ZipEntry entry = jar.getEntry(fileName);
77          if (entry == null) {
78              closeJarQuietly(jar);
79              return null;
80          }
81  
82          try {
83              return new BufferedInputStream(jar.getInputStream(entry)) {
84                  // because we do not expose a handle to the jar file this stream is associated with, we need to make sure
85                  // we explicitly close the jar file when we're done with the stream (else we'll have a file handle leak)
86                  public void close() throws IOException {
87                      super.close();
88                      jar.close();
89                  }
90              };
91          } catch (IOException e) {
92              throw new PluginParseException("Cannot retrieve " + fileName + " from plugin JAR [" + jarFile + "]", e);
93          }
94      }
95  
96      public String getName() {
97          return jarFile.getName();
98      }
99  
100     @Override
101     public String toString() {
102         return getName();
103     }
104 
105     /**
106      * @return a buffered file input stream of the file on disk. This input stream
107      * is not resettable.
108      */
109     public InputStream getInputStream() {
110         try {
111             return new BufferedInputStream(new FileInputStream(jarFile));
112         } catch (FileNotFoundException e) {
113             throw new PluginParseException("Could not open JAR file: " + jarFile, e);
114         }
115     }
116 
117     public File toFile() {
118         return jarFile;
119     }
120 
121     @Override
122     public boolean containsJavaExecutableCode() {
123         final JarFile jar = open();
124         try {
125             final Manifest manifest = getManifest(jar);
126             return hasBundleActivator(manifest)
127                     ||
128                     hasSpringContext(manifest)
129                     ||
130                     any(forEnumeration(jar.entries()),
131                             entry -> isJavaClass(entry) || isJavaLibrary(entry) || isSpringContext(entry));
132         } finally {
133             closeJarQuietly(jar);
134         }
135     }
136 
137     @Override
138     public boolean containsSpringContext() {
139         final JarFile jar = open();
140         try {
141             final Manifest manifest = getManifest(jar);
142             return hasSpringContext(manifest)
143                     ||
144                     any(forEnumeration(jar.entries()), this::isSpringContext);
145         } finally {
146             closeJarQuietly(jar);
147         }
148     }
149 
150     @Override
151     public Set<String> extraModuleDescriptorFiles(String rootFolder) {
152         final JarFile jar = open();
153         try {
154             final Matcher m = Pattern.compile(Pattern.quote(rootFolder) + "/[^/.]*\\.(?i)xml$").matcher("");
155             return Sets.newHashSet(transform(filter(forEnumeration(jar.entries()),
156                     entry -> {
157                         m.reset(entry.getName());
158                         return m.find();
159                     }), jarEntry -> jarEntry.getName()));
160         } finally {
161             closeJarQuietly(jar);
162         }
163     }
164 
165     @Override
166     public com.atlassian.plugin.ReferenceMode getReferenceMode() {
167         return referenceMode;
168     }
169 
170     private boolean isJavaClass(ZipEntry entry) {
171         return entry.getName().endsWith(".class");
172     }
173 
174     private boolean isJavaLibrary(ZipEntry entry) {
175         return entry.getName().endsWith(".jar");
176     }
177 
178     private boolean isSpringContext(ZipEntry entry) {
179         final String entryName = entry.getName();
180         return entryName.startsWith("META-INF/spring/") && entryName.endsWith(".xml");
181     }
182 
183     private boolean hasSpringContext(Manifest manifest) {
184         return hasManifestEntry(manifest, "Spring-Context");
185     }
186 
187     private boolean hasBundleActivator(Manifest manifest) {
188         return hasManifestEntry(manifest, "Bundle-Activator");
189     }
190 
191     private boolean hasManifestEntry(Manifest manifest, String manifestEntryName) {
192         return manifest != null
193                 && manifest.getMainAttributes() != null
194                 && manifest.getMainAttributes().getValue(manifestEntryName) != null;
195     }
196 
197     private JarFile open() {
198         try {
199             return new JarFile(jarFile);
200         } catch (IOException e) {
201             throw new PluginParseException("Cannot open JAR file: " + jarFile, e);
202         }
203     }
204 
205     private Manifest getManifest(JarFile jar) {
206         try {
207             return jar.getManifest();
208         } catch (IOException e) {
209             throw new PluginParseException("Cannot get manifest for JAR file: " + jarFile, e);
210         }
211     }
212 
213     private void closeJarQuietly(JarFile jar) {
214         if (jar != null) {
215             try {
216                 jar.close();
217             } catch (IOException e) {
218                 log.debug("Exception closing jar file {}.", jarFile, e);
219             }
220         }
221     }
222 }