View Javadoc

1   package com.atlassian.plugin.osgi.factory.transform.stage;
2   
3   import static com.atlassian.plugin.osgi.factory.transform.JarUtils.withJar;
4   import static com.google.common.collect.Iterables.transform;
5   
6   import com.atlassian.plugin.osgi.factory.transform.JarUtils;
7   import com.atlassian.plugin.osgi.factory.transform.PluginTransformationException;
8   
9   import org.apache.commons.io.IOUtils;
10  
11  import com.google.common.base.Function;
12  import com.google.common.collect.ImmutableSet;
13  
14  import java.io.File;
15  import java.io.IOException;
16  import java.io.InputStream;
17  import java.util.Collections;
18  import java.util.HashSet;
19  import java.util.Set;
20  import java.util.jar.JarEntry;
21  import java.util.jar.JarFile;
22  import java.util.jar.JarInputStream;
23  
24  /**
25   * Contains utility functions for use in TransformStage implementations.
26   *
27   * @since 2.6.0 
28   */
29  final class TransformStageUtils
30  {
31      /**
32       * Not for instantiation.
33       */
34      private TransformStageUtils()
35      {}
36  
37      /**
38       * Scan entries in jar for expectedItems.
39       * It exits early once all the expected items are satisfied.
40       *
41       * @param inputStream the input jar entry stream, cannot be null. Caller is responsible for closing it.
42       * @param expectedItems items expected from the stream, cannot be null.
43       * @param mapper function which maps JarEntry to item which then can be matched against expectedItems, cannot be null.
44       *
45       * @return the set of matched items.
46       * @throws IOException if the inputStream can't find the next entry or it is somehow corrupt
47       */
48      static Set<String> scanJarForItems(final JarInputStream inputStream, final Set<String> expectedItems, final Function<JarEntry, String> mapper) throws IOException
49      {
50          final Set<String> matches = new HashSet<String>();
51  
52          JarEntry entry;
53          while ((entry = inputStream.getNextJarEntry()) != null)
54          {
55              final String item = mapper.apply(entry);
56              if ((item != null) && expectedItems.contains(item))
57              {
58                  matches.add(item);
59                  // early exit opportunity
60                  if (matches.size() == expectedItems.size())
61                  {
62                      break;
63                  }
64              }
65          }
66  
67          return Collections.unmodifiableSet(matches);
68      }
69  
70      /**
71       * Scan inner jars for expected classes.
72       * Early exit once all the required classes are satisfied.
73       *
74       * @param pluginFile the plugin jar file, cannot be null.
75       * @param innerJars the inner jars to look at, never null. This is because there can be inner jars that we're not interested in the plugin.
76       * @param expectedClasses the classes that we expect to find, never null.
77       *
78       * @return the set of classes matched, never null.
79       */
80      static Set<String> scanInnerJars(final File pluginFile, final Set<String> innerJars, final Set<String> expectedClasses)
81      {
82          return withJar(pluginFile, new JarUtils.Extractor<Set<String>>()
83          {
84              public Set<String> get(final JarFile pluginJarFile)
85              {
86                  // this keeps track of all the matches.
87                  final Set<String> matches = new HashSet<String>();
88  
89                  // scan each inner jar.
90                  for (final String innerJar : innerJars)
91                  {
92                      JarInputStream innerJarStream = null;
93                      try
94                      {
95                          // read inner jar into JarInputStream.
96                          innerJarStream = new JarInputStream(pluginJarFile.getInputStream(pluginJarFile.getEntry(innerJar)));
97                          final Set<String> innerMatches = scanJarForItems(innerJarStream, expectedClasses, JarEntryToClassName.INSTANCE);
98                          // recalculate the matches.
99                          matches.addAll(innerMatches);
100                     }
101                     catch (final IOException ioe)
102                     {
103                         throw new PluginTransformationException("Error reading inner jar:" + innerJar + " in file: " + pluginFile, ioe);
104                     }
105                     finally
106                     {
107                         closeNestedStreamQuietly(innerJarStream);
108                     }
109 
110                     // early exit.
111                     if (matches.size() == expectedClasses.size())
112                     {
113                         break;
114                     }
115                 }
116                 return Collections.unmodifiableSet(matches);
117             }
118         });
119     }
120 
121     /**
122      * Try to close the given streams in order.
123      * Exit once one is closed.
124      *
125      * @param streams streams to be closed. The higher ones must come first.
126      */
127     static void closeNestedStreamQuietly(final InputStream... streams)
128     {
129         for (final InputStream stream : streams)
130         {
131             if (stream != null)
132             {
133                 IOUtils.closeQuietly(stream);
134                 break;
135             }
136         }
137     }
138 
139     /**
140      * Extracts package name from the given class name.
141      *
142      * @param fullClassName a valid class name.
143      * @return package name.
144      */
145     static String getPackageName(final String fullClassName)
146     {
147         return PackageName.INSTANCE.apply(fullClassName);
148     }
149 
150     /**
151      * Extract package names from the given set of classes.
152      *
153      * @param classes set of classes, cannot be null.
154      * @return a set of package names, can be empty but never null.
155      */
156     static Set<String> getPackageNames(final Iterable<String> classes)
157     {
158         return ImmutableSet.copyOf(transform(classes, PackageName.INSTANCE));
159     }
160 
161     /**
162      * Convert a jar path to class name.
163      * such as "com/atlassian/Test.class" -> "com.atlassian.Test".
164      *
165      * @param jarPath the entry name inside jar.
166      * @return class name, or null if the path is not a class file.
167      */
168     static String jarPathToClassName(final String jarPath)
169     {
170         if ((jarPath == null) || !jarPath.contains(".class"))
171         {
172             return null;
173         }
174 
175         return jarPath.replaceAll("/", ".").substring(0, jarPath.length() - ".class".length());
176     }
177 
178     /**
179      * Class name -> package name transformer.
180      */
181     enum PackageName implements Function<String, String>
182     {
183         INSTANCE;
184 
185         public String apply(final String fullClassName)
186         {
187             // A valid java class name must have a dot in it.
188             return fullClassName.substring(0, fullClassName.lastIndexOf("."));
189         }
190     }
191 
192     /**
193      * Maps jarEntry -> class name.
194      */
195     enum JarEntryToClassName implements Function<JarEntry, String>
196     {
197         INSTANCE;
198 
199         public String apply(final JarEntry entry)
200         {
201             final String jarPath = entry.getName();
202             if ((jarPath == null) || !jarPath.contains(".class"))
203             {
204                 return null;
205             }
206 
207             return jarPath.replaceAll("/", ".").substring(0, jarPath.length() - ".class".length());
208         }
209     }
210 }