blob: 3126056ff982aa380d3cda7b70dda8aa336a53d4 [file] [log] [blame]
Stuart McCullochbb014372012-06-07 21:57:32 +00001package aQute.lib.osgi;
2
3import java.io.*;
4import java.util.*;
5import java.util.Map.Entry;
6import java.util.jar.*;
7import java.util.regex.*;
8import java.util.zip.*;
9
10import aQute.bnd.component.*;
11import aQute.bnd.differ.*;
12import aQute.bnd.differ.Baseline.Info;
13import aQute.bnd.make.*;
14import aQute.bnd.make.component.*;
15import aQute.bnd.make.metatype.*;
16import aQute.bnd.maven.*;
17import aQute.bnd.service.*;
18import aQute.bnd.service.diff.*;
19import aQute.lib.collections.*;
20import aQute.lib.osgi.Descriptors.PackageRef;
21import aQute.lib.osgi.Descriptors.TypeRef;
22import aQute.libg.generics.*;
23import aQute.libg.header.*;
24
25/**
26 * Include-Resource: ( [name '=' ] file )+
27 *
28 * Private-Package: package-decl ( ',' package-decl )*
29 *
30 * Export-Package: package-decl ( ',' package-decl )*
31 *
32 * Import-Package: package-decl ( ',' package-decl )*
33 *
34 * @version $Revision$
35 */
36public class Builder extends Analyzer {
37 static Pattern IR_PATTERN = Pattern
38 .compile("[{]?-?@?(?:[^=]+=)?\\s*([^}!]+).*");
39 private final DiffPluginImpl differ = new DiffPluginImpl();
40 private Pattern xdoNotCopy = null;
41 private static final int SPLIT_MERGE_LAST = 1;
42 private static final int SPLIT_MERGE_FIRST = 2;
43 private static final int SPLIT_ERROR = 3;
44 private static final int SPLIT_FIRST = 4;
45 private static final int SPLIT_DEFAULT = 0;
46 private final List<File> sourcePath = new ArrayList<File>();
47 private final Make make = new Make(this);
48
49 public Builder(Processor parent) {
50 super(parent);
51 }
52
53 public Builder() {
54 }
55
56 public Jar build() throws Exception {
57 trace("build");
58 init();
59 if (isTrue(getProperty(NOBUNDLES)))
60 return null;
61
62 if (getProperty(CONDUIT) != null)
63 error("Specified " + CONDUIT
64 + " but calls build() instead of builds() (might be a programmer error");
65
66 Jar dot = new Jar("dot");
67 try {
68 long modified = Long.parseLong(getProperty("base.modified"));
69 dot.updateModified(modified, "Base modified");
70 }
71 catch (Exception e) {
72 // Ignore
73 }
74 setJar(dot);
75
76 doExpand(dot);
77 doIncludeResources(dot);
78 doWab(dot);
79
80 // Check if we override the calculation of the
81 // manifest. We still need to calculated it because
82 // we need to have analyzed the classpath.
83
84 Manifest manifest = calcManifest();
85
86 String mf = getProperty(MANIFEST);
87 if (mf != null) {
88 File mff = getFile(mf);
89 if (mff.isFile()) {
90 try {
91 InputStream in = new FileInputStream(mff);
92 manifest = new Manifest(in);
93 in.close();
94 }
95 catch (Exception e) {
96 error(MANIFEST + " while reading manifest file", e);
97 }
98 }
99 else {
100 error(MANIFEST + ", no such file " + mf);
101 }
102 }
103
104 if (getProperty(NOMANIFEST) == null)
105 dot.setManifest(manifest);
106 else
107 dot.setDoNotTouchManifest();
108
109 // This must happen after we analyzed so
110 // we know what it is on the classpath
111 addSources(dot);
112
113 if (getProperty(POM) != null)
114 dot.putResource("pom.xml", new PomResource(dot.getManifest()));
115
116 if (!isNoBundle())
117 doVerify(dot);
118
119 if (dot.getResources().isEmpty())
120 warning("The JAR is empty: The instructions for the JAR named %s did not cause any content to be included, this is likely wrong",
121 getBsn());
122
123 dot.updateModified(lastModified(), "Last Modified Processor");
124 dot.setName(getBsn());
125
126 sign(dot);
127 doDigests(dot);
128 doSaveManifest(dot);
129
130 doDiff(dot); // check if need to diff this bundle
131 doBaseline(dot); // check for a baseline
132 return dot;
133 }
134
135 /**
136 * Check if we need to calculate any checksums.
137 *
138 * @param dot
139 * @throws Exception
140 */
141 private void doDigests(Jar dot) throws Exception {
142 Parameters ps = OSGiHeader.parseHeader(getProperty(DIGESTS));
143 if (ps.isEmpty())
144 return;
145 trace("digests %s", ps);
146 String[] digests = ps.keySet().toArray(new String[ps.size()]);
147 dot.calcChecksums(digests);
148 }
149
150 /**
151 * Allow any local initialization by subclasses before we build.
152 */
153 public void init() throws Exception {
154 begin();
155 doRequireBnd();
156
157 // Check if we have sensible setup
158
159 if (getClasspath().size() == 0
160 && (getProperty(EXPORT_PACKAGE) != null || getProperty(EXPORT_PACKAGE) != null || getProperty(PRIVATE_PACKAGE) != null))
161 warning("Classpath is empty. Private-Package and Export-Package can only expand from the classpath when there is one");
162
163 }
164
165 /**
166 * Turn this normal bundle in a web and add any resources.
167 *
168 * @throws Exception
169 */
170 private Jar doWab(Jar dot) throws Exception {
171 String wab = getProperty(WAB);
172 String wablib = getProperty(WABLIB);
173 if (wab == null && wablib == null)
174 return dot;
175
176 trace("wab %s %s", wab, wablib);
177 setBundleClasspath(append("WEB-INF/classes", getProperty(BUNDLE_CLASSPATH)));
178
179 Set<String> paths = new HashSet<String>(dot.getResources().keySet());
180
181 for (String path : paths) {
182 if (path.indexOf('/') > 0 && !Character.isUpperCase(path.charAt(0))) {
183 trace("wab: moving: %s", path);
184 dot.rename(path, "WEB-INF/classes/" + path);
185 }
186 }
187
188 Parameters clauses = parseHeader(getProperty(WABLIB));
189 for (String key : clauses.keySet()) {
190 File f = getFile(key);
191 addWabLib(dot, f);
192 }
193 doIncludeResource(dot, wab);
194 return dot;
195 }
196
197 /**
198 * Add a wab lib to the jar.
199 *
200 * @param f
201 */
202 private void addWabLib(Jar dot, File f) throws Exception {
203 if (f.exists()) {
204 Jar jar = new Jar(f);
205 jar.setDoNotTouchManifest();
206 addClose(jar);
207 String path = "WEB-INF/lib/" + f.getName();
208 dot.putResource(path, new JarResource(jar));
209 setProperty(BUNDLE_CLASSPATH, append(getProperty(BUNDLE_CLASSPATH), path));
210
211 Manifest m = jar.getManifest();
212 String cp = m.getMainAttributes().getValue("Class-Path");
213 if (cp != null) {
214 Collection<String> parts = split(cp, ",");
215 for (String part : parts) {
216 File sub = getFile(f.getParentFile(), part);
217 if (!sub.exists() || !sub.getParentFile().equals(f.getParentFile())) {
218 warning("Invalid Class-Path entry %s in %s, must exist and must reside in same directory",
219 sub, f);
220 }
221 else {
222 addWabLib(dot, sub);
223 }
224 }
225 }
226 }
227 else {
228 error("WAB lib does not exist %s", f);
229 }
230 }
231
232 /**
233 * Get the manifest and write it out separately if -savemanifest is set
234 *
235 * @param dot
236 */
237 private void doSaveManifest(Jar dot) throws Exception {
238 String output = getProperty(SAVEMANIFEST);
239 if (output == null)
240 return;
241
242 File f = getFile(output);
243 if (f.isDirectory()) {
244 f = new File(f, "MANIFEST.MF");
245 }
246 f.delete();
247 f.getParentFile().mkdirs();
248 OutputStream out = new FileOutputStream(f);
249 try {
250 Jar.writeManifest(dot.getManifest(), out);
251 }
252 finally {
253 out.close();
254 }
255 changedFile(f);
256 }
257
258 protected void changedFile(File f) {
259 }
260
261 /**
262 * Sign the jar file.
263 *
264 * -sign : <alias> [ ';' 'password:=' <password> ] [ ';' 'keystore:='
265 * <keystore> ] [ ';' 'sign-password:=' <pw> ] ( ',' ... )*
266 *
267 * @return
268 */
269
270 void sign(Jar jar) throws Exception {
271 String signing = getProperty("-sign");
272 if (signing == null)
273 return;
274
275 trace("Signing %s, with %s", getBsn(), signing);
276 List<SignerPlugin> signers = getPlugins(SignerPlugin.class);
277
278 Parameters infos = parseHeader(signing);
279 for (Entry<String, Attrs> entry : infos.entrySet()) {
280 for (SignerPlugin signer : signers) {
281 signer.sign(this, entry.getKey());
282 }
283 }
284 }
285
286 public boolean hasSources() {
287 return isTrue(getProperty(SOURCES));
288 }
289
290 /**
291 * Answer extra packages. In this case we implement conditional package. Any
292 */
293 protected Jar getExtra() throws Exception {
294 Parameters conditionals = getParameters(CONDITIONAL_PACKAGE);
295 if (conditionals.isEmpty())
296 return null;
297 Instructions instructions = new Instructions(conditionals);
298
299 Collection<PackageRef> referred = instructions.select(getReferred().keySet(), false);
300 referred.removeAll(getContained().keySet());
301
302 Jar jar = new Jar("conditional-import");
303 addClose(jar);
304 for (PackageRef pref : referred) {
305 for (Jar cpe : getClasspath()) {
306 Map<String, Resource> map = cpe.getDirectories().get(pref.getPath());
307 if (map != null) {
308 jar.addDirectory(map, false);
309 break;
310 }
311 }
312 }
313 if (jar.getDirectories().size() == 0)
314 return null;
315 return jar;
316 }
317
318 /**
319 * Intercept the call to analyze and cleanup versions after we have analyzed
320 * the setup. We do not want to cleanup if we are going to verify.
321 */
322
323 public void analyze() throws Exception {
324 super.analyze();
325 cleanupVersion(getImports(), null);
326 cleanupVersion(getExports(), getVersion());
327 String version = getProperty(BUNDLE_VERSION);
328 if (version != null) {
329 version = cleanupVersion(version);
330 if (version.endsWith(".SNAPSHOT")) {
331 version = version.replaceAll("SNAPSHOT$", getProperty(SNAPSHOT, "SNAPSHOT"));
332 }
333 setProperty(BUNDLE_VERSION, version);
334 }
335 }
336
337 public void cleanupVersion(Packages packages, String defaultVersion) {
338 for (Map.Entry<PackageRef, Attrs> entry : packages.entrySet()) {
339 Attrs attributes = entry.getValue();
340 String v = attributes.get(Constants.VERSION_ATTRIBUTE);
341 if (v == null && defaultVersion != null) {
342 if (!isTrue(getProperty(Constants.NODEFAULTVERSION))) {
343 v = defaultVersion;
344 if (isPedantic())
345 warning("Used bundle version %s for exported package %s", v, entry.getKey());
346 }
347 else {
348 if (isPedantic())
349 warning("No export version for exported package %s", entry.getKey());
350 }
351 }
352 if (v != null)
353 attributes.put(Constants.VERSION_ATTRIBUTE, cleanupVersion(v));
354 }
355 }
356
357 /**
358 *
359 */
360 private void addSources(Jar dot) {
361 if (!hasSources())
362 return;
363
364 Set<PackageRef> packages = Create.set();
365
366 for (TypeRef typeRef : getClassspace().keySet()) {
367 PackageRef packageRef = typeRef.getPackageRef();
368 String sourcePath = typeRef.getSourcePath();
369 String packagePath = packageRef.getPath();
370
371 boolean found = false;
372 String[] fixed = {"packageinfo", "package.html", "module-info.java",
373 "package-info.java"};
374
375 for (Iterator<File> i = getSourcePath().iterator(); i.hasNext();) {
376 File root = i.next();
377
378 // TODO should use bcp?
379
380 File f = getFile(root, sourcePath);
381 if (f.exists()) {
382 found = true;
383 if (!packages.contains(packageRef)) {
384 packages.add(packageRef);
385 File bdir = getFile(root, packagePath);
386 for (int j = 0; j < fixed.length; j++) {
387 File ff = getFile(bdir, fixed[j]);
388 if (ff.isFile()) {
389 String name = "OSGI-OPT/src/" + packagePath + "/" + fixed[j];
390 dot.putResource(name, new FileResource(ff));
391 }
392 }
393 }
394 if (packageRef.isDefaultPackage())
395 System.err.println("Duh?");
396 dot.putResource("OSGI-OPT/src/" + sourcePath, new FileResource(f));
397 }
398 }
399 if (!found) {
400 for (Jar jar : getClasspath()) {
401 Resource resource = jar.getResource(sourcePath);
402 if (resource != null) {
403 dot.putResource("OSGI-OPT/src/" + sourcePath, resource);
404 }
405 else {
406 resource = jar.getResource("OSGI-OPT/src/" + sourcePath);
407 if (resource != null) {
408 dot.putResource("OSGI-OPT/src/" + sourcePath, resource);
409 }
410 }
411 }
412 }
413 if (getSourcePath().isEmpty())
414 warning("Including sources but " + SOURCEPATH
415 + " does not contain any source directories ");
416 // TODO copy from the jars where they came from
417 }
418 }
419
420 boolean firstUse = true;
421 private Tree tree;
422
423 public Collection<File> getSourcePath() {
424 if (firstUse) {
425 firstUse = false;
426 String sp = getProperty(SOURCEPATH);
427 if (sp != null) {
428 Parameters map = parseHeader(sp);
429 for (Iterator<String> i = map.keySet().iterator(); i.hasNext();) {
430 String file = i.next();
431 if (!isDuplicate(file)) {
432 File f = getFile(file);
433 if (!f.isDirectory()) {
434 error("Adding a sourcepath that is not a directory: " + f);
435 }
436 else {
437 sourcePath.add(f);
438 }
439 }
440 }
441 }
442 }
443 return sourcePath;
444 }
445
446 private void doVerify(Jar dot) throws Exception {
447 Verifier verifier = new Verifier(this);
448 // Give the verifier the benefit of our analysis
449 // prevents parsing the files twice
450 verifier.verify();
451 getInfo(verifier);
452 }
453
454 private void doExpand(Jar dot) throws IOException {
455
456 // Build an index of the class path that we can then
457 // use destructively
458 MultiMap<String, Jar> packages = new MultiMap<String, Jar>();
459 for (Jar srce : getClasspath()) {
460 for (Entry<String, Map<String, Resource>> e : srce.getDirectories().entrySet()) {
461 if (e.getValue() != null)
462 packages.add(e.getKey(), srce);
463 }
464 }
465
466 Parameters privatePackages = getPrivatePackage();
467 if (isTrue(getProperty(Constants.UNDERTEST))) {
468 String h = getProperty(Constants.TESTPACKAGES, "test;presence:=optional");
469 privatePackages.putAll(parseHeader(h));
470 }
471
472 if (!privatePackages.isEmpty()) {
473 Instructions privateFilter = new Instructions(privatePackages);
474 Set<Instruction> unused = doExpand(dot, packages, privateFilter);
475
476 if (!unused.isEmpty()) {
477 warning("Unused Private-Package instructions, no such package(s) on the class path: %s",
478 unused);
479 }
480 }
481
482 Parameters exportedPackage = getExportPackage();
483 if (!exportedPackage.isEmpty()) {
484 Instructions exportedFilter = new Instructions(exportedPackage);
485
486 // We ignore unused instructions for exports, they should show
487 // up as errors during analysis. Otherwise any overlapping
488 // packages with the private packages should show up as
489 // unused
490
491 doExpand(dot, packages, exportedFilter);
492 }
493 }
494
495 /**
496 * Destructively filter the packages from the build up index. This index is
497 * used by the Export Package as well as the Private Package
498 *
499 * @param jar
500 * @param name
501 * @param instructions
502 */
503 private Set<Instruction> doExpand(Jar jar, MultiMap<String, Jar> index, Instructions filter) {
504 Set<Instruction> unused = Create.set();
505
506 for (Entry<Instruction, Attrs> e : filter.entrySet()) {
507 Instruction instruction = e.getKey();
508 if (instruction.isDuplicate())
509 continue;
510
511 Attrs directives = e.getValue();
512
513 // We can optionally filter on the
514 // source of the package. We assume
515 // they all match but this can be overridden
516 // on the instruction
517 Instruction from = new Instruction(directives.get(FROM_DIRECTIVE, "*"));
518
519 boolean used = false;
520
521 for (Iterator<Entry<String, List<Jar>>> entry = index.entrySet().iterator(); entry
522 .hasNext();) {
523 Entry<String, List<Jar>> p = entry.next();
524
525 String directory = p.getKey();
526 PackageRef packageRef = getPackageRef(directory);
527
528 // Skip * and meta data, we're talking packages!
529 if (packageRef.isMetaData() && instruction.isAny())
530 continue;
531
532 if (!instruction.matches(packageRef.getFQN()))
533 continue;
534
535 // Ensure it is never matched again
536 entry.remove();
537
538 // ! effectively removes it from consideration by others (this
539 // includes exports)
540 if (instruction.isNegated())
541 continue;
542
543 // Do the from: directive, filters on the JAR type
544 List<Jar> providers = filterFrom(from, p.getValue());
545 if (providers.isEmpty())
546 continue;
547
548 int splitStrategy = getSplitStrategy(directives.get(SPLIT_PACKAGE_DIRECTIVE));
549 copyPackage(jar, providers, directory, splitStrategy);
550
551 used = true;
552 }
553
554 if (!used && !isTrue(directives.get("optional:")))
555 unused.add(instruction);
556 }
557 return unused;
558 }
559
560 /**
561 * @param from
562 * @return
563 */
564 private List<Jar> filterFrom(Instruction from, List<Jar> providers) {
565 if (from.isAny())
566 return providers;
567
568 List<Jar> np = new ArrayList<Jar>();
569 for (Iterator<Jar> i = providers.iterator(); i.hasNext();) {
570 Jar j = i.next();
571 if (from.matches(j.getName())) {
572 np.add(j);
573 }
574 }
575 return np;
576 }
577
578 /**
579 * Copy the package from the providers based on the split package strategy.
580 *
581 * @param dest
582 * @param providers
583 * @param directory
584 * @param splitStrategy
585 */
586 private void copyPackage(Jar dest, List<Jar> providers, String path, int splitStrategy) {
587 switch (splitStrategy) {
588 case SPLIT_MERGE_LAST :
589 for (Jar srce : providers) {
590 copy(dest, srce, path, true);
591 }
592 break;
593
594 case SPLIT_MERGE_FIRST :
595 for (Jar srce : providers) {
596 copy(dest, srce, path, false);
597 }
598 break;
599
600 case SPLIT_ERROR :
601 error(diagnostic(path, providers));
602 break;
603
604 case SPLIT_FIRST :
605 copy(dest, providers.get(0), path, false);
606 break;
607
608 default :
609 if (providers.size() > 1)
610 warning("%s", diagnostic(path, providers));
611 for (Jar srce : providers) {
612 copy(dest, srce, path, false);
613 }
614 break;
615 }
616 }
617
618 /**
619 * Cop
620 *
621 * @param dest
622 * @param srce
623 * @param path
624 * @param overwriteResource
625 */
626 private void copy(Jar dest, Jar srce, String path, boolean overwrite) {
627 dest.copy(srce, path, overwrite);
628
629 String key = path + "/bnd.info";
630 Resource r = dest.getResource(key);
631 if (r != null)
632 dest.putResource(key, new PreprocessResource(this, r));
633
634 if (hasSources()) {
635 String srcPath = "OSGI-OPT/src/" + path;
636 Map<String, Resource> srcContents = srce.getDirectories().get(srcPath);
637 if (srcContents != null) {
638 dest.addDirectory(srcContents, overwrite);
639 }
640 }
641 }
642
643 /**
644 * Analyze the classpath for a split package
645 *
646 * @param pack
647 * @param classpath
648 * @param source
649 * @return
650 */
651 private String diagnostic(String pack, List<Jar> culprits) {
652 // Default is like merge-first, but with a warning
653 return "Split package, multiple jars provide the same package:"
654 + pack
655 + "\nUse Import/Export Package directive -split-package:=(merge-first|merge-last|error|first) to get rid of this warning\n"
656 + "Package found in " + culprits + "\n" //
657 + "Class path " + getClasspath();
658 }
659
660 private int getSplitStrategy(String type) {
661 if (type == null)
662 return SPLIT_DEFAULT;
663
664 if (type.equals("merge-last"))
665 return SPLIT_MERGE_LAST;
666
667 if (type.equals("merge-first"))
668 return SPLIT_MERGE_FIRST;
669
670 if (type.equals("error"))
671 return SPLIT_ERROR;
672
673 if (type.equals("first"))
674 return SPLIT_FIRST;
675
676 error("Invalid strategy for split-package: " + type);
677 return SPLIT_DEFAULT;
678 }
679
680 /**
681 * Matches the instructions against a package.
682 *
683 * @param instructions The list of instructions
684 * @param pack The name of the package
685 * @param unused The total list of patterns, matched patterns are removed
686 * @param source The name of the source container, can be filtered upon with
687 * the from: directive.
688 * @return
689 */
690 private Instruction matches(Instructions instructions, String pack, Set<Instruction> unused,
691 String source) {
692 for (Entry<Instruction, Attrs> entry : instructions.entrySet()) {
693 Instruction pattern = entry.getKey();
694
695 // It is possible to filter on the source of the
696 // package with the from: directive. This is an
697 // instruction that must match the name of the
698 // source class path entry.
699
700 String from = entry.getValue().get(FROM_DIRECTIVE);
701 if (from != null) {
702 Instruction f = new Instruction(from);
703 if (!f.matches(source) || f.isNegated())
704 continue;
705 }
706
707 // Now do the normal
708 // matching
709 if (pattern.matches(pack)) {
710 if (unused != null)
711 unused.remove(pattern);
712 return pattern;
713 }
714 }
715 return null;
716 }
717
718 /**
719 * Parse the Bundle-Includes header. Files in the bundles Include header are
720 * included in the jar. The source can be a directory or a file.
721 *
722 * @throws IOException
723 * @throws FileNotFoundException
724 */
725 private void doIncludeResources(Jar jar) throws Exception {
726 String includes = getProperty("Bundle-Includes");
727 if (includes == null) {
728 includes = getProperty(INCLUDERESOURCE);
729 if (includes == null || includes.length() == 0)
730 includes = getProperty("Include-Resource");
731 }
732 else
733 warning("Please use -includeresource instead of Bundle-Includes");
734
735 doIncludeResource(jar, includes);
736
737 }
738
739 private void doIncludeResource(Jar jar, String includes) throws Exception {
740 Parameters clauses = parseHeader(includes);
741 doIncludeResource(jar, clauses);
742 }
743
744 private void doIncludeResource(Jar jar, Parameters clauses) throws ZipException, IOException,
745 Exception {
746 for (Entry<String, Attrs> entry : clauses.entrySet()) {
747 doIncludeResource(jar, entry.getKey(), entry.getValue());
748 }
749 }
750
751 private void doIncludeResource(Jar jar, String name, Map<String, String> extra)
752 throws ZipException, IOException, Exception {
753
754 boolean preprocess = false;
755 boolean absentIsOk = false;
756
757 if (name.startsWith("{") && name.endsWith("}")) {
758 preprocess = true;
759 name = name.substring(1, name.length() - 1).trim();
760 }
761
762 String parts[] = name.split("\\s*=\\s*");
763 String source = parts[0];
764 String destination = parts[0];
765 if (parts.length == 2)
766 source = parts[1];
767
768 if (source.startsWith("-")) {
769 source = source.substring(1);
770 absentIsOk = true;
771 }
772
773 if (source.startsWith("@")) {
774 extractFromJar(jar, source.substring(1), parts.length == 1 ? "" : destination,
775 absentIsOk);
776 }
777 else
778 if (extra.containsKey("cmd")) {
779 doCommand(jar, source, destination, extra, preprocess, absentIsOk);
780 }
781 else
782 if (extra.containsKey("literal")) {
783 String literal = extra.get("literal");
784 Resource r = new EmbeddedResource(literal.getBytes("UTF-8"), 0);
785 String x = extra.get("extra");
786 if (x != null)
787 r.setExtra(x);
788 jar.putResource(name, r);
789 }
790 else {
791 File sourceFile;
792 String destinationPath;
793
794 sourceFile = getFile(source);
795 if (parts.length == 1) {
796 // Directories should be copied to the root
797 // but files to their file name ...
798 if (sourceFile.isDirectory())
799 destinationPath = "";
800 else
801 destinationPath = sourceFile.getName();
802 }
803 else {
804 destinationPath = parts[0];
805 }
806 // Handle directories
807 if (sourceFile.isDirectory()) {
808 destinationPath = doResourceDirectory(jar, extra, preprocess, sourceFile,
809 destinationPath);
810 return;
811 }
812
813 // destinationPath = checkDestinationPath(destinationPath);
814
815 if (!sourceFile.exists()) {
816 if (absentIsOk)
817 return;
818
819 noSuchFile(jar, name, extra, source, destinationPath);
820 }
821 else
822 copy(jar, destinationPath, sourceFile, preprocess, extra);
823 }
824 }
825
826 /**
827 * It is possible in Include-Resource to use a system command that generates
828 * the contents, this is indicated with {@code cmd} attribute. The command
829 * can be repeated for a number of source files with the {@code for}
830 * attribute which indicates a list of repetitions, often down with the
831 * {@link Macro#_lsa(String[])} or {@link Macro#_lsb(String[])} macro. The
832 * repetition will repeat the given command for each item. The @} macro can
833 * be used to replace the current item. If no {@code for} is given, the
834 * source is used as the only item.
835 *
836 * If the destination contains a macro, each iteration will create a new
837 * file, otherwise the destination name is used.
838 *
839 * The execution of the command is delayed until the JAR is actually written
840 * to the file system for performance reasons.
841 *
842 * @param jar
843 * @param source
844 * @param destination
845 * @param extra
846 * @param preprocess
847 * @param absentIsOk
848 */
849 private void doCommand(Jar jar, String source, String destination, Map<String, String> extra,
850 boolean preprocess, boolean absentIsOk) {
851 String repeat = extra.get("for"); // TODO constant
852 if (repeat == null)
853 repeat = source;
854
855 Collection<String> requires = split(extra.get("requires"));
856 long lastModified = 0;
857 for (String required : requires) {
858 File file = getFile(required);
859 if (!file.isFile()) {
860 error("Include-Resource.cmd for %s, requires %s, but no such file %s", source,
861 required, file.getAbsoluteFile());
862 }
863 else
864 lastModified = Math.max(lastModified, file.lastModified());
865 }
866
867 String cmd = extra.get("cmd");
868
869 Collection<String> items = Processor.split(repeat);
870
871 CombinedResource cr = null;
872
873 if (!destination.contains("${@}")) {
874 cr = new CombinedResource();
875 }
876 trace("last modified requires %s", lastModified);
877
878 for (String item : items) {
879 setProperty("@", item);
880 try {
881 String path = getReplacer().process(destination);
882 String command = getReplacer().process(cmd);
883 File file = getFile(item);
884
885 Resource r = new CommandResource(command, this, Math.max(lastModified,
886 file.exists() ? file.lastModified():0L));
887
888 if (preprocess)
889 r = new PreprocessResource(this, r);
890
891 if (cr == null)
892 jar.putResource(path, r);
893 else
894 cr.addResource(r);
895 }
896 finally {
897 unsetProperty("@");
898 }
899 }
900
901 // Add last so the correct modification date is used
902 // to update the modified time.
903 if ( cr != null)
904 jar.putResource(destination, cr);
905 }
906
907 private String doResourceDirectory(Jar jar, Map<String, String> extra, boolean preprocess,
908 File sourceFile, String destinationPath) throws Exception {
909 String filter = extra.get("filter:");
910 boolean flatten = isTrue(extra.get("flatten:"));
911 boolean recursive = true;
912 String directive = extra.get("recursive:");
913 if (directive != null) {
914 recursive = isTrue(directive);
915 }
916
917 Instruction.Filter iFilter = null;
918 if (filter != null) {
919 iFilter = new Instruction.Filter(new Instruction(filter), recursive, getDoNotCopy());
920 }
921 else {
922 iFilter = new Instruction.Filter(null, recursive, getDoNotCopy());
923 }
924
925 Map<String, File> files = newMap();
926 resolveFiles(sourceFile, iFilter, recursive, destinationPath, files, flatten);
927
928 for (Map.Entry<String, File> entry : files.entrySet()) {
929 copy(jar, entry.getKey(), entry.getValue(), preprocess, extra);
930 }
931 return destinationPath;
932 }
933
934 private void resolveFiles(File dir, FileFilter filter, boolean recursive, String path,
935 Map<String, File> files, boolean flatten) {
936
937 if (doNotCopy(dir.getName())) {
938 return;
939 }
940
941 File[] fs = dir.listFiles(filter);
942 for (File file : fs) {
943 if (file.isDirectory()) {
944 if (recursive) {
945 String nextPath;
946 if (flatten)
947 nextPath = path;
948 else
949 nextPath = appendPath(path, file.getName());
950
951 resolveFiles(file, filter, recursive, nextPath, files, flatten);
952 }
953 // Directories are ignored otherwise
954 }
955 else {
956 String p = appendPath(path, file.getName());
957 if (files.containsKey(p))
958 warning("Include-Resource overwrites entry %s from file %s", p, file);
959 files.put(p, file);
960 }
961 }
962 }
963
964 private void noSuchFile(Jar jar, String clause, Map<String, String> extra, String source,
965 String destinationPath) throws Exception {
966 Jar src = getJarFromName(source, "Include-Resource " + source);
967 if (src != null) {
968 // Do not touch the manifest so this also
969 // works for signed files.
970 src.setDoNotTouchManifest();
971 JarResource jarResource = new JarResource(src);
972 jar.putResource(destinationPath, jarResource);
973 }
974 else {
975 Resource lastChance = make.process(source);
976 if (lastChance != null) {
977 String x = extra.get("extra");
978 if (x != null)
979 lastChance.setExtra(x);
980 jar.putResource(destinationPath, lastChance);
981 }
982 else
983 error("Input file does not exist: " + source);
984 }
985 }
986
987 /**
988 * Extra resources from a Jar and add them to the given jar. The clause is
989 * the
990 *
991 * @param jar
992 * @param clauses
993 * @param i
994 * @throws ZipException
995 * @throws IOException
996 */
997 private void extractFromJar(Jar jar, String source, String destination, boolean absentIsOk)
998 throws ZipException, IOException {
999 // Inline all resources and classes from another jar
1000 // optionally appended with a modified regular expression
1001 // like @zip.jar!/META-INF/MANIFEST.MF
1002 int n = source.lastIndexOf("!/");
1003 Instruction instr = null;
1004 if (n > 0) {
1005 instr = new Instruction(source.substring(n + 2));
1006 source = source.substring(0, n);
1007 }
1008
1009 // Pattern filter = null;
1010 // if (n > 0) {
1011 // String fstring = source.substring(n + 2);
1012 // source = source.substring(0, n);
1013 // filter = wildcard(fstring);
1014 // }
1015 Jar sub = getJarFromName(source, "extract from jar");
1016 if (sub == null) {
1017 if (absentIsOk)
1018 return;
1019
1020 error("Can not find JAR file " + source);
1021 }
1022 else {
1023 addAll(jar, sub, instr, destination);
1024 }
1025 }
1026
1027 /**
1028 * Add all the resources in the given jar that match the given filter.
1029 *
1030 * @param sub the jar
1031 * @param filter a pattern that should match the resoures in sub to be added
1032 */
1033 public boolean addAll(Jar to, Jar sub, Instruction filter) {
1034 return addAll(to, sub, filter, "");
1035 }
1036
1037 /**
1038 * Add all the resources in the given jar that match the given filter.
1039 *
1040 * @param sub the jar
1041 * @param filter a pattern that should match the resoures in sub to be added
1042 */
1043 public boolean addAll(Jar to, Jar sub, Instruction filter, String destination) {
1044 boolean dupl = false;
1045 for (String name : sub.getResources().keySet()) {
1046 if ("META-INF/MANIFEST.MF".equals(name))
1047 continue;
1048
1049 if (filter == null || filter.matches(name) != filter.isNegated())
1050 dupl |= to.putResource(Processor.appendPath(destination, name),
1051 sub.getResource(name), true);
1052 }
1053 return dupl;
1054 }
1055
1056 private void copy(Jar jar, String path, File from, boolean preprocess, Map<String, String> extra)
1057 throws Exception {
1058 if (doNotCopy(from.getName()))
1059 return;
1060
1061 if (from.isDirectory()) {
1062
1063 File files[] = from.listFiles();
1064 for (int i = 0; i < files.length; i++) {
1065 copy(jar, appendPath(path, files[i].getName()), files[i], preprocess, extra);
1066 }
1067 }
1068 else {
1069 if (from.exists()) {
1070 Resource resource = new FileResource(from);
1071 if (preprocess) {
1072 resource = new PreprocessResource(this, resource);
1073 }
1074 String x = extra.get("extra");
1075 if (x != null)
1076 resource.setExtra(x);
1077 if (path.endsWith("/"))
1078 path = path + from.getName();
1079 jar.putResource(path, resource);
1080
1081 if (isTrue(extra.get(LIB_DIRECTIVE))) {
1082 setProperty(BUNDLE_CLASSPATH, append(getProperty(BUNDLE_CLASSPATH), path));
1083 }
1084 }
1085 else {
1086 error("Input file does not exist: " + from);
1087 }
1088 }
1089 }
1090
1091 public void setSourcepath(File[] files) {
1092 for (int i = 0; i < files.length; i++)
1093 addSourcepath(files[i]);
1094 }
1095
1096 public void addSourcepath(File cp) {
1097 if (!cp.exists())
1098 warning("File on sourcepath that does not exist: " + cp);
1099
1100 sourcePath.add(cp);
1101 }
1102
1103 public void close() {
1104 super.close();
1105 }
1106
1107 /**
1108 * Build Multiple jars. If the -sub command is set, we filter the file with
1109 * the given patterns.
1110 *
1111 * @return
1112 * @throws Exception
1113 */
1114 public Jar[] builds() throws Exception {
1115 begin();
1116
1117 // Are we acting as a conduit for another JAR?
1118 String conduit = getProperty(CONDUIT);
1119 if (conduit != null) {
1120 Parameters map = parseHeader(conduit);
1121 Jar[] result = new Jar[map.size()];
1122 int n = 0;
1123 for (String file : map.keySet()) {
1124 Jar c = new Jar(getFile(file));
1125 addClose(c);
1126 String name = map.get(file).get("name");
1127 if (name != null)
1128 c.setName(name);
1129
1130 result[n++] = c;
1131 }
1132 return result;
1133 }
1134
1135 List<Jar> result = new ArrayList<Jar>();
1136 List<Builder> builders;
1137
1138 builders = getSubBuilders();
1139
1140 for (Builder builder : builders) {
1141 try {
1142 Jar jar = builder.build();
1143 jar.setName(builder.getBsn());
1144 result.add(jar);
1145 }
1146 catch (Exception e) {
1147 e.printStackTrace();
1148 error("Sub Building " + builder.getBsn(), e);
1149 }
1150 if (builder != this)
1151 getInfo(builder, builder.getBsn() + ": ");
1152 }
1153 return result.toArray(new Jar[result.size()]);
1154 }
1155
1156 /**
1157 * Answer a list of builders that represent this file or a list of files
1158 * specified in -sub. This list can be empty. These builders represents to
1159 * be created artifacts and are each scoped to such an artifacts. The
1160 * builders can be used to build the bundles or they can be used to find out
1161 * information about the to be generated bundles.
1162 *
1163 * @return List of 0..n builders representing artifacts.
1164 * @throws Exception
1165 */
1166 public List<Builder> getSubBuilders() throws Exception {
1167 String sub = getProperty(SUB);
1168 if (sub == null || sub.trim().length() == 0 || EMPTY_HEADER.equals(sub))
1169 return Arrays.asList(this);
1170
1171 List<Builder> builders = new ArrayList<Builder>();
1172 if (isTrue(getProperty(NOBUNDLES)))
1173 return builders;
1174
1175 Parameters subsMap = parseHeader(sub);
1176 for (Iterator<String> i = subsMap.keySet().iterator(); i.hasNext();) {
1177 File file = getFile(i.next());
1178 if (file.isFile()) {
1179 builders.add(getSubBuilder(file));
1180 i.remove();
1181 }
1182 }
1183
1184 Instructions instructions = new Instructions(subsMap);
1185
1186 List<File> members = new ArrayList<File>(Arrays.asList(getBase().listFiles()));
1187
1188 nextFile: while (members.size() > 0) {
1189
1190 File file = members.remove(0);
1191
1192 // Check if the file is one of our parents
1193 Processor p = this;
1194 while (p != null) {
1195 if (file.equals(p.getPropertiesFile()))
1196 continue nextFile;
1197 p = p.getParent();
1198 }
1199
1200 for (Iterator<Instruction> i = instructions.keySet().iterator(); i.hasNext();) {
1201
1202 Instruction instruction = i.next();
1203 if (instruction.matches(file.getName())) {
1204
1205 if (!instruction.isNegated()) {
1206 builders.add(getSubBuilder(file));
1207 }
1208
1209 // Because we matched (even though we could be negated)
1210 // we skip any remaining searches
1211 continue nextFile;
1212 }
1213 }
1214 }
1215 return builders;
1216 }
1217
1218 public Builder getSubBuilder(File file) throws Exception {
1219 Builder builder = getSubBuilder();
1220 if (builder != null) {
1221 builder.setProperties(file);
1222 addClose(builder);
1223 }
1224 return builder;
1225 }
1226
1227 public Builder getSubBuilder() throws Exception {
1228 Builder builder = new Builder(this);
1229 builder.setBase(getBase());
1230
1231 for (Jar file : getClasspath()) {
1232 builder.addClasspath(file);
1233 }
1234
1235 return builder;
1236 }
1237
1238 /**
1239 * A macro to convert a maven version to an OSGi version
1240 */
1241
1242 public String _maven_version(String args[]) {
1243 if (args.length > 2)
1244 error("${maven_version} macro receives too many arguments " + Arrays.toString(args));
1245 else
1246 if (args.length < 2)
1247 error("${maven_version} macro has no arguments, use ${maven_version;1.2.3-SNAPSHOT}");
1248 else {
1249 return cleanupVersion(args[1]);
1250 }
1251 return null;
1252 }
1253
1254 public String _permissions(String args[]) throws IOException {
1255 StringBuilder sb = new StringBuilder();
1256
1257 for (String arg : args) {
1258 if ("packages".equals(arg) || "all".equals(arg)) {
1259 for (PackageRef imp : getImports().keySet()) {
1260 if (!imp.isJava()) {
1261 sb.append("(org.osgi.framework.PackagePermission \"");
1262 sb.append(imp);
1263 sb.append("\" \"import\")\r\n");
1264 }
1265 }
1266 for (PackageRef exp : getExports().keySet()) {
1267 sb.append("(org.osgi.framework.PackagePermission \"");
1268 sb.append(exp);
1269 sb.append("\" \"export\")\r\n");
1270 }
1271 }
1272 else
1273 if ("admin".equals(arg) || "all".equals(arg)) {
1274 sb.append("(org.osgi.framework.AdminPermission)");
1275 }
1276 else
1277 if ("permissions".equals(arg))
1278 ;
1279 else
1280 error("Invalid option in ${permissions}: %s", arg);
1281 }
1282 return sb.toString();
1283 }
1284
1285 /**
1286 *
1287 */
1288 public void removeBundleSpecificHeaders() {
1289 Set<String> set = new HashSet<String>(Arrays.asList(BUNDLE_SPECIFIC_HEADERS));
1290 setForceLocal(set);
1291 }
1292
1293 /**
1294 * Check if the given resource is in scope of this bundle. That is, it
1295 * checks if the Include-Resource includes this resource or if it is a class
1296 * file it is on the class path and the Export-Pacakge or Private-Package
1297 * include this resource.
1298 *
1299 * @param f
1300 * @return
1301 */
1302 public boolean isInScope(Collection<File> resources) throws Exception {
1303 Parameters clauses = parseHeader(getProperty(Constants.EXPORT_PACKAGE));
1304 clauses.putAll(parseHeader(getProperty(Constants.PRIVATE_PACKAGE)));
1305 if (isTrue(getProperty(Constants.UNDERTEST))) {
1306 clauses.putAll(parseHeader(getProperty(Constants.TESTPACKAGES,
1307 "test;presence:=optional")));
1308 }
1309
1310 Collection<String> ir = getIncludedResourcePrefixes();
1311
1312 Instructions instructions = new Instructions(clauses);
1313
1314 for (File r : resources) {
1315 String cpEntry = getClasspathEntrySuffix(r);
1316 if (cpEntry != null) {
1317 String pack = Descriptors.getPackage(cpEntry);
1318 Instruction i = matches(instructions, pack, null, r.getName());
1319 if (i != null)
1320 return !i.isNegated();
1321 }
1322
1323 // Check if this resource starts with one of the I-C header
1324 // paths.
1325 String path = r.getAbsolutePath();
1326 for (String p : ir) {
1327 if (path.startsWith(p))
1328 return true;
1329 }
1330 }
1331 return false;
1332 }
1333
1334 /**
1335 * Extra the paths for the directories and files that are used in the
1336 * Include-Resource header.
1337 *
1338 * @return
1339 */
1340 private Collection<String> getIncludedResourcePrefixes() {
1341 List<String> prefixes = new ArrayList<String>();
1342 Parameters includeResource = getIncludeResource();
1343 for (Entry<String, Attrs> p : includeResource.entrySet()) {
1344 if (p.getValue().containsKey("literal"))
1345 continue;
1346
1347 Matcher m = IR_PATTERN.matcher(p.getKey());
1348 if (m.matches()) {
1349 File f = getFile(m.group(1));
1350 prefixes.add(f.getAbsolutePath());
1351 }
1352 }
1353 return prefixes;
1354 }
1355
1356 /**
1357 * Answer the string of the resource that it has in the container.
1358 *
1359 * @param resource The resource to look for
1360 * @return
1361 * @throws Exception
1362 */
1363 public String getClasspathEntrySuffix(File resource) throws Exception {
1364 for (Jar jar : getClasspath()) {
1365 File source = jar.getSource();
1366 if (source != null) {
1367 source = source.getCanonicalFile();
1368 String sourcePath = source.getAbsolutePath();
1369 String resourcePath = resource.getAbsolutePath();
1370
1371 if (resourcePath.startsWith(sourcePath)) {
1372 // Make sure that the path name is translated correctly
1373 // i.e. on Windows the \ must be translated to /
1374 String filePath = resourcePath.substring(sourcePath.length() + 1);
1375
1376 return filePath.replace(File.separatorChar, '/');
1377 }
1378 }
1379 }
1380 return null;
1381 }
1382
1383 /**
1384 * doNotCopy
1385 *
1386 * The doNotCopy variable maintains a patter for files that should not be
1387 * copied. There is a default {@link #DEFAULT_DO_NOT_COPY} but this ca be
1388 * overridden with the {@link Constants#DONOTCOPY} property.
1389 */
1390
1391 public boolean doNotCopy(String v) {
1392 return getDoNotCopy().matcher(v).matches();
1393 }
1394
1395 public Pattern getDoNotCopy() {
1396 if (xdoNotCopy == null) {
1397 String string = null;
1398 try {
1399 string = getProperty(DONOTCOPY, DEFAULT_DO_NOT_COPY);
1400 xdoNotCopy = Pattern.compile(string);
1401 }
1402 catch (Exception e) {
1403 error("Invalid value for %s, value is %s", DONOTCOPY, string);
1404 xdoNotCopy = Pattern.compile(DEFAULT_DO_NOT_COPY);
1405 }
1406 }
1407 return xdoNotCopy;
1408 }
1409
1410 /**
1411 */
1412
1413 static MakeBnd makeBnd = new MakeBnd();
1414 static MakeCopy makeCopy = new MakeCopy();
1415 static ServiceComponent serviceComponent = new ServiceComponent();
1416 static DSAnnotations dsAnnotations = new DSAnnotations();
1417 static MetatypePlugin metatypePlugin = new MetatypePlugin();
1418
1419 @Override
1420 protected void setTypeSpecificPlugins(Set<Object> list) {
1421 list.add(makeBnd);
1422 list.add(makeCopy);
1423 list.add(serviceComponent);
1424 list.add(dsAnnotations);
1425 list.add(metatypePlugin);
1426 super.setTypeSpecificPlugins(list);
1427 }
1428
1429 /**
1430 * Diff this bundle to another bundle for the given packages.
1431 *
1432 * @throws Exception
1433 */
1434
1435 public void doDiff(Jar dot) throws Exception {
1436 Parameters diffs = parseHeader(getProperty("-diff"));
1437 if (diffs.isEmpty())
1438 return;
1439
1440 trace("diff %s", diffs);
1441
1442 if (tree == null)
1443 tree = differ.tree(this);
1444
1445 for (Entry<String, Attrs> entry : diffs.entrySet()) {
1446 String path = entry.getKey();
1447 File file = getFile(path);
1448 if (!file.isFile()) {
1449 error("Diffing against %s that is not a file", file);
1450 continue;
1451 }
1452
1453 boolean full = entry.getValue().get("--full") != null;
1454 boolean warning = entry.getValue().get("--warning") != null;
1455
1456 Tree other = differ.tree(file);
1457 Diff api = tree.diff(other).get("<api>");
1458 Instructions instructions = new Instructions(entry.getValue().get("--pack"));
1459
1460 trace("diff against %s --full=%s --pack=%s --warning=%s", file, full, instructions);
1461 for (Diff p : api.getChildren()) {
1462 String pname = p.getName();
1463 if (p.getType() == Type.PACKAGE && instructions.matches(pname)) {
1464 if (p.getDelta() != Delta.UNCHANGED) {
1465
1466 if (!full)
1467 if (warning)
1468 warning("Differ %s", p);
1469 else
1470 error("Differ %s", p);
1471 else {
1472 if (warning)
1473 warning("Diff found a difference in %s for packages %s", file,
1474 instructions);
1475 else
1476 error("Diff found a difference in %s for packages %s", file,
1477 instructions);
1478 show(p, "", warning);
1479 }
1480 }
1481 }
1482 }
1483 }
1484 }
1485
1486 /**
1487 * Show the diff recursively
1488 *
1489 * @param p
1490 * @param i
1491 */
1492 private void show(Diff p, String indent, boolean warning) {
1493 Delta d = p.getDelta();
1494 if (d == Delta.UNCHANGED)
1495 return;
1496
1497 if (warning)
1498 warning("%s%s", indent, p);
1499 else
1500 error("%s%s", indent, p);
1501
1502 indent = indent + " ";
1503 switch (d) {
1504 case CHANGED :
1505 case MAJOR :
1506 case MINOR :
1507 case MICRO :
1508 break;
1509
1510 default :
1511 return;
1512 }
1513 for (Diff c : p.getChildren())
1514 show(c, indent, warning);
1515 }
1516
1517 /**
1518 * Base line against a previous version
1519 *
1520 * @throws Exception
1521 */
1522
1523 private void doBaseline(Jar dot) throws Exception {
1524 Parameters diffs = parseHeader(getProperty("-baseline"));
1525 if (diffs.isEmpty())
1526 return;
1527
1528 System.err.printf("baseline %s%n", diffs);
1529
1530 Baseline baseline = new Baseline(this, differ);
1531
1532 for (Entry<String, Attrs> entry : diffs.entrySet()) {
1533 String path = entry.getKey();
1534 File file = getFile(path);
1535 if (!file.isFile()) {
1536 error("Diffing against %s that is not a file", file);
1537 continue;
1538 }
1539 Jar other = new Jar(file);
1540 Set<Info> infos = baseline.baseline(dot, other, null);
1541 for (Info info : infos) {
1542 if (info.mismatch) {
1543 error("%s %-50s %-10s %-10s %-10s %-10s %-10s\n", info.mismatch ? '*' : ' ',
1544 info.packageName, info.packageDiff.getDelta(), info.newerVersion,
1545 info.olderVersion, info.suggestedVersion,
1546 info.suggestedIfProviders == null ? "-" : info.suggestedIfProviders);
1547 }
1548 }
1549 }
1550 }
1551
1552 public void addSourcepath(Collection<File> sourcepath) {
1553 for (File f : sourcepath) {
1554 addSourcepath(f);
1555 }
1556 }
1557
1558}