1 package com.atlassian.maven.plugins.amps.osgi;
2
3 import org.apache.maven.plugin.logging.Log;
4 import org.apache.maven.project.MavenProject;
5 import org.apache.maven.plugin.MojoFailureException;
6 import org.apache.maven.artifact.Artifact;
7 import org.apache.commons.io.IOUtils;
8
9 import java.util.*;
10 import java.util.zip.ZipInputStream;
11 import java.util.zip.ZipEntry;
12 import java.io.File;
13 import java.io.FileInputStream;
14 import java.io.IOException;
15
16 import aQute.libg.header.OSGiHeader;
17
18
19
20
21
22
23 public class PackageImportVersionValidator
24 {
25 private final MavenProject project;
26 private final Log log;
27 private final String productName;
28 private final Map<String,Set<String>> jarPackageCache = new HashMap<String,Set<String>>();
29 private static final int MIN_PACKAGES_FOR_WILDCARD = 4;
30
31 public PackageImportVersionValidator(MavenProject project, Log log, String productName)
32 {
33 this.project = project;
34 this.log = log;
35 this.productName = productName;
36 }
37
38
39
40
41
42
43
44 public void validate(String imports)
45 {
46 if (imports != null)
47 {
48 Map<String,String> foundPackages = new HashMap<String,String>();
49 boolean validationFailed = false;
50
51 Map<String,Map<String,String>> pkgImports = OSGiHeader.parseHeader(imports);
52 for (Map.Entry<String,Map<String,String>> pkgImport : pkgImports.entrySet())
53 {
54 String pkg = pkgImport.getKey();
55 if (pkgImport.getValue() != null && pkgImport.getValue().size() > 0)
56 {
57 Map<String,String> props = pkgImport.getValue();
58 String version = props.get("version");
59 foundPackages.put(pkg, guessVersion( pkg));
60 if (version == null || version.length() == 0)
61 {
62 validationFailed = true;
63 }
64 }
65 else
66 {
67 validationFailed = true;
68 foundPackages.put(pkg, guessVersion(pkg));
69 }
70 }
71
72 if (validationFailed)
73 {
74 StringBuilder sb = new StringBuilder();
75 sb.append("The manifest should contain versions for all imports to prevent ambiguity at install time ");
76 sb.append("due to multiple versions of a package. Here are some suggestions for the ");
77 sb.append("maven-").append(productName).append("-plugin configuration generated for this project ");
78 sb.append("to start from:\n ");
79 sb.append(" <configuration>\n");
80 sb.append(" <instructions>\n");
81 sb.append(" <Import-Package>\n");
82 for (Map.Entry<String,String> entry : compressPackages(foundPackages).entrySet())
83 {
84 sb.append(" ").append(entry.getKey()).append(";version=\"").append(entry.getValue()).append("\",\n");
85 }
86 sb.delete(sb.length() - 2, sb.length());
87 sb.append("\n");
88 sb.append(" </Import-Package>\n");
89 sb.append(" </instructions>\n");
90 sb.append(" </configuration>\n");
91 sb.append("You may notice many packages you weren't expecting. This is usually because of a bundled jar ");
92 sb.append("that references packages that don't apply. You can usually remove these or if necessary, ");
93 sb.append("mark them as optional by adding ';resolution:=optional' to the package import. Packages ");
94 sb.append("that are detected as version '0.0.0' usually mean either they are JDK packages or ones that ");
95 sb.append("aren't referenced in your project, and therefore, likely candidates for removal entirely.");
96 log.warn(sb.toString());
97 }
98 }
99 }
100
101
102
103
104
105
106
107 static Map<String,String> compressPackages(Map<String, String> allPackages)
108 {
109 Map<String,String> pkgs = new HashMap<String,String>();
110 Set<String> unmatchedPackages = new TreeSet<String>(allPackages.keySet());
111
112
113 for (String pkg : new TreeSet<String>(allPackages.keySet()))
114 {
115
116 if (!unmatchedPackages.contains(pkg))
117 {
118 continue;
119 }
120 String version = allPackages.get(pkg);
121
122
123 Set<String> others = new TreeSet<String>(unmatchedPackages);
124 others.remove(pkg);
125
126
127 for (Iterator<String> i = others.iterator(); i.hasNext(); )
128 {
129 String otherPkg = i.next();
130 if (!allPackages.get(otherPkg).equals(version))
131 {
132 i.remove();
133 }
134 }
135
136
137
138
139 int numberOfPackages = 1;
140 for (int curpos = 0; curpos<pkg.length(); curpos++)
141 {
142 char curchar = pkg.charAt(curpos);
143 if (curchar == '.')
144 {
145 numberOfPackages++;
146 }
147
148 for (Iterator<String> i = others.iterator(); i.hasNext(); )
149 {
150 String otherPkg = i.next();
151
152
153 if (otherNotMatchesNextChar(curpos, curchar, otherPkg))
154 {
155 i.remove();
156 }
157 }
158
159 if (numberOfPackages == MIN_PACKAGES_FOR_WILDCARD || curpos == pkg.length() - 1)
160 {
161 if (others.size() > 0 && numberOfPackages == MIN_PACKAGES_FOR_WILDCARD)
162 {
163 String pattern = greedlyBuildPattern(pkg, others, curpos).toString();
164 pkgs.put(pattern + "*", version);
165 unmatchedPackages.removeAll(others);
166 }
167 else
168 {
169
170 pkgs.put(pkg, version);
171 }
172 unmatchedPackages.remove(pkg);
173 break;
174 }
175 }
176 }
177 return pkgs;
178 }
179
180 private static boolean otherNotMatchesNextChar(int curpos, char curchar, String other)
181 {
182 return other.length() <= curpos || curchar != other.charAt(curpos);
183 }
184
185
186
187
188
189
190
191
192 private static StringBuilder greedlyBuildPattern(String pkg, Set<String> others, int curpos)
193 {
194 StringBuilder pattern = new StringBuilder(pkg.substring(0, curpos + 1));
195 boolean canConsumeAnotherChar = true;
196 for (int greedyPos = curpos + 1; greedyPos < pkg.length() && canConsumeAnotherChar; greedyPos++)
197 {
198 for (String greedyOther : others)
199 {
200 canConsumeAnotherChar = true;
201 if (otherNotMatchesNextChar(greedyPos, pkg.charAt(greedyPos), greedyOther))
202 {
203 canConsumeAnotherChar = false;
204 break;
205 }
206 }
207 if (canConsumeAnotherChar)
208 {
209 pattern.append(pkg.charAt(greedyPos));
210 }
211 }
212 return pattern;
213 }
214
215
216
217
218
219
220
221 private String guessVersion(String pkg)
222 {
223 for (Artifact artifact : new HashSet<Artifact>(project.getArtifacts()))
224 {
225
226 File file = artifact.getFile();
227 if (file.exists() && file.getName().endsWith(".jar"))
228 {
229 Set<String> contents = jarPackageCache.get(file.getAbsolutePath());
230 if (contents == null)
231 {
232 contents = new HashSet<String>();
233 jarPackageCache.put(file.getAbsolutePath(), contents);
234 ZipInputStream in = null;
235 try
236 {
237 in = new ZipInputStream(new FileInputStream(file));
238 ZipEntry entry;
239 while ((entry = in.getNextEntry()) != null)
240 {
241 contents.add(entry.getName());
242 }
243 }
244 catch (IOException e)
245 {
246
247 }
248 finally
249 {
250 IOUtils.closeQuietly(in);
251 }
252 }
253
254 if (contents.contains(pkg.replace('.','/') + "/"))
255 {
256 return artifact.getVersion();
257 }
258 }
259 }
260 return "0.0.0";
261 }
262 }