blob: 22748aed762b066209f531f7062883c44af40b55 [file] [log] [blame]
Stuart McCullochf29ef3f2008-12-04 07:58:07 +00001/* Copyright 2006 aQute SARL
2 * Licensed under the Apache License, Version 2.0, see http://www.apache.org/licenses/LICENSE-2.0 */
3package aQute.lib.osgi;
4
5import java.io.*;
6import java.lang.reflect.*;
7import java.net.*;
8import java.text.*;
9import java.util.*;
10import java.util.regex.*;
11
Stuart McCullocha75a5152009-01-29 07:46:54 +000012import aQute.libg.sed.*;
Stuart McCullochf29ef3f2008-12-04 07:58:07 +000013import aQute.libg.version.*;
14
15/**
16 * Provide a macro processor. This processor can replace variables in strings
17 * based on a properties and a domain. The domain can implement functions that
18 * start with a "_" and take args[], the names of these functions are available
19 * as functions in the macro processor (without the _). Macros can nest to any
20 * depth but may not contain loops.
21 *
22 */
Stuart McCullocha75a5152009-01-29 07:46:54 +000023public class Macro implements Replacer {
Stuart McCullochf29ef3f2008-12-04 07:58:07 +000024 Properties properties;
25 Processor domain;
26 Object targets[];
27 boolean flattening;
28
29 public Macro(Properties properties, Processor domain, Object... targets) {
30 this.properties = properties;
31 this.domain = domain;
32 this.targets = targets;
Stuart McCullocha75a5152009-01-29 07:46:54 +000033 if (targets != null) {
34 for (Object o : targets) {
35 assert o != null;
36 }
37 }
Stuart McCullochf29ef3f2008-12-04 07:58:07 +000038 }
39
40 public Macro(Processor processor) {
41 this(new Properties(), processor);
42 }
43
44 public String process(String line) {
45 return process(line, null);
46 }
47
48 String process(String line, Link link) {
49 StringBuffer sb = new StringBuffer();
50 process(line, 0, '\u0000', '\u0000', sb, link);
51 return sb.toString();
52 }
53
54 int process(CharSequence org, int index, char begin, char end,
55 StringBuffer result, Link link) {
56 StringBuilder line = new StringBuilder(org);
57 int nesting = 1;
58
59 StringBuffer variable = new StringBuffer();
60 outer: while (index < line.length()) {
61 char c1 = line.charAt(index++);
62 if (c1 == end) {
63 if (--nesting == 0) {
64 result.append(replace(variable.toString(), link));
65 return index;
66 }
67 } else if (c1 == begin)
68 nesting++;
69 else if (c1 == '\\' && index < line.length() - 1
70 && line.charAt(index) == '$') {
71 // remove the escape backslash and interpret the dollar as a
72 // literal
73 index++;
74 variable.append('$');
75 continue outer;
76 } else if (c1 == '$' && index < line.length() - 2) {
77 char c2 = line.charAt(index);
78 char terminator = getTerminator(c2);
79 if (terminator != 0) {
80 index = process(line, index + 1, c2, terminator, variable,
81 link);
82 continue outer;
83 }
84 }
85 variable.append(c1);
86 }
87 result.append(variable);
88 return index;
89 }
90
91 char getTerminator(char c) {
92 switch (c) {
93 case '(':
94 return ')';
95 case '[':
96 return ']';
97 case '{':
98 return '}';
99 case '<':
100 return '>';
101 case '\u00ab': // Guillemet double << >>
102 return '\u00bb';
103 case '\u2039': // Guillemet single
104 return '\u203a';
105 }
106 return 0;
107 }
108
109 protected String replace(String key, Link link) {
110 if (link != null && link.contains(key))
111 return "${infinite:" + link.toString() + "}";
112
113 if (key != null) {
114 key = key.trim();
115 if (key.length() > 0) {
116 String value = (String) properties.getProperty(key);
117 if (value != null)
118 return process(value, new Link(link, key));
119
120 value = doCommands(key);
121 if (value != null)
122 return process(value, new Link(link, key));
123
124 if (key != null && key.trim().length() > 0) {
125 value = System.getProperty(key);
126 if (value != null)
127 return value;
128 }
129 if (!flattening)
130 domain.warning("No translation found for macro: " + key);
131 } else {
132 domain.warning("Found empty macro key");
133 }
134 } else {
135 domain.warning("Found null macro key");
136 }
137 return "${" + key + "}";
138 }
139
140 /**
141 * Parse the key as a command. A command consist of parameters separated by
142 * ':'.
143 *
144 * @param key
145 * @return
146 */
147 static Pattern commands = Pattern.compile("(?<!\\\\);");
148
149 private String doCommands(String key) {
150 String[] args = commands.split(key);
151 if (args == null || args.length == 0)
152 return null;
153
154 for (int i = 0; i < args.length; i++)
155 if (args[i].indexOf('\\') >= 0)
156 args[i] = args[i].replaceAll("\\\\;", ";");
157
158 Processor rover = domain;
159 while (rover != null) {
160 String result = doCommand(rover, args[0], args);
161 if (result != null)
162 return result;
163
164 rover = rover.getParent();
165 }
166
167 for (int i = 0; targets != null && i < targets.length; i++) {
168 String result = doCommand(targets[i], args[0], args);
169 if (result != null)
170 return result;
171 }
172
173 return doCommand(this, args[0], args);
174 }
175
176 private String doCommand(Object target, String method, String[] args) {
177 if (target == null)
Stuart McCullocha75a5152009-01-29 07:46:54 +0000178 ; // System.out.println("Huh? Target should never be null " +
179 // domain);
Stuart McCullochf29ef3f2008-12-04 07:58:07 +0000180 else {
181 String cname = "_" + method.replaceAll("-", "_");
182 try {
183 Method m = target.getClass().getMethod(cname,
184 new Class[] { String[].class });
185 return (String) m.invoke(target, new Object[] { args });
186 } catch (NoSuchMethodException e) {
187 // Ignore
188 } catch (InvocationTargetException e) {
189 domain.warning("Exception in replace: " + e.getCause());
190 e.printStackTrace();
191 } catch (Exception e) {
192 domain.warning("Exception in replace: " + e + " method="
193 + method);
194 e.printStackTrace();
195 }
196 }
197 return null;
198 }
199
200 /**
201 * Return a unique list where the duplicates are removed.
202 *
203 * @param args
204 * @return
205 */
206 static String _uniqHelp = "${uniq;<list> ...}";
207
208 public String _uniq(String args[]) {
209 verifyCommand(args, _uniqHelp, null, 1, Integer.MAX_VALUE);
210 Set<String> set = new LinkedHashSet<String>();
211 for (int i = 1; i < args.length; i++) {
212 Processor.split(args[i], set);
213 }
214 return Processor.join(set, ",");
215 }
216
217 public String _filter(String args[]) {
218 return filter(args, false);
219 }
220
221 public String _filterout(String args[]) {
222 return filter(args, true);
223
224 }
225
226 static String _filterHelp = "${%s;<list>;<regex>}";
227
228 String filter(String[] args, boolean include) {
229 verifyCommand(args, String.format(_filterHelp, args[0]), null, 3, 3);
230
231 Collection<String> list = new ArrayList<String>(Processor
232 .split(args[1]));
233 Pattern pattern = Pattern.compile(args[2]);
234
235 for (Iterator<String> i = list.iterator(); i.hasNext();) {
236 if (pattern.matcher(i.next()).matches() == include)
237 i.remove();
238 }
239 return Processor.join(list);
240 }
241
242 static String _sortHelp = "${sort;<list>...}";
243
244 public String _sort(String args[]) {
245 verifyCommand(args, _sortHelp, null, 2, Integer.MAX_VALUE);
246
247 List<String> result = new ArrayList<String>();
248 for (int i = 1; i < args.length; i++) {
249 Processor.split(args[i], result);
250 }
251 Collections.sort(result);
252 return Processor.join(result);
253 }
254
255 static String _joinHelp = "${join;<list>...}";
256
257 public String _join(String args[]) {
258
259 verifyCommand(args, _joinHelp, null, 1, Integer.MAX_VALUE);
260
261 List<String> result = new ArrayList<String>();
262 for (int i = 1; i < args.length; i++) {
263 Processor.split(args[i], result);
264 }
265 return Processor.join(result);
266 }
267
268 static String _ifHelp = "${if;<condition>;<iftrue> [;<iffalse>] }";
269
270 public String _if(String args[]) {
271 verifyCommand(args, _ifHelp, null, 3, 4);
272 String condition = args[1].trim();
273 if (condition.length() != 0)
274 return args[2];
275 if (args.length > 3)
276 return args[3];
277 else
278 return "";
279 }
280
281 public String _now(String args[]) {
282 return new Date().toString();
283 }
284
285 public static String _fmodifiedHelp = "${fmodified;<list of filenames>...}, return latest modification date";
286
287 public String _fmodified(String args[]) throws Exception {
288 verifyCommand(args, _fmodifiedHelp, null, 2, Integer.MAX_VALUE);
289
290 long time = 0;
291 Collection<String> names = new ArrayList<String>();
292 for (int i = 1; i < args.length; i++) {
293 Processor.split(args[i], names);
294 }
295 for (String name : names) {
296 File f = new File(name);
297 if (f.exists() && f.lastModified() > time)
298 time = f.lastModified();
299 }
300 return "" + time;
301 }
302
303 public String _long2date(String args[]) {
304 try {
305 return new Date(Long.parseLong(args[1])).toString();
306 } catch (Exception e) {
307 e.printStackTrace();
308 }
309 return "not a valid long";
310 }
311
312 public String _literal(String args[]) {
313 if (args.length != 2)
314 throw new RuntimeException(
315 "Need a value for the ${literal;<value>} macro");
316 return "${" + args[1] + "}";
317 }
318
319 public String _def(String args[]) {
320 if (args.length != 2)
321 throw new RuntimeException(
322 "Need a value for the ${def;<value>} macro");
323
324 String value = properties.getProperty(args[1]);
325 if (value == null)
326 return "";
327 else
328 return value;
329 }
330
331 /**
332 *
333 * replace ; <list> ; regex ; replace
334 *
335 * @param args
336 * @return
337 */
338 public String _replace(String args[]) {
339 if (args.length != 4) {
340 domain.warning("Invalid nr of arguments to replace "
341 + Arrays.asList(args));
342 return null;
343 }
344
345 String list[] = args[1].split("\\s*,\\s*");
346 StringBuffer sb = new StringBuffer();
347 String del = "";
348 for (int i = 0; i < list.length; i++) {
349 String element = list[i].trim();
350 if (!element.equals("")) {
351 sb.append(del);
352 sb.append(element.replaceAll(args[2], args[3]));
353 del = ", ";
354 }
355 }
356
357 return sb.toString();
358 }
359
360 public String _warning(String args[]) {
361 for (int i = 1; i < args.length; i++) {
362 domain.warning(process(args[i]));
363 }
364 return "";
365 }
366
367 public String _error(String args[]) {
368 for (int i = 1; i < args.length; i++) {
369 domain.error(process(args[i]));
370 }
371 return "";
372 }
373
374 /**
375 * toclassname ; <path>.class ( , <path>.class ) *
376 *
377 * @param args
378 * @return
379 */
380 static String _toclassnameHelp = "${classname;<list of class names>}, convert class paths to FQN class names ";
381
382 public String _toclassname(String args[]) {
383 verifyCommand(args, _toclassnameHelp, null, 2, 2);
384 Collection<String> paths = Processor.split(args[1]);
385
386 List<String> names = new ArrayList<String>(paths.size());
387 for (String path : paths) {
388 if (path.endsWith(".class")) {
389 String name = path.substring(0, path.length() - 6).replace('/',
390 '.');
391 names.add(name);
Stuart McCullocha75a5152009-01-29 07:46:54 +0000392 } else if (path.endsWith(".java")) {
393 String name = path.substring(0, path.length() - 5).replace('/',
394 '.');
395 names.add(name);
Stuart McCullochf29ef3f2008-12-04 07:58:07 +0000396 } else {
397 domain
398 .warning("in toclassname, "
399 + args[1]
400 + " is not a class path because it does not end in .class");
401 }
402 }
403 return Processor.join(names, ",");
404 }
405
406 /**
407 * toclassname ; <path>.class ( , <path>.class ) *
408 *
409 * @param args
410 * @return
411 */
412
413 static String _toclasspathHelp = "${toclasspath;<list>}, convert a list of class names to paths";
414
415 public String _toclasspath(String args[]) {
416 verifyCommand(args, _toclasspathHelp, null, 2, 2);
417
418 Collection<String> names = Processor.split(args[1]);
419 Collection<String> paths = new ArrayList<String>(names.size());
420 for (String name : names) {
421 String path = name.replace('.', '/') + ".class";
422 paths.add(path);
423 }
424 return Processor.join(paths, ",");
425 }
426
427 public String _dir(String args[]) {
428 if (args.length < 2) {
429 domain.warning("Need at least one file name for ${dir;...}");
430 return null;
431 } else {
432 String del = "";
433 StringBuffer sb = new StringBuffer();
434 for (int i = 1; i < args.length; i++) {
435 File f = new File(args[i]).getAbsoluteFile();
436 if (f.exists() && f.getParentFile().exists()) {
437 sb.append(del);
438 sb.append(f.getParentFile().getAbsolutePath());
439 del = ",";
440 }
441 }
442 return sb.toString();
443 }
444
445 }
446
447 public String _basename(String args[]) {
448 if (args.length < 2) {
449 domain.warning("Need at least one file name for ${basename;...}");
450 return null;
451 } else {
452 String del = "";
453 StringBuffer sb = new StringBuffer();
454 for (int i = 1; i < args.length; i++) {
455 File f = new File(args[i]).getAbsoluteFile();
456 if (f.exists() && f.getParentFile().exists()) {
457 sb.append(del);
458 sb.append(f.getName());
459 del = ",";
460 }
461 }
462 return sb.toString();
463 }
464
465 }
466
467 public String _isfile(String args[]) {
468 if (args.length < 2) {
469 domain.warning("Need at least one file name for ${isfile;...}");
470 return null;
471 } else {
472 boolean isfile = true;
473 for (int i = 1; i < args.length; i++) {
474 File f = new File(args[i]).getAbsoluteFile();
475 isfile &= f.isFile();
476 }
477 return isfile ? "true" : "false";
478 }
479
480 }
481
482 public String _isdir(String args[]) {
483 if (args.length < 2) {
484 domain.warning("Need at least one file name for ${isdir;...}");
485 return null;
486 } else {
487 boolean isdir = true;
488 for (int i = 1; i < args.length; i++) {
489 File f = new File(args[i]).getAbsoluteFile();
490 isdir &= f.isDirectory();
491 }
492 return isdir ? "true" : "false";
493 }
494
495 }
496
497 public String _tstamp(String args[]) {
498 String format = "yyyyMMddhhmm";
499 long now = System.currentTimeMillis();
500
501 if (args.length > 1) {
502 format = args[1];
503 if (args.length > 2) {
504 now = Long.parseLong(args[2]);
505 if (args.length > 3) {
506 domain.warning("Too many arguments for tstamp: "
507 + Arrays.toString(args));
508 }
509 }
510 }
511 SimpleDateFormat sdf = new SimpleDateFormat(format);
512 return sdf.format(new Date(now));
513 }
514
515 /**
516 * Wildcard a directory. The lists can contain Instruction that are matched
517 * against the given directory
518 *
519 * ${wc;<dir>;<list>(;<list>)*}
520 *
521 * @author aqute
522 *
523 */
524
525 public String _lsr(String args[]) {
526 return ls(args, true);
527 }
528
529 public String _lsa(String args[]) {
530 return ls(args, false);
531 }
532
533 String ls(String args[], boolean relative) {
534 if (args.length < 2)
535 throw new IllegalArgumentException(
536 "the ${ls} macro must at least have a directory as parameter");
537
538 File dir = new File(args[1]);
539 if (!dir.isAbsolute())
540 throw new IllegalArgumentException(
541 "the ${ls} macro directory parameter is not absolute: "
542 + dir);
543
544 if (!dir.exists())
545 throw new IllegalArgumentException(
546 "the ${ls} macro directory parameter does not exist: "
547 + dir);
548
549 if (!dir.isDirectory())
550 throw new IllegalArgumentException(
551 "the ${ls} macro directory parameter points to a file instead of a directory: "
552 + dir);
553
554 String[] files = dir.list();
555 List<String> result;
556
557 if (args.length < 3) {
558 result = Arrays.asList(files);
559 } else
560 result = new ArrayList<String>();
561
562 for (int i = 2; i < args.length; i++) {
563 String parts[] = args[i].split("\\s*,\\s*");
564 for (String pattern : parts) {
565 // So make it in to an instruction
566 Instruction instr = Instruction.getPattern(pattern);
567
568 // For each project, match it against the instruction
569 for (int f = 0; f < files.length; f++) {
570 if (files[f] != null) {
571 if (instr.matches(files[f])) {
572 if (!instr.isNegated()) {
573 if (relative)
574 result.add(files[f]);
575 else
576 result.add(new File(dir, files[f])
577 .getAbsolutePath());
578 }
579 files[f] = null;
580 }
581 }
582 }
583 }
584 }
585 return Processor.join(result, ",");
586 }
587
588 public String _currenttime(String args[]) {
589 return Long.toString(System.currentTimeMillis());
590 }
591
592 /**
593 * Modify a version to set a version policy. Thed policy is a mask that is
594 * mapped to a version.
595 *
596 * <pre>
597 * + increment
598 * - decrement
599 * = maintain
600 * &tilde; discard
601 *
602 * ==+ = maintain major, minor, increment micro, discard qualifier
603 * &tilde;&tilde;&tilde;= = just get the qualifier
604 * version=&quot;[${version;==;${@}},${version;=+;${@}})&quot;
605 * </pre>
606 *
607 *
608 *
609 *
610 * @param args
611 * @return
612 */
613 static String _versionHelp = "${version;<mask>;<version>}, modify a version\n"
614 + "<mask> ::= [ M [ M [ M [ MQ ]]]\n"
615 + "M ::= '+' | '-' | MQ\n"
616 + "MQ ::= '~' | '='";
617 static Pattern _versionPattern[] = new Pattern[] { null, null,
618 Pattern.compile("[-+=~]{0,3}[=~]?"), Verifier.VERSION };
619
620 public String _version(String args[]) {
621 verifyCommand(args, _versionHelp, null, 3, 3);
622
623 String mask = args[1];
624
625 Version version = new Version(args[2]);
626 StringBuilder sb = new StringBuilder();
627 String del = "";
628
629 for (int i = 0; i < mask.length(); i++) {
630 char c = mask.charAt(i);
631 String result = null;
632 if (c != '~') {
633 if (i == 3) {
634 result = version.getQualifier();
635 } else {
636 int x = version.get(i);
637 switch (c) {
638 case '+':
639 x++;
640 break;
641 case '-':
642 x--;
643 break;
644 case '=':
645 break;
646 }
647 result = Integer.toString(x);
648 }
649 if (result != null) {
650 sb.append(del);
651 del = ".";
652 sb.append(result);
653 }
654 }
655 }
656 return sb.toString();
657 }
658
659 /**
660 * System command. Execute a command and insert the result.
661 *
662 * @param args
663 * @param help
664 * @param patterns
665 * @param low
666 * @param high
667 */
668 public String _system(String args[]) throws Exception {
669 verifyCommand(args,
670 "${system;<command>[;<in>]}, execute a system command", null,
671 2, 3);
672 String command = args[1];
673 String input = null;
674
675 if (args.length > 2) {
676 input = args[2];
677 }
678
679 Process process = Runtime.getRuntime().exec(command, null,
680 domain.getBase());
681 if (input != null) {
682 process.getOutputStream().write(input.getBytes("UTF-8"));
683 }
684 process.getOutputStream().close();
685
686 String s = getString(process.getInputStream());
687 process.getInputStream().close();
688 int exitValue = process.waitFor();
689 if (exitValue != 0) {
690 domain.error("System command " + command + " failed with "
691 + exitValue);
692 }
693 return s.trim();
694 }
695
696 /**
697 * Get the contents of a file.
698 *
699 * @param in
700 * @return
701 * @throws IOException
702 */
703
704 public String _cat(String args[]) throws IOException {
705 verifyCommand(args, "${cat;<in>}, get the content of a file", null, 2,
706 2);
707 File f = domain.getFile(args[1]);
708 if (f.isFile()) {
709 InputStream in = new FileInputStream(f);
710 return getString(in);
711 } else if (f.isDirectory()) {
Stuart McCullocha75a5152009-01-29 07:46:54 +0000712 return Arrays.toString(f.list());
Stuart McCullochf29ef3f2008-12-04 07:58:07 +0000713 } else {
714 try {
715 URL url = new URL(args[1]);
716 InputStream in = url.openStream();
717 return getString(in);
Stuart McCullocha75a5152009-01-29 07:46:54 +0000718 } catch (MalformedURLException mfue) {
Stuart McCullochf29ef3f2008-12-04 07:58:07 +0000719 // Ignore here
720 }
721 return null;
722 }
723 }
724
725 public static String getString(InputStream in) throws IOException {
726 try {
727 StringBuilder sb = new StringBuilder();
728 BufferedReader rdr = new BufferedReader(new InputStreamReader(in));
729 String line = null;
730 while ((line = rdr.readLine()) != null) {
731 sb.append(line);
732 sb.append("\n");
733 }
734 return sb.toString();
735 } finally {
736 in.close();
737 }
738 }
739
740 public static void verifyCommand(String args[], String help,
741 Pattern[] patterns, int low, int high) {
742 String message = "";
743 if (args.length > high) {
744 message = "too many arguments";
745 } else if (args.length < low) {
746 message = "too few arguments";
747 } else {
748 for (int i = 0; patterns != null && i < patterns.length
749 && i < args.length - 1; i++) {
750 if (patterns[i] != null
751 && !patterns[i].matcher(args[i + 1]).matches()) {
752 message += String.format(
753 "Argument %s (%s) does not match %s\n", i, args[i],
754 patterns[i].pattern());
755 }
756 }
757 }
758 if (message.length() != 0) {
759 StringBuilder sb = new StringBuilder();
760 String del = "${";
761 for (String arg : args) {
762 sb.append(del);
763 sb.append(arg);
764 del = ";";
765 }
766 sb.append("}, is not understood. ");
767 sb.append(message);
768 throw new IllegalArgumentException(sb.toString());
769 }
770 }
771
772 // Helper class to track expansion of variables
773 // on the stack.
774 static class Link {
775 Link previous;
776 String key;
777
778 public Link(Link previous, String key) {
779 this.previous = previous;
780 this.key = key;
781 }
782
783 public boolean contains(String key) {
784 if (this.key.equals(key))
785 return true;
786
787 if (previous == null)
788 return false;
789
790 return previous.contains(key);
791 }
792
793 public String toString() {
794 StringBuffer sb = new StringBuffer();
795 String del = "[";
796 for (Link r = this; r != null; r = r.previous) {
797 sb.append(del);
798 sb.append(r.key);
799 del = ",";
800 }
801 sb.append("]");
802 return sb.toString();
803 }
804 }
805
806 /**
807 * Take all the properties and translate them to actual values. This method
808 * takes the set properties and traverse them over all entries, including
809 * the default properties for that properties. The values no longer contain
810 * macros.
811 *
812 * @return A new Properties with the flattened values
813 */
814 public Properties getFlattenedProperties() {
815 // Some macros only work in a lower processor, so we
816 // do not report unknown macros while flattening
817 flattening = true;
818 try {
819 Properties flattened = new Properties();
820 for (Enumeration<?> e = properties.propertyNames(); e
821 .hasMoreElements();) {
822 String key = (String) e.nextElement();
Stuart McCullocha75a5152009-01-29 07:46:54 +0000823 if (!key.startsWith("_"))
Stuart McCullochf29ef3f2008-12-04 07:58:07 +0000824 flattened.put(key, process(properties.getProperty(key)));
825 }
826 return flattened;
827 } finally {
828 flattening = false;
829 }
830 };
831
832}