acebf0c05c450d72e88047232b26b3abb91032be
[mir.git] / source / mir / util / ParameterExpander.java
1 package  mir.util;
2
3 import multex.Failure;
4 import multex.Exc;
5
6 import java.util.*;
7
8 public class ParameterExpander {
9   final static String NODE_SEPARATOR = ".";
10
11   public static List splitString(String aString, String aSeparator) {
12     List result= new Vector();
13     int previousPosition = 0;
14     int position;
15     int endOfNamePosition;
16
17     while ((position = aString.indexOf(aSeparator, previousPosition))>=0) {
18       result.add(aString.substring(previousPosition, position));
19       previousPosition = position + aSeparator.length();
20     }
21
22     result.add(aString.substring(previousPosition, aString.length()));
23
24     return result;
25   }
26
27   private static Object findNode(String aKey, Map aMap, List aParts, boolean aMakeIfNotPresent) throws Exception {
28     Iterator i;
29     String location = "";
30     Object node = aMap;
31     Object newNode;
32
33     i = aParts.iterator();
34
35     while (i.hasNext()) {
36       String part = (String) i.next();
37
38       if (!(node instanceof Map)) {
39         throw new Exception( "Can't expand key " + aKey + ": " + location + " is not a map" );
40       }
41
42       if (location.length()>0) {
43         location=location + NODE_SEPARATOR;
44       }
45       location = location + part;
46
47       newNode = ((Map) node).get(part);
48
49       if (newNode == null)
50         if (aMakeIfNotPresent) {
51           newNode = new HashMap();
52           ((Map) node).put(part, newNode);
53         }
54         else
55           throw new ParameterExpanderExc( "Can't expand key {1}: {2} does not exist", new Object[]{aKey,location} );
56
57       node = newNode;
58     }
59
60     return node;
61   }
62
63   public static Object findValueForKey(Map aMap, String aKey) throws Exception {
64     Object node;
65     List parts = splitString(aKey, NODE_SEPARATOR);
66
67     node = findNode(aKey, aMap, parts, false);
68
69     return node;
70   }
71
72   public static String findStringForKey(Map aMap, String aKey) throws Exception {
73     Object expandedValue = findValueForKey(aMap, aKey);
74
75     if (!(expandedValue instanceof String))
76       throw new ParameterExpanderExc( "Value of key is not a string but a {1}", new Object[]{expandedValue.getClass().getName()} );
77
78     return (String) expandedValue;
79   }
80
81   public static void setValueForKey(Map aMap, String aKey, Object aValue) throws Exception {
82     List parts = splitString(aKey, NODE_SEPARATOR);
83
84     String key = (String) parts.get(parts.size()-1);
85     parts.remove(parts.size()-1);
86
87     Object node=findNode(aKey, aMap, parts, true);
88
89     if (node instanceof Map) {
90       ((Map) node).put(key, aValue);
91     }
92     else
93       throw new ParameterExpanderExc( "Can't set key {1}: not inside a Map", new Object[]{aKey} );
94   }
95
96   public static String expandExpression(Map aMap, String anExpression) throws Exception {
97     int previousPosition = 0;
98     int position;
99     int endOfNamePosition;
100     StringBuffer result = new StringBuffer();
101
102     while ((position=anExpression.indexOf("$", previousPosition))>=0) {
103       result.append(anExpression.substring(previousPosition, position));
104
105       if (position>=anExpression.length()-1) {
106         result.append(anExpression.substring(position, anExpression.length()));
107         previousPosition=anExpression.length();
108       }
109       else
110       {
111         if (anExpression.charAt(position+1) == '{') {
112           endOfNamePosition=anExpression.indexOf('}', position);
113           if (endOfNamePosition>=0) {
114             result.append(findStringForKey(aMap, anExpression.substring(position+2, endOfNamePosition)));
115             previousPosition=endOfNamePosition+1;
116           }
117           else {
118             throw new ParameterExpanderExc("Missing } in {1}",new Object[]{anExpression});
119           }
120         }
121         else
122         {
123           previousPosition=position+2;
124           result.append(anExpression.charAt(position+1));
125         }
126       }
127     }
128     result.append(anExpression.substring(previousPosition, anExpression.length()));
129
130     return result.toString();
131   }
132
133   public static boolean evaluateBooleanExpression(Map aMap, String anExpression) throws Exception {
134     Parser parser = new Parser(anExpression, aMap);
135
136     return parser.parseBoolean();
137   }
138
139   public static Object evaluateExpression(Map aMap, String anExpression) throws Exception {
140     Parser parser = new Parser(anExpression, aMap);
141
142     return parser.parseWhole();
143   }
144
145   private static class Reader {
146     private String data;
147     private int position;
148
149     public Reader(String aData) {
150       data = aData;
151       position=0;
152     }
153
154     public Character peek() {
155       if (position<data.length()) {
156         return (new Character(data.charAt(position)));
157       }
158
159       return null;
160     }
161
162     public boolean hasNext() {
163       return peek()!=null;
164     }
165
166     public Character getNext() {
167       Character result = peek();
168
169       if (result!=null)
170         position++;
171
172       return result;
173     }
174
175     public int getPosition() {
176       return position;
177     }
178   }
179
180   private static abstract class Token {
181   }
182
183   public static abstract class PunctuationToken extends Token { public PunctuationToken() { }; }
184     private static class LeftSquareBraceToken extends PunctuationToken {};
185     private static class RightSquareBraceToken extends PunctuationToken {};
186     private static class ANDToken extends PunctuationToken {};
187     private static class ORToken extends PunctuationToken {};
188     private static class EqualsToken extends PunctuationToken {};
189     private static class EqualsNotToken extends PunctuationToken {};
190     private static class NOTToken extends PunctuationToken {};
191     private static class LeftParenthesisToken extends PunctuationToken {};
192     private static class RightParenthesisToken extends PunctuationToken {};
193     private static class CommaToken extends PunctuationToken {};
194     private static class PeriodToken extends PunctuationToken {};
195     private static class PlusToken extends PunctuationToken {};
196     private static class TimesToken extends PunctuationToken {};
197     private static class DivideToken extends PunctuationToken {};
198     private static class MinusToken extends PunctuationToken {};
199     private static class ConcatenateToken extends PunctuationToken {};
200     private static class LessThanOrEqualsToken extends PunctuationToken {};
201     private static class GreaterThanOrEqualsToken extends PunctuationToken {};
202     private static class LessThanToken extends PunctuationToken {};
203     private static class GreaterThanToken extends PunctuationToken {};
204
205
206   private static class IdentifierToken extends Token {
207     private String name;
208
209     public IdentifierToken(String aName) {
210       name = aName;
211     }
212
213     public String getName() {
214       return name;
215     }
216
217   }
218
219   private static class LiteralToken extends Token {
220     private Object value;
221
222     public LiteralToken(Object aValue) {
223       value = aValue;
224     }
225
226     public Object getValue() {
227       return value;
228     }
229   }
230
231   private static class Scanner {
232     private Reader reader;
233     private Token nextToken;
234
235     public Scanner(Reader aReader) {
236       reader = aReader;
237     }
238
239     public Token scanStringLiteral() {
240       StringBuffer result = new StringBuffer();
241       Character delimiter;
242
243       delimiter = reader.getNext();
244
245       while (reader.hasNext() && !reader.peek().equals(delimiter)) {
246         result.append(reader.getNext());
247       }
248
249       if (!reader.hasNext())
250         throw new RuntimeException("unterminated string");
251       else
252         reader.getNext();
253
254       return new LiteralToken(result.toString());
255     }
256
257     public int getPosition() {
258       return reader.getPosition();
259     }
260
261     private Token scanNumber() {
262       StringBuffer result = new StringBuffer();
263       result.append(reader.getNext());
264
265       while (reader.hasNext() && isNumberRest(reader.peek().charValue())) {
266         result.append(reader.getNext());
267       }
268
269       try {
270         return new LiteralToken(Integer.getInteger(result.toString()));
271       }
272       catch (NumberFormatException e) {
273         throw new RuntimeException("Invalid number: " + e.getMessage());
274       }
275     }
276
277     private Token scanIdentifierKeyword() {
278       StringBuffer result = new StringBuffer();
279       result.append(reader.getNext());
280
281       while (reader.hasNext() && isIdentifierRest(reader.peek().charValue())) {
282         result.append(reader.getNext());
283       }
284
285       return new IdentifierToken(result.toString());
286     }
287
288     private Token scanPunctuation() {
289       Character c;
290
291       c = reader.getNext();
292
293       switch(c.charValue()) {
294         case '[': return new LeftSquareBraceToken();
295         case ']': return new RightSquareBraceToken();
296         case '&':
297           if (reader.hasNext() && reader.peek().charValue() == '&') {
298             reader.getNext();
299             return new ANDToken();
300           }
301           else {
302             return new ConcatenateToken();
303           }
304         case '|':
305           if (reader.hasNext() && reader.peek().charValue() == '|') {
306             reader.getNext();
307             return new ORToken();
308           }
309           else {
310             throw new RuntimeException("Unknown character: '|'");
311           }
312
313         case '=':
314           if (reader.hasNext() && reader.peek().charValue() == '=') {
315             reader.getNext();
316             return new EqualsToken();
317           }
318           else {
319             throw new RuntimeException("Unknown character: '='");
320           }
321
322         case '!':
323           if (reader.hasNext() && reader.peek().charValue() == '=') {
324             reader.getNext();
325             return new EqualsNotToken();
326           }
327           else {
328             return new NOTToken();
329           }
330
331         case '(': return new LeftParenthesisToken ();
332
333         case ')': return new RightParenthesisToken ();
334         case ',': return new CommaToken ();
335         case '.': return new PeriodToken ();
336         case '+': return new PlusToken ();
337         case '*': return new TimesToken ();
338         case '/': return new DivideToken ();
339         case '-': return new MinusToken ();
340         case '<':
341           if (reader.hasNext() && reader.peek().charValue() == '=') {
342             reader.getNext();
343             return new LessThanOrEqualsToken();
344           }
345           else {
346             return new LessThanToken();
347           }
348
349         case '>':
350           if (reader.hasNext() && reader.peek().charValue() == '=') {
351             reader.getNext();
352             return new GreaterThanOrEqualsToken();
353           }
354           else {
355             return new GreaterThanToken();
356           }
357         default:
358           throw new RuntimeException("Unexpected character: "+c);
359       }
360     }
361
362     public void skipWhitespace() {
363       while (reader.hasNext() && Character.isWhitespace(reader.peek().charValue()))
364         reader.getNext();
365     };
366
367     private boolean isIdentifierStart(char c) {
368       return Character.isLetter(c) || (c == '_');
369     }
370
371     private boolean isIdentifierRest(char c) {
372       return Character.isLetterOrDigit(c) || (c == '_');
373     }
374
375     private boolean isNumberStart(char c) {
376       return Character.isDigit(c);
377     }
378
379     private boolean isNumberRest(char c) {
380       return Character.isDigit(c);
381     }
382
383     public Token scanNext() {
384       skipWhitespace();
385
386       if (!reader.hasNext())
387         return null;
388
389       Character c = reader.peek();
390
391       switch(c.charValue()) {
392         case '\'':
393         case '"': return scanStringLiteral();
394
395         default: {
396           if (isIdentifierStart(c.charValue())) {
397             return scanIdentifierKeyword();
398           }
399           else if (isNumberStart(c.charValue())) {
400             return scanNumber();
401           }
402           else
403             return scanPunctuation();
404         }
405       }
406     }
407
408     public Token scan() {
409       Token result = null;
410
411       if (nextToken!=null) {
412         result = nextToken;
413       }
414       else {
415         result = scanNext();
416       }
417
418       nextToken=null;
419
420       return result;
421     }
422
423     public Token peek() {
424       nextToken = scan();
425
426       return nextToken;
427     }
428
429     public boolean hasToken() {
430       return peek()!=null;
431     }
432   }
433
434   private static class Parser {
435     private Scanner scanner;
436     private Map valueMap;
437
438     public Parser(String anExpression, Map aValueMap) {
439       scanner = new Scanner(new Reader(anExpression));
440       valueMap = aValueMap;
441     }
442
443     public boolean parseBoolean() {
444       try {
445         return interpretAsBoolean(parseWhole());
446       }
447       catch (Throwable t) {
448         t.printStackTrace(System.out);
449         throw new RuntimeException("Parser error at position "+getLocation()+":"+t.getMessage());
450       }
451     }
452
453     public String parseString() {
454       try {
455         return interpretAsString(parseWhole());
456       }
457       catch (Throwable t) {
458         throw new RuntimeException("Parser error at position "+getLocation()+":"+t.getMessage());
459       }
460     }
461
462     private String getLocation() {
463       int position = scanner.getPosition();
464
465       return Integer.toString(position);
466     }
467
468     private Object parseWhole() {
469       Object result = parse();
470
471       if (scanner.hasToken()) {
472         throw new RuntimeException("Operator expected");
473       }
474
475       return result;
476     }
477
478     private Object parse() {
479       return parseUntil(MAX_OPERATOR_LEVEL);
480     }
481
482     private Object parseSet() {
483       Token token;
484       Object expression;
485       Set result = new HashSet();
486
487       token = scanner.scan();
488       if (!(token instanceof LeftParenthesisToken)) {
489         throw new RuntimeException("( expected after in keyword");
490       }
491
492       if (scanner.peek() instanceof RightParenthesisToken) {
493         scanner.scan();
494         return result;
495       }
496
497       do {
498         expression = parse();
499
500         if (expression==null) {
501           throw new RuntimeException("expression expected");
502         }
503
504         result.add(expression);
505
506         token = scanner.scan();
507       } while (token instanceof CommaToken);
508
509       if (!(token instanceof RightParenthesisToken)) {
510         throw new RuntimeException(") or , expected");
511       }
512
513       return result;
514     }
515
516     private Object parseVariable() {
517       boolean done;
518       Token token;
519       Object currentValue = valueMap;
520       Object qualifier;
521
522       do {
523         token = scanner.scan();
524         if (token instanceof LeftSquareBraceToken) {
525           qualifier = parseUntil(MAX_OPERATOR_LEVEL);
526           token = scanner.scan();
527           if (!(token instanceof RightSquareBraceToken))
528             throw new RuntimeException("] expected");
529         }
530         else if (token instanceof IdentifierToken) {
531           qualifier = ((IdentifierToken) token).getName();
532         }
533         else
534           throw new RuntimeException("fieldname or [ expected");
535
536         if (currentValue!=null) {
537           if (currentValue instanceof Map) {
538             currentValue = ((Map) currentValue).get(qualifier);
539           }
540           else {
541             throw new RuntimeException("cannot reference into anything other than a map");
542           }
543         }
544         else {
545           // throw? or allow null.null?
546         }
547
548         if (scanner.peek() instanceof PeriodToken)
549         {
550           scanner.scan();
551           done = false;
552         }
553         else
554           done = true;
555       } while (!done);
556
557       return currentValue;
558     }
559
560
561     private Object parseUntil(int aMaxOperatorLevel) {
562       Token token = scanner.peek();
563       Object value;
564
565       if (token instanceof LeftParenthesisToken) {
566         scanner.scan();
567         value = parse();
568         token = scanner.peek();
569         if (!(token instanceof RightParenthesisToken))
570           throw new RuntimeException(") expected");
571         scanner.scan();
572       }
573       else if (isUnaryOperator(token)) {
574         scanner.scan();
575         value = parseUntil(unaryOperatorLevel(token));
576         value = expandOperatorExpression(token, value);
577       }
578       else if (token instanceof IdentifierToken) {
579         value = parseVariable();
580       }
581       else if (token instanceof LiteralToken) {
582         scanner.scan();
583         value = ((LiteralToken) token).getValue();
584       }
585       else
586         throw new RuntimeException("Expression expected");
587
588       token = scanner.peek();
589
590       while (isBinaryOperator(token) && binaryOperatorLevel(token)<aMaxOperatorLevel) {
591         Object value2;
592         scanner.scan();
593
594         if (isINOperator(token)) {
595           value2 = parseSet();
596         }
597         else {
598           value2 = parseUntil(binaryOperatorLevel(token));
599         }
600
601         value = expandOperatorExpression(token, value, value2);
602
603         token = scanner.peek();
604       }
605
606       return value;
607     }
608
609     private static final int MAX_OPERATOR_LEVEL = 1000;               // && || !
610     private static final int LOGICAL_OPERATOR_LEVEL = 5;               // && || !
611     private static final int COMPARISON_OPERATOR_LEVEL = 4;            // == <= >= in < >
612     private static final int ADDITION_OPERATOR_LEVEL = 3;              // + - &
613     private static final int MULTIPLICATION_OPERATOR_LEVEL = 2;        // * /
614
615     private int unaryOperatorLevel(Token aToken) {
616       if (aToken instanceof NOTToken)
617         return LOGICAL_OPERATOR_LEVEL;
618       else if (aToken instanceof MinusToken)
619         return ADDITION_OPERATOR_LEVEL;
620
621       throw new RuntimeException("Internal error: unknown unary operator: " + aToken.getClass().getName());
622     }
623
624     private boolean isUnaryOperator(Token aToken) {
625       return
626           ((aToken instanceof NOTToken) ||
627            (aToken instanceof MinusToken));
628     }
629
630     private int binaryOperatorLevel(Token aToken) {
631       if ((aToken instanceof ANDToken) ||
632           (aToken instanceof ORToken))
633         return LOGICAL_OPERATOR_LEVEL;
634
635       if ((aToken instanceof EqualsToken) ||
636           (aToken instanceof EqualsNotToken) ||
637           (aToken instanceof LessThanOrEqualsToken) ||
638           (aToken instanceof LessThanToken) ||
639           (aToken instanceof GreaterThanOrEqualsToken) ||
640           (aToken instanceof GreaterThanToken) ||
641           isINOperator(aToken))
642         return COMPARISON_OPERATOR_LEVEL;
643
644       if ((aToken instanceof PlusToken) ||
645           (aToken instanceof ConcatenateToken) ||
646           (aToken instanceof MinusToken))
647         return ADDITION_OPERATOR_LEVEL;
648
649       if ((aToken instanceof TimesToken) ||
650           (aToken instanceof DivideToken))
651         return MULTIPLICATION_OPERATOR_LEVEL;
652
653       throw new RuntimeException("Internal error: unknown binary operator: " + aToken.getClass().getName());
654     }
655
656     private boolean isINOperator(Token aToken) {
657       return (aToken instanceof IdentifierToken && ((IdentifierToken) aToken).getName().equals("in"));
658     }
659
660     private boolean isBinaryOperator(Token aToken) {
661       return
662            (aToken instanceof ANDToken) ||
663            (aToken instanceof ORToken) ||
664            (aToken instanceof EqualsToken) ||
665            (aToken instanceof EqualsNotToken) ||
666            (aToken instanceof PlusToken) ||
667            (aToken instanceof TimesToken) ||
668            (aToken instanceof DivideToken) ||
669            (aToken instanceof MinusToken) ||
670            (aToken instanceof ConcatenateToken) ||
671            (aToken instanceof LessThanOrEqualsToken) ||
672            (aToken instanceof LessThanToken) ||
673            (aToken instanceof GreaterThanOrEqualsToken) ||
674            (aToken instanceof GreaterThanToken) ||
675            isINOperator(aToken);
676     }
677
678     private boolean interpretAsBoolean(Object aValue) {
679       if (aValue instanceof Boolean)
680         return ((Boolean) aValue).booleanValue();
681
682       return aValue!=null;
683     }
684
685     private int interpretAsInteger(Object aValue) {
686       if (aValue instanceof Integer)
687         return ((Integer) aValue).intValue();
688
689       throw new RuntimeException("Not an integer");
690     }
691
692     private String interpretAsString(Object aValue) {
693       if (aValue instanceof String)
694         return (String) aValue;
695       if (aValue instanceof Integer)
696         return ((Integer) aValue).toString();
697
698       throw new RuntimeException("Not a string");
699     }
700
701     private Object expandOperatorExpression(Token aToken, Object aValue) {
702       if (aToken instanceof NOTToken)
703         return new Boolean(!interpretAsBoolean(aValue));
704       else if (aToken instanceof MinusToken)
705         return new Integer(-interpretAsInteger(aValue));
706
707       throw new RuntimeException("Internal error: unknown unary operator: " + aToken.getClass().getName());
708     }
709
710     private boolean areEqual(Object aValue1, Object aValue2) {
711       if (aValue1==null || aValue2==null)
712         return (aValue1==null) && (aValue2==null);
713       else
714         return aValue1.equals(aValue2);
715     }
716
717     private Object expandOperatorExpression(Token aToken, Object aValue1, Object aValue2) {
718       if (isINOperator(aToken)) {
719         if (!(aValue2 instanceof Set)) {
720           throw new RuntimeException("Internal error: set expected");
721         }
722
723         Iterator i = ((Set) aValue2).iterator();
724
725         while (i.hasNext()) {
726           if (areEqual(aValue1, i.next()))
727             return Boolean.TRUE;
728         }
729
730         return Boolean.FALSE;
731       }
732
733       if (aToken instanceof ANDToken)
734         return new Boolean(interpretAsBoolean(aValue1) && interpretAsBoolean(aValue2));
735       if (aToken instanceof ORToken)
736         return new Boolean(interpretAsBoolean(aValue1) || interpretAsBoolean(aValue2));
737       if (aToken instanceof EqualsToken) {
738         return new Boolean(areEqual(aValue1, aValue2));
739       }
740       if (aToken instanceof EqualsNotToken)
741         return new Boolean(!areEqual(aValue1, aValue2));
742       if (aToken instanceof PlusToken)
743         return new Integer(interpretAsInteger(aValue1) + interpretAsInteger(aValue2));
744       if (aToken instanceof TimesToken)
745         return new Integer(interpretAsInteger(aValue1) * interpretAsInteger(aValue2));
746       if (aToken instanceof DivideToken)
747         return new Integer(interpretAsInteger(aValue1) / interpretAsInteger(aValue2));
748       if (aToken instanceof MinusToken)
749         return new Integer(interpretAsInteger(aValue1) - interpretAsInteger(aValue2));
750
751       if (aToken instanceof ConcatenateToken)
752         return interpretAsString(aValue1) + interpretAsString(aValue1);
753
754       if (aToken instanceof LessThanOrEqualsToken)
755         return new Boolean(interpretAsInteger(aValue1) <= interpretAsInteger(aValue2));
756       if (aToken instanceof LessThanToken)
757         return new Boolean(interpretAsInteger(aValue1) < interpretAsInteger(aValue2));
758       if (aToken instanceof GreaterThanOrEqualsToken)
759         return new Boolean(interpretAsInteger(aValue1) >= interpretAsInteger(aValue2));
760       if (aToken instanceof GreaterThanToken)
761         return new Boolean(interpretAsInteger(aValue1) > interpretAsInteger(aValue2));
762
763       throw new RuntimeException("Internal error: unknown binary operator: " + aToken.getClass().getName());
764     }
765   }
766
767   public static class ParameterExpanderExc extends Exc {
768     public ParameterExpanderExc(String msg, Object[] objects) {
769       super(msg, objects);
770     }
771   }
772 }