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