blob: 12ab273da73506ea4bcb449786be05cbb68fea49 [file] [log] [blame]
Stuart McCullochd00f9712009-07-13 10:06:47 +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
12import aQute.libg.sed.*;
13import 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 */
23public class Macro implements Replacer {
24 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;
33 if (targets != null) {
34 for (Object o : targets) {
35 assert o != null;
36 }
37 }
38 }
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 public static 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)
178 ; // System.out.println("Huh? Target should never be null " +
179 // domain);
180 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);
392 } else if (path.endsWith(".java")) {
393 String name = path.substring(0, path.length() - 5).replace('/',
394 '.');
395 names.add(name);
396 } 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>[;boolean]}, convert a list of class names to paths";
414
415 public String _toclasspath(String args[]) {
416 verifyCommand(args, _toclasspathHelp, null, 2, 3);
417 boolean cl= true;
418 if (args.length>2)
419 cl = new Boolean(args[2]);
420
421 Collection<String> names = Processor.split(args[1]);
422 Collection<String> paths = new ArrayList<String>(names.size());
423 for (String name : names) {
424 String path = name.replace('.', '/') + (cl ? ".class" : "");
425 paths.add(path);
426 }
427 return Processor.join(paths, ",");
428 }
429
430 public String _dir(String args[]) {
431 if (args.length < 2) {
432 domain.warning("Need at least one file name for ${dir;...}");
433 return null;
434 } else {
435 String del = "";
436 StringBuffer sb = new StringBuffer();
437 for (int i = 1; i < args.length; i++) {
438 File f = new File(args[i]).getAbsoluteFile();
439 if (f.exists() && f.getParentFile().exists()) {
440 sb.append(del);
441 sb.append(f.getParentFile().getAbsolutePath());
442 del = ",";
443 }
444 }
445 return sb.toString();
446 }
447
448 }
449
450 public String _basename(String args[]) {
451 if (args.length < 2) {
452 domain.warning("Need at least one file name for ${basename;...}");
453 return null;
454 } else {
455 String del = "";
456 StringBuffer sb = new StringBuffer();
457 for (int i = 1; i < args.length; i++) {
458 File f = new File(args[i]).getAbsoluteFile();
459 if (f.exists() && f.getParentFile().exists()) {
460 sb.append(del);
461 sb.append(f.getName());
462 del = ",";
463 }
464 }
465 return sb.toString();
466 }
467
468 }
469
470 public String _isfile(String args[]) {
471 if (args.length < 2) {
472 domain.warning("Need at least one file name for ${isfile;...}");
473 return null;
474 } else {
475 boolean isfile = true;
476 for (int i = 1; i < args.length; i++) {
477 File f = new File(args[i]).getAbsoluteFile();
478 isfile &= f.isFile();
479 }
480 return isfile ? "true" : "false";
481 }
482
483 }
484
485 public String _isdir(String args[]) {
486 if (args.length < 2) {
487 domain.warning("Need at least one file name for ${isdir;...}");
488 return null;
489 } else {
490 boolean isdir = true;
491 for (int i = 1; i < args.length; i++) {
492 File f = new File(args[i]).getAbsoluteFile();
493 isdir &= f.isDirectory();
494 }
495 return isdir ? "true" : "false";
496 }
497
498 }
499
500 public String _tstamp(String args[]) {
501 String format = "yyyyMMddHHmm";
502 long now = System.currentTimeMillis();
503
504 if (args.length > 1) {
505 format = args[1];
506 if (args.length > 2) {
507 now = Long.parseLong(args[2]);
508 if (args.length > 3) {
509 domain.warning("Too many arguments for tstamp: "
510 + Arrays.toString(args));
511 }
512 }
513 }
514 SimpleDateFormat sdf = new SimpleDateFormat(format);
515 return sdf.format(new Date(now));
516 }
517
518 /**
519 * Wildcard a directory. The lists can contain Instruction that are matched
520 * against the given directory
521 *
522 * ${wc;<dir>;<list>(;<list>)*}
523 *
524 * @author aqute
525 *
526 */
527
528 public String _lsr(String args[]) {
529 return ls(args, true);
530 }
531
532 public String _lsa(String args[]) {
533 return ls(args, false);
534 }
535
536 String ls(String args[], boolean relative) {
537 if (args.length < 2)
538 throw new IllegalArgumentException(
539 "the ${ls} macro must at least have a directory as parameter");
540
541 File dir = new File(args[1]);
542 if (!dir.isAbsolute())
543 throw new IllegalArgumentException(
544 "the ${ls} macro directory parameter is not absolute: "
545 + dir);
546
547 if (!dir.exists())
548 throw new IllegalArgumentException(
549 "the ${ls} macro directory parameter does not exist: "
550 + dir);
551
552 if (!dir.isDirectory())
553 throw new IllegalArgumentException(
554 "the ${ls} macro directory parameter points to a file instead of a directory: "
555 + dir);
556
557 String[] files = dir.list();
558 List<String> result;
559
560 if (args.length < 3) {
561 result = Arrays.asList(files);
562 } else
563 result = new ArrayList<String>();
564
565 for (int i = 2; i < args.length; i++) {
566 String parts[] = args[i].split("\\s*,\\s*");
567 for (String pattern : parts) {
568 // So make it in to an instruction
569 Instruction instr = Instruction.getPattern(pattern);
570
571 // For each project, match it against the instruction
572 for (int f = 0; f < files.length; f++) {
573 if (files[f] != null) {
574 if (instr.matches(files[f])) {
575 if (!instr.isNegated()) {
576 if (relative)
577 result.add(files[f]);
578 else
579 result.add(new File(dir, files[f])
580 .getAbsolutePath());
581 }
582 files[f] = null;
583 }
584 }
585 }
586 }
587 }
588 return Processor.join(result, ",");
589 }
590
591 public String _currenttime(String args[]) {
592 return Long.toString(System.currentTimeMillis());
593 }
594
595 /**
596 * Modify a version to set a version policy. Thed policy is a mask that is
597 * mapped to a version.
598 *
599 * <pre>
600 * + increment
601 * - decrement
602 * = maintain
603 * &tilde; discard
604 *
605 * ==+ = maintain major, minor, increment micro, discard qualifier
606 * &tilde;&tilde;&tilde;= = just get the qualifier
607 * version=&quot;[${version;==;${@}},${version;=+;${@}})&quot;
608 * </pre>
609 *
610 *
611 *
612 *
613 * @param args
614 * @return
615 */
616 static String _versionHelp = "${version;<mask>;<version>}, modify a version\n"
617 + "<mask> ::= [ M [ M [ M [ MQ ]]]\n"
618 + "M ::= '+' | '-' | MQ\n"
619 + "MQ ::= '~' | '='";
620 static Pattern _versionPattern[] = new Pattern[] { null, null,
621 Pattern.compile("[-+=~]{0,3}[=~]?"), Verifier.VERSION };
622
623 public String _version(String args[]) {
624 verifyCommand(args, _versionHelp, null, 3, 3);
625
626 String mask = args[1];
627
628 Version version = new Version(args[2]);
629 StringBuilder sb = new StringBuilder();
630 String del = "";
631
632 for (int i = 0; i < mask.length(); i++) {
633 char c = mask.charAt(i);
634 String result = null;
635 if (c != '~') {
636 if (i == 3) {
637 result = version.getQualifier();
638 } else if (Character.isDigit(c)) {
639 // Handle masks like +00, =+0
640 result = String.valueOf(c);
641 } else {
642 int x = version.get(i);
643 switch (c) {
644 case '+':
645 x++;
646 break;
647 case '-':
648 x--;
649 break;
650 case '=':
651 break;
652 }
653 result = Integer.toString(x);
654 }
655 if (result != null) {
656 sb.append(del);
657 del = ".";
658 sb.append(result);
659 }
660 }
661 }
662 return sb.toString();
663 }
664
665 /**
666 * System command. Execute a command and insert the result.
667 *
668 * @param args
669 * @param help
670 * @param patterns
671 * @param low
672 * @param high
673 */
674 public String _system(String args[]) throws Exception {
675 verifyCommand(args,
676 "${system;<command>[;<in>]}, execute a system command", null,
677 2, 3);
678 String command = args[1];
679 String input = null;
680
681 if (args.length > 2) {
682 input = args[2];
683 }
684
685 Process process = Runtime.getRuntime().exec(command, null,
686 domain.getBase());
687 if (input != null) {
688 process.getOutputStream().write(input.getBytes("UTF-8"));
689 }
690 process.getOutputStream().close();
691
692 String s = getString(process.getInputStream());
693 process.getInputStream().close();
694 int exitValue = process.waitFor();
695 if (exitValue != 0) {
696 domain.error("System command " + command + " failed with "
697 + exitValue);
698 }
699 return s.trim();
700 }
701
702 /**
703 * Get the contents of a file.
704 *
705 * @param in
706 * @return
707 * @throws IOException
708 */
709
710 public String _cat(String args[]) throws IOException {
711 verifyCommand(args, "${cat;<in>}, get the content of a file", null, 2,
712 2);
713 File f = domain.getFile(args[1]);
714 if (f.isFile()) {
715 InputStream in = new FileInputStream(f);
716 return getString(in);
717 } else if (f.isDirectory()) {
718 return Arrays.toString(f.list());
719 } else {
720 try {
721 URL url = new URL(args[1]);
722 InputStream in = url.openStream();
723 return getString(in);
724 } catch (MalformedURLException mfue) {
725 // Ignore here
726 }
727 return null;
728 }
729 }
730
731 public static String getString(InputStream in) throws IOException {
732 try {
733 StringBuilder sb = new StringBuilder();
734 BufferedReader rdr = new BufferedReader(new InputStreamReader(in));
735 String line = null;
736 while ((line = rdr.readLine()) != null) {
737 sb.append(line);
738 sb.append("\n");
739 }
740 return sb.toString();
741 } finally {
742 in.close();
743 }
744 }
745
746 public static void verifyCommand(String args[], String help,
747 Pattern[] patterns, int low, int high) {
748 String message = "";
749 if (args.length > high) {
750 message = "too many arguments";
751 } else if (args.length < low) {
752 message = "too few arguments";
753 } else {
754 for (int i = 0; patterns != null && i < patterns.length
755 && i < args.length - 1; i++) {
756 if (patterns[i] != null
757 && !patterns[i].matcher(args[i + 1]).matches()) {
758 message += String.format(
759 "Argument %s (%s) does not match %s\n", i, args[i],
760 patterns[i].pattern());
761 }
762 }
763 }
764 if (message.length() != 0) {
765 StringBuilder sb = new StringBuilder();
766 String del = "${";
767 for (String arg : args) {
768 sb.append(del);
769 sb.append(arg);
770 del = ";";
771 }
772 sb.append("}, is not understood. ");
773 sb.append(message);
774 throw new IllegalArgumentException(sb.toString());
775 }
776 }
777
778 // Helper class to track expansion of variables
779 // on the stack.
780 static class Link {
781 Link previous;
782 String key;
783
784 public Link(Link previous, String key) {
785 this.previous = previous;
786 this.key = key;
787 }
788
789 public boolean contains(String key) {
790 if (this.key.equals(key))
791 return true;
792
793 if (previous == null)
794 return false;
795
796 return previous.contains(key);
797 }
798
799 public String toString() {
800 StringBuffer sb = new StringBuffer();
801 String del = "[";
802 for (Link r = this; r != null; r = r.previous) {
803 sb.append(del);
804 sb.append(r.key);
805 del = ",";
806 }
807 sb.append("]");
808 return sb.toString();
809 }
810 }
811
812 /**
813 * Take all the properties and translate them to actual values. This method
814 * takes the set properties and traverse them over all entries, including
815 * the default properties for that properties. The values no longer contain
816 * macros.
817 *
818 * @return A new Properties with the flattened values
819 */
820 public Properties getFlattenedProperties() {
821 // Some macros only work in a lower processor, so we
822 // do not report unknown macros while flattening
823 flattening = true;
824 try {
825 Properties flattened = new Properties();
826 for (Enumeration<?> e = properties.propertyNames(); e
827 .hasMoreElements();) {
828 String key = (String) e.nextElement();
829 if (!key.startsWith("_"))
830 if ( key.startsWith("-"))
831 flattened.put(key, properties.getProperty(key));
832 else
833 flattened.put(key, process(properties.getProperty(key)));
834 }
835 return flattened;
836 } finally {
837 flattening = false;
838 }
839 };
840
841}