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