View Javadoc
1   /*
2    * This file is part of ***  M y C o R e  ***
3    * See http://www.mycore.de/ for details.
4    *
5    * MyCoRe is free software: you can redistribute it and/or modify
6    * it under the terms of the GNU General Public License as published by
7    * the Free Software Foundation, either version 3 of the License, or
8    * (at your option) any later version.
9    *
10   * MyCoRe is distributed in the hope that it will be useful,
11   * but WITHOUT ANY WARRANTY; without even the implied warranty of
12   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13   * GNU General Public License for more details.
14   *
15   * You should have received a copy of the GNU General Public License
16   * along with MyCoRe.  If not, see <http://www.gnu.org/licenses/>.
17   */
18  
19  package org.mycore.common;
20  
21  import java.lang.reflect.Constructor;
22  import java.lang.reflect.InvocationTargetException;
23  import java.util.ArrayList;
24  import java.util.HashMap;
25  import java.util.List;
26  import java.util.Map;
27  import java.util.Map.Entry;
28  import java.util.Properties;
29  import java.util.Set;
30  
31  import org.apache.logging.log4j.LogManager;
32  import org.apache.logging.log4j.Logger;
33  
34  /**
35   * <p>
36   * This class parses and resolve strings which contains variables.
37   * To add a variable call <code>addVariable</code>.
38   * </p><p>
39   * The algorithm is optimized that each character is touched only once.
40   * </p><p>
41   * To resolve a string a valid syntax is required:
42   * </p><p>
43   * <b>{}:</b> Use curly brackets for variables or properties. For example "{var1}"
44   * or "{MCR.basedir}" 
45   * </p><p>
46   * <b>[]:</b> Use squared brackets to define a condition. All data within
47   * squared brackets is only used if the internal variables are
48   * not null and not empty. For example "[hello {lastName}]" is only resolved
49   * if the value of "lastName" is not null and not empty. Otherwise the whole
50   * content in the squared brackets are ignored.
51   * </p><p>
52   * <b>\:</b> Use the escape character to use all predefined characters.
53   * </p>
54   * <p>
55   * Sample:<br>
56   * "Lastname: {lastName}[, Firstname: {firstName}]"<br>
57   * </p>
58   * 
59   * @author Matthias Eichner
60   */
61  public class MCRTextResolver {
62  
63      private static final Logger LOGGER = LogManager.getLogger(MCRTextResolver.class);
64  
65      protected TermContainer termContainer;
66  
67      /**
68       * This map contains all variables that can be resolved.
69       */
70      protected Map<String, String> variablesMap;
71  
72      /**
73       * Retains the text if a variable couldn't be resolved.
74       * Example if {Variable} could not be resolved:
75       * true: "Hello {Variable}" -&gt; "Hello {Variable}"
76       * false: "Hello "
77       * <p>By default retainText is true</p>
78       */
79      protected boolean retainText;
80  
81      /**
82       * Defines how deep the text is resolved.
83       * <dl>
84       * <dt>Deep</dt><dd>everything is resolved</dd>
85       * <dt>NoVariables</dt><dd>the value of variables is not being resolved</dd>
86       * </dl>
87       */
88      protected ResolveDepth resolveDepth;
89  
90      protected CircularDependencyTracker tracker;
91  
92      /**
93       * Creates the term list for the text resolver and adds
94       * the default terms.
95       */
96      protected void registerDefaultTerms() throws NoSuchMethodException, InvocationTargetException,
97          IllegalAccessException, InstantiationException {
98          registerTerm(Variable.class);
99          registerTerm(Condition.class);
100         registerTerm(EscapeCharacter.class);
101     }
102 
103     /**
104      * Register a new term. The resolver invokes the term via reflection.
105      * 
106      * @param termClass the term class to register. 
107      */
108     public void registerTerm(Class<? extends Term> termClass) throws NoSuchMethodException, InvocationTargetException,
109         IllegalAccessException, InstantiationException {
110         this.termContainer.add(termClass);
111     }
112 
113     /**
114      * Unregister a term.
115      * 
116      * @param termClass this class is unregistered
117      */
118     public void unregisterTerm(Class<? extends Term> termClass) throws NoSuchMethodException,
119         InvocationTargetException, InstantiationException, IllegalAccessException {
120         this.termContainer.remove(termClass);
121     }
122 
123     /**
124      * Defines how deep the text is resolved.
125      * <dl>
126      * <dt>Deep</dt><dd>everything is resolved</dd>
127      * <dt>NoVariables</dt><dd>the value of variables is not being resolved</dd>
128      * </dl>
129      */
130     public enum ResolveDepth {
131         Deep, NoVariables
132     }
133 
134     /**
135      * Creates a new text resolver with a map of variables.
136      */
137     public MCRTextResolver() {
138         this.variablesMap = new HashMap<>();
139         this.setResolveDepth(ResolveDepth.Deep);
140         this.setRetainText(true);
141         this.tracker = new CircularDependencyTracker(this);
142         try {
143             this.termContainer = new TermContainer(this);
144             this.registerDefaultTerms();
145         } catch (Exception exc) {
146             throw new MCRException("Unable to register default terms", exc);
147         }
148     }
149 
150     /**
151      * Creates a new text resolver. To add variables call
152      * <code>addVariable</code>, otherwise only MyCoRe property
153      * resolving is possible.
154      */
155     public MCRTextResolver(Map<String, String> variablesMap) {
156         this();
157         mixin(variablesMap);
158     }
159 
160     public MCRTextResolver(Properties properties) {
161         this();
162         mixin(properties);
163     }
164 
165     protected TermContainer getTermContainer() {
166         return this.termContainer;
167     }
168 
169     protected CircularDependencyTracker getTracker() {
170         return this.tracker;
171     }
172 
173     public void mixin(Map<String, String> variables) {
174         for (Entry<String, String> entrySet : variables.entrySet()) {
175             String key = entrySet.getKey();
176             String value = entrySet.getValue();
177             this.addVariable(key, value);
178         }
179     }
180 
181     public void mixin(Properties properties) {
182         for (Entry<Object, Object> entrySet : properties.entrySet()) {
183             String key = entrySet.getKey().toString();
184             String value = entrySet.getValue().toString();
185             this.addVariable(key, value);
186         }
187     }
188 
189     /**
190      * Sets if the text should be retained if a variable couldn't be resolved.
191      * <p>
192      * Example:<br>
193      * true: "Hello {Variable}" -&gt; "Hello {Variable}"<br>
194      * false: "Hello "
195      * </p>
196      * <p>By default retainText is true</p>
197      */
198     public void setRetainText(boolean retainText) {
199         this.retainText = retainText;
200     }
201 
202     /**
203      * Checks if the text should be retained if a variable couldn't be resolved.
204      * <p>By default retainText is true</p>
205      */
206     public boolean isRetainText() {
207         return this.retainText;
208     }
209 
210     /**
211      * Adds a new variable to the resolver. This overwrites a
212      * existing variable with the same name.
213      * 
214      * @param name name of the variable
215      * @param value value of the variable
216      * @return the previous value of the specified name, or null
217      * if it did not have one
218      */
219     public String addVariable(String name, String value) {
220         return variablesMap.put(name, value);
221     }
222 
223     /**
224      * Removes a variable from the resolver. This method does
225      * nothing if no variable with the name exists.
226      * 
227      * @return the value of the removed variable, or null if
228      * no variable with the name exists
229      */
230     public String removeVariable(String name) {
231         return variablesMap.remove(name);
232     }
233 
234     /**
235      * Checks if a variable with the specified name exists.
236      * 
237      * @return true if a variable exists, otherwise false
238      */
239     public boolean containsVariable(String name) {
240         return variablesMap.containsKey(name);
241     }
242 
243     /**
244      * Sets the resolve depth.
245      * 
246      * @param resolveDepth defines how deep the text is resolved.
247      */
248     public void setResolveDepth(ResolveDepth resolveDepth) {
249         this.resolveDepth = resolveDepth;
250     }
251 
252     /**
253      * Returns the current resolve depth.
254      * 
255      * @return resolve depth enumeration
256      */
257     public ResolveDepth getResolveDepth() {
258         return this.resolveDepth;
259     }
260 
261     /**
262      * This method resolves all variables in the text.
263      * The syntax is described at the head of the class.
264      * 
265      * @param text the string where the variables have to be
266      * resolved
267      * @return the resolved string
268      */
269     public String resolve(String text) {
270         this.getTracker().clear();
271         Text textResolver = new Text(this);
272         textResolver.resolve(text, 0);
273         return textResolver.getValue();
274     }
275 
276     /**
277      * Returns the value of a variable.
278      * 
279      * @param varName the name of the variable
280      * @return the value
281      */
282     public String getValue(String varName) {
283         return variablesMap.get(varName);
284     }
285 
286     /**
287      * Returns a <code>Map</code> of all variables.
288      * 
289      * @return a <code>Map</code> of all variables.
290      */
291     public Map<String, String> getVariables() {
292         return variablesMap;
293     }
294 
295     /**
296      * A term is a defined part in a text. In general, a term is defined by brackets,
297      * but this is not required. Here are some example terms:
298      * <ul>
299      * <li>Variable: {term1}</li>
300      * <li>Condition: [term2]</li>
301      * <li>EscapeChar: \[</li>
302      * </ul>
303      * 
304      * You can write your own terms and add them to the text resolver. A sample is
305      * shown in the <code>MCRTextResolverTest</code> class.
306      * 
307      * @author Matthias Eichner
308      */
309     protected abstract static class Term {
310         /**
311          * The string buffer within the term. For example: {<b>var</b>}. 
312          */
313         protected StringBuffer termBuffer;
314 
315         /**
316          * If the term is successfully resolved. By default this
317          * is true.
318          */
319         protected boolean resolved;
320 
321         /**
322          * The current character position in the term.
323          */
324         protected int position;
325 
326         protected MCRTextResolver textResolver;
327 
328         public Term(MCRTextResolver textResolver) {
329             this.textResolver = textResolver;
330             this.termBuffer = new StringBuffer();
331             this.resolved = true;
332             this.position = 0;
333         }
334 
335         /**
336          * Resolves the text from the startPosition to the end of the text
337          * or if a term specific end character is found.
338          * 
339          * @param text the term to resolve
340          * @param startPosition the current character position
341          * @return the value of the term after resolving
342          */
343         public String resolve(String text, int startPosition) {
344             for (position = startPosition; position < text.length(); position++) {
345                 Term internalTerm = getTerm(text, position);
346                 if (internalTerm != null) {
347                     position += internalTerm.getStartEnclosingString().length();
348                     internalTerm.resolve(text, position);
349                     if (!internalTerm.resolved) {
350                         resolved = false;
351                     }
352                     position = internalTerm.position;
353                     termBuffer.append(internalTerm.getValue());
354                 } else {
355                     boolean complete = resolveInternal(text, position);
356                     if (complete) {
357                         int endEnclosingSize = getEndEnclosingString().length();
358                         if (endEnclosingSize > 1) {
359                             position += endEnclosingSize - 1;
360                         }
361                         break;
362                     }
363                 }
364             }
365             return getValue();
366         }
367 
368         /**
369          * Returns a new term in dependence of the current character (position of the text).
370          * If no term is defined null is returned.
371          * 
372          * @return a term or null if no one found
373          */
374         private Term getTerm(String text, int pos) {
375             TermContainer termContainer = this.getTextResolver().getTermContainer();
376             for (Entry<String, Class<? extends Term>> termEntry : termContainer.getTermSet()) {
377                 String startEnclosingStringOfTerm = termEntry.getKey();
378                 if (text.startsWith(startEnclosingStringOfTerm, pos)
379                     && !startEnclosingStringOfTerm.equals(this.getEndEnclosingString())) {
380                     try {
381                         return termContainer.instantiate(termEntry.getValue());
382                     } catch (Exception exc) {
383                         LOGGER.error(exc);
384                     }
385                 }
386             }
387             return null;
388         }
389 
390         /**
391          * Does term specific resolving for the current character.
392          * 
393          * @return true if the end string is reached, otherwise false
394          */
395         protected abstract boolean resolveInternal(String text, int pos);
396 
397         /**
398          * Returns the value of the term. Overwrite this if you
399          * don't want to get the default termBuffer content as value.
400          * 
401          * @return the value of the term
402          */
403         public String getValue() {
404             return termBuffer.toString();
405         }
406 
407         /**
408          * Implement this to define the start enclosing string for
409          * your term. The resolver searches in the text for this
410          * string, if found, the text is processed by your term.
411          * 
412          * @return the start enclosing string
413          */
414         public abstract String getStartEnclosingString();
415 
416         /**
417          * Implement this to define the end enclosing string for
418          * your term. You have to check manual in the
419          * <code>resolveInternal</code> method if the end of  
420          * your term is reached.
421          * 
422          * @return the end enclosing string
423          */
424         public abstract String getEndEnclosingString();
425 
426         public MCRTextResolver getTextResolver() {
427             return textResolver;
428         }
429 
430     }
431 
432     /**
433      * A variable is surrounded by curly brackets. It supports recursive
434      * resolving for the content of the variable. The name of the variable
435      * is set by the termBuffer and the value is equal the content of the
436      * valueBuffer.
437      */
438     protected static class Variable extends Term {
439 
440         /**
441          * A variable doesn't return the termBuffer, but
442          * this valueBuffer.
443          */
444         private StringBuffer valueBuffer;
445 
446         private boolean complete;
447 
448         public Variable(MCRTextResolver textResolver) {
449             super(textResolver);
450             valueBuffer = new StringBuffer();
451             complete = false;
452         }
453 
454         @Override
455         public boolean resolveInternal(String text, int pos) {
456             if (text.startsWith(getEndEnclosingString(), pos)) {
457                 this.track();
458                 // get the value from the variables table
459                 String value = getTextResolver().getValue(termBuffer.toString());
460                 if (value == null) {
461                     resolved = false;
462                     if (getTextResolver().isRetainText()) {
463                         this.valueBuffer.append(getStartEnclosingString()).append(termBuffer)
464                             .append(getEndEnclosingString());
465                     }
466                     this.untrack();
467                     complete = true;
468                     return true;
469                 }
470                 // resolve the content of the variable recursive
471                 // to resolve all other internal variables, condition etc.
472                 if (getTextResolver().getResolveDepth() != ResolveDepth.NoVariables) {
473                     Text recursiveResolvedText = resolveText(value);
474                     resolved = recursiveResolvedText.resolved;
475                     value = recursiveResolvedText.getValue();
476                 }
477                 // set the value of the variable
478                 valueBuffer.append(value);
479                 this.untrack();
480                 complete = true;
481                 return true;
482             }
483             termBuffer.append(text.charAt(pos));
484             return false;
485         }
486 
487         @Override
488         public String getValue() {
489             if (!complete) {
490                 // assume that the variable is not complete 
491                 return getStartEnclosingString() + termBuffer;
492             }
493             return valueBuffer.toString();
494         }
495 
496         @Override
497         public String getStartEnclosingString() {
498             return "{";
499         }
500 
501         @Override
502         public String getEndEnclosingString() {
503             return "}";
504         }
505 
506         /**
507          * Tracks the variable to check for circular dependency.
508          */
509         protected void track() {
510             this.getTextResolver().getTracker().track("var", getTrackID());
511         }
512 
513         protected void untrack() {
514             this.getTextResolver().getTracker().untrack("var", getTrackID());
515         }
516 
517         protected String getTrackID() {
518             return getStartEnclosingString() + termBuffer + getEndEnclosingString();
519         }
520 
521         /**
522          * This method resolves all variables in the text.
523          * The syntax is described at the head of the class.
524          * 
525          * @param text the string where the variables have to be
526          * resolved
527          * @return the resolved string
528          */
529         public Text resolveText(String text) {
530             Text textResolver = new Text(getTextResolver());
531             textResolver.resolve(text, 0);
532             return textResolver;
533         }
534 
535     }
536 
537     /**
538      * A condition is defined by squared brackets. All data which
539      * is set in these brackets is only used if the internal variables are
540      * not null and not empty. For example "[hello {lastName}]" is only resolved
541      * if the value of "lastName" is not null and not empty. Otherwise the whole
542      * content in the squared brackets are ignored.
543      */
544     protected static class Condition extends Term {
545 
546         public Condition(MCRTextResolver textResolver) {
547             super(textResolver);
548         }
549 
550         @Override
551         protected boolean resolveInternal(String text, int pos) {
552             if (text.startsWith(getEndEnclosingString(), pos)) {
553                 return true;
554             }
555             termBuffer.append(text.charAt(pos));
556             return false;
557         }
558 
559         @Override
560         public String getValue() {
561             if (resolved) {
562                 return super.getValue();
563             }
564             return "";
565         }
566 
567         @Override
568         public String getStartEnclosingString() {
569             return "[";
570         }
571 
572         @Override
573         public String getEndEnclosingString() {
574             return "]";
575         }
576     }
577 
578     /**
579      * As escape character the backslashed is used. Only the
580      * first character after the escape char is add to the term.
581      */
582     protected static class EscapeCharacter extends Term {
583 
584         public EscapeCharacter(MCRTextResolver textResolver) {
585             super(textResolver);
586         }
587 
588         @Override
589         public boolean resolveInternal(String text, int pos) {
590             return true;
591         }
592 
593         @Override
594         public String resolve(String text, int startPos) {
595             position = startPos;
596             char c = text.charAt(position);
597             termBuffer.append(c);
598             return termBuffer.toString();
599         }
600 
601         @Override
602         public String getStartEnclosingString() {
603             return "\\";
604         }
605 
606         @Override
607         public String getEndEnclosingString() {
608             return "";
609         }
610     }
611 
612     /**
613      * A simple text, every character is added to the term (except its
614      * a special one).
615      */
616     protected static class Text extends Term {
617         public Text(MCRTextResolver textResolver) {
618             super(textResolver);
619         }
620 
621         @Override
622         public boolean resolveInternal(String text, int pos) {
623             termBuffer.append(text.charAt(pos));
624             return false;
625         }
626 
627         @Override
628         public String getStartEnclosingString() {
629             return "";
630         }
631 
632         @Override
633         public String getEndEnclosingString() {
634             return "";
635         }
636     }
637 
638     /**
639      * Simple class to hold terms and instantiate them.
640      */
641     protected static class TermContainer {
642 
643         protected Map<String, Class<? extends Term>> termMap = new HashMap<>();
644 
645         protected MCRTextResolver textResolver;
646 
647         public TermContainer(MCRTextResolver textResolver) {
648             this.textResolver = textResolver;
649         }
650 
651         public Term instantiate(Class<? extends Term> termClass) throws InvocationTargetException,
652             NoSuchMethodException, InstantiationException, IllegalAccessException {
653             Constructor<? extends Term> c = termClass.getConstructor(MCRTextResolver.class);
654             return c.newInstance(this.textResolver);
655         }
656 
657         public void add(Class<? extends Term> termClass) throws InvocationTargetException, NoSuchMethodException,
658             InstantiationException, IllegalAccessException {
659             Term term = instantiate(termClass);
660             this.termMap.put(term.getStartEnclosingString(), termClass);
661         }
662 
663         public void remove(Class<? extends Term> termClass) throws InvocationTargetException, NoSuchMethodException,
664             InstantiationException, IllegalAccessException {
665             Term term = instantiate(termClass);
666             this.termMap.remove(term.getStartEnclosingString());
667         }
668 
669         public Set<Entry<String, Class<? extends Term>>> getTermSet() {
670             return this.termMap.entrySet();
671         }
672 
673     }
674 
675     protected static class CircularDependencyTracker {
676         protected MCRTextResolver textResolver;
677 
678         protected Map<String, List<String>> trackMap;
679 
680         public CircularDependencyTracker(MCRTextResolver textResolver) {
681             this.textResolver = textResolver;
682             this.trackMap = new HashMap<>();
683         }
684 
685         public void track(String type, String id) throws CircularDependencyExecption {
686             List<String> idList = trackMap.computeIfAbsent(type, k -> new ArrayList<>());
687             if (idList.contains(id)) {
688                 throw new CircularDependencyExecption(idList, id);
689             }
690             idList.add(id);
691         }
692 
693         public void untrack(String type, String id) {
694             List<String> idList = trackMap.get(type);
695             if (idList == null) {
696                 LOGGER.error("text resolver circular dependency tracking error: cannot get type {} of {}", type, id);
697                 return;
698             }
699             idList.remove(id);
700         }
701 
702         public void clear() {
703             this.trackMap.clear();
704         }
705 
706     }
707 
708     protected static class CircularDependencyExecption extends RuntimeException {
709 
710         private static final long serialVersionUID = -2448797538275144448L;
711 
712         private List<String> dependencyList;
713 
714         private String id;
715 
716         public CircularDependencyExecption(List<String> dependencyList, String id) {
717             this.dependencyList = dependencyList;
718             this.id = id;
719         }
720 
721         @Override
722         public String getMessage() {
723             StringBuilder msg = new StringBuilder("A circular dependency exception occurred");
724             msg.append("\n").append("circular path: ");
725             for (String dep : dependencyList) {
726                 msg.append(dep).append(" > ");
727             }
728             msg.append(id);
729             return msg.toString();
730         }
731 
732         public String getId() {
733             return id;
734         }
735 
736         public List<String> getDependencyList() {
737             return dependencyList;
738         }
739 
740     }
741 
742 }