001    /**
002     * Copyright 2010 The Kuali Foundation Licensed under the
003     * Educational Community License, Version 2.0 (the "License"); you may
004     * not use this file except in compliance with the License. You may
005     * obtain a copy of the License at
006     *
007     * http://www.osedu.org/licenses/ECL-2.0
008     *
009     * Unless required by applicable law or agreed to in writing,
010     * software distributed under the License is distributed on an "AS IS"
011     * BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
012     * or implied. See the License for the specific language governing
013     * permissions and limitations under the License.
014     */
015    
016    package org.kuali.student.common.messagebuilder.booleanmessage.ast;
017    
018    import java.util.ArrayList;
019    import java.util.List;
020    import java.util.Locale;
021    import java.util.Map;
022    
023    import org.antlr.runtime.ANTLRStringStream;
024    import org.antlr.runtime.CommonTokenStream;
025    import org.antlr.runtime.RecognitionException;
026    import org.antlr.runtime.Token;
027    import org.antlr.runtime.tree.CommonTreeAdaptor;
028    import org.antlr.runtime.tree.TreeAdaptor;
029    
030    import org.kuali.student.common.messagebuilder.booleanmessage.BooleanMessage;
031    import org.kuali.student.common.messagebuilder.booleanmessage.ast.exceptions.BooleanFunctionException;
032    import org.kuali.student.common.messagebuilder.booleanmessage.ast.parsers.BooleanFunctionLexer;
033    import org.kuali.student.common.messagebuilder.booleanmessage.ast.parsers.BooleanFunctionParser;
034    
035    import org.slf4j.Logger;
036    import org.slf4j.LoggerFactory;
037    
038    public class BinaryMessageTree {
039        /** SLF4J logging framework */
040        final static Logger logger = LoggerFactory.getLogger(BinaryMessageTree.class);
041    
042        private BooleanNode root;
043            private ArrayList<BooleanNode> nodes;
044            private String language;
045            
046            private Map<String, ? extends BooleanMessage> nodeMessageMap;
047            
048            private static final TreeAdaptor adaptor = new CommonTreeAdaptor() {
049                    public Object create(Token payload) {
050                            return new BooleanNode(payload);
051                    }
052            };
053            
054            /**
055             * Constructor.
056             */
057            public BinaryMessageTree() {
058                    language = Locale.getDefault().getLanguage();
059                    nodes = new ArrayList<BooleanNode>();
060            }
061    
062            /**
063             * Constructor. 
064             * Creates a new binary message tree for the in the specified language
065             * for the boolean message map.
066             *  
067             * @param messageLanguage Language for the message 
068             * @param messageMap map of boolean messages
069             */
070            public BinaryMessageTree(String language, Map<String, ? extends BooleanMessage> messageMap) {
071                    this.language = language;
072                    nodeMessageMap = messageMap;
073                    nodes = new ArrayList<BooleanNode>();
074            }
075            
076            /**
077             * <p>Builds the binary message tree from the <code>booleanFunction</code>.</p>
078             * <p>Order of boolean operation: ANDs before ORs and 
079             * operations inside parentheses before anything else.</p>
080             * <p>Example: A AND B OR C AND D evaluates to (A AND B) OR (C AND D).</p>
081             * 
082             * @param booleanFunction: Boolean expression 
083             * @return Root boolean node
084             */
085            public BooleanNode buildTree(String booleanFunction){
086                    if (booleanFunction == null || booleanFunction.trim().isEmpty()) {
087                            return null;
088                    }
089                    
090                    BooleanFunctionLexer lex = new BooleanFunctionLexer(new ANTLRStringStream(booleanFunction) );
091                    CommonTokenStream tokens = new CommonTokenStream(lex);
092                    
093                    BooleanFunctionParser parser = new BooleanFunctionParser(tokens);
094                    parser.setTreeAdaptor(adaptor);
095                    
096                    // parse the expression
097                    BooleanFunctionParser.booleanExpression_return booleanExpression = null;
098            try {
099                    booleanExpression = parser.booleanExpression();  
100            } catch (RecognitionException e)  {
101                    String parserErrorMsg = parser.getErrorMessage(e, parser.getTokenNames());
102                    String errorMsg = "Boolean Function Parser Error. " +
103                                    "Invalid Boolean Expression: '" + booleanFunction + "'; " + parserErrorMsg;  
104                    throw new BooleanFunctionException(errorMsg, e);
105            }
106            // get the root of the AST from the parsed expression
107            return root = (BooleanNode) booleanExpression.getTree();
108            }
109            
110            /** 
111             * This method walks the tree depth first, while setting node values with 
112             * setNode(). Setting a nodes parent and adding the node to the ArrayList 
113             * for later. This method has to be called after buildTree. 
114             * 
115             * @param bnode Boolean node
116             * @param parent Parent boolean node
117             */
118            public void traverseTreePostOrder(BooleanNode bnode, BooleanNode parent) {
119                    for (int i = 0; i < bnode.getChildCount(); i++) {
120                            traverseTreePostOrder((BooleanNode) bnode.getChild(i), bnode);
121                    }
122                    setNode(bnode);
123                    if (parent != null) {
124                            bnode.setParent(parent);
125                    }
126    
127                    if(logger.isDebugEnabled()) {
128                        logger.debug(bnode.getText());
129                    }
130                    nodes.add(bnode);
131            }
132    
133            /**
134             * This method walks the tree depth first. Setting a nodes parent and 
135             * adding the node to the ArrayList for later. 
136             * This method has to be called after buildTree. 
137             * 
138             * @param bnode Boolean node
139             * @param parent Parent boolean node
140             */
141            public void traverseTreePostOrderDontSetNode(BooleanNode bnode, BooleanNode parent) {
142                    if (bnode != null) {
143                            for (int i = 0; i < bnode.getChildCount(); i++ ) {
144                                    traverseTreePostOrderDontSetNode((BooleanNode)bnode.getChild(i), bnode);
145                            }
146    
147                            if (parent != null) {
148                                    bnode.setParent(parent);
149                            }
150                        
151                            if(logger.isDebugEnabled()) {
152                                    logger.debug(bnode.getText());
153                            }
154                            nodes.add(bnode);
155                    }
156            }
157    
158            /** 
159             * Here we set the value(true or false) and the failure message for each node. 
160             * Setting the failure message here does not mean the function failed, the node just holds the message. 
161             * Failure is determined by the rules engine which will extract the failure message.
162             * 
163             * @param bnode Boolean node
164             */
165            private void setNode(BooleanNode bnode) {
166                    bnode.setLanguage(language);
167                    
168                    if (bnode.getChildCount() == 0) {
169                            // If node is a leaf then set value and message
170                            BooleanMessage message = nodeMessageMap.get(bnode.getLabel());
171                            bnode.setValue(message.isSuccesful());
172                            bnode.setNodeMessage(message.getMessage());
173                    }
174                    else {
175                            // If node is intermediate, compute value and set it
176                            BooleanNode child0 = (BooleanNode)bnode.getChild(0);
177                            BooleanNode child1 = (BooleanNode)bnode.getChild(1);
178                            
179                            if ( bnode.getLabel().equalsIgnoreCase("+") ) {
180                                    // OR node
181                                    Boolean newValue = child0.getValue() || child1.getValue();
182                                    bnode.setValue(newValue);
183                                    bnode.setNodeMessage("null");
184                            }
185                            else if ( bnode.getLabel().equalsIgnoreCase("*") ) {
186                                    // AND node
187                                    Boolean newValue = child0.getValue() && child1.getValue();
188                                    bnode.setValue(newValue);
189                                    bnode.setNodeMessage("null");
190                            }
191                    }
192            }
193            
194            /**
195             * Gets all boolean nodes.
196             * 
197             * @return All boolean nodes
198             */
199            public List<BooleanNode> getAllNodes() {
200                    return nodes;
201            }
202    
203            /**
204             * Gets the root boolean node.
205             * 
206             * @return Root boolean node
207             */
208            public BooleanNode getRoot() {
209                    return root;
210            }
211    }