001/**
002 * Copyright 2005-2017 The Kuali Foundation
003 *
004 * Licensed under the Educational Community License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 * http://www.opensource.org/licenses/ecl2.php
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016package org.kuali.rice.krms.impl.provider.repository;
017
018import org.apache.commons.collections.CollectionUtils;
019import org.apache.commons.lang.StringUtils;
020import org.kuali.rice.krms.api.engine.Term;
021import org.kuali.rice.krms.api.engine.expression.ComparisonOperatorService;
022import org.kuali.rice.krms.api.repository.RepositoryDataException;
023import org.kuali.rice.krms.api.repository.function.FunctionDefinition;
024import org.kuali.rice.krms.api.repository.function.FunctionParameterDefinition;
025import org.kuali.rice.krms.api.repository.function.FunctionRepositoryService;
026import org.kuali.rice.krms.api.repository.proposition.PropositionDefinition;
027import org.kuali.rice.krms.api.repository.proposition.PropositionParameter;
028import org.kuali.rice.krms.api.repository.proposition.PropositionParameterType;
029import org.kuali.rice.krms.api.repository.term.TermDefinition;
030import org.kuali.rice.krms.api.repository.term.TermParameterDefinition;
031import org.kuali.rice.krms.api.repository.term.TermRepositoryService;
032import org.kuali.rice.krms.api.repository.term.TermSpecificationDefinition;
033import org.kuali.rice.krms.framework.engine.Function;
034import org.kuali.rice.krms.framework.engine.Proposition;
035import org.kuali.rice.krms.framework.engine.expression.BinaryOperatorExpression;
036import org.kuali.rice.krms.framework.engine.expression.BooleanValidatingExpression;
037import org.kuali.rice.krms.framework.engine.expression.ComparisonOperator;
038import org.kuali.rice.krms.framework.engine.expression.ConstantExpression;
039import org.kuali.rice.krms.framework.engine.expression.Expression;
040import org.kuali.rice.krms.framework.engine.expression.ExpressionBasedProposition;
041import org.kuali.rice.krms.framework.engine.expression.FunctionExpression;
042import org.kuali.rice.krms.framework.engine.expression.TermExpression;
043import org.kuali.rice.krms.framework.type.FunctionTypeService;
044import org.kuali.rice.krms.framework.type.PropositionTypeService;
045import org.kuali.rice.krms.impl.type.KrmsTypeResolver;
046
047import java.util.ArrayList;
048import java.util.LinkedList;
049import java.util.List;
050import java.util.Map;
051import java.util.TreeMap;
052
053/**
054 * A default implementation of {@link PropositionTypeService} for propositions
055 * which are composed of terms, operators, and functions.  A simple proposition
056 * is self-contained and has no compound "sub" propositions.  However, it's
057 * behavior is defined by the set of parameters on the {@link PropositionDefinition}.
058 * 
059 * @author Kuali Rice Team (rice.collab@kuali.org)
060 *
061 */
062public class SimplePropositionTypeService implements PropositionTypeService {
063
064    private FunctionRepositoryService functionRepositoryService;
065    private TermRepositoryService termRepositoryService;
066    private KrmsTypeResolver typeResolver;
067    private ComparisonOperatorService comparisonOperatorService;
068        
069        @Override
070        public Proposition loadProposition(PropositionDefinition propositionDefinition) {
071                return new ExpressionBasedProposition(translateToExpression(propositionDefinition));
072        }
073
074        /**
075         * Translates the parameters on the given proposition definition to create an expression for evaluation.
076         * The proposition parameters are defined in a reverse-polish notation so a stack is used for
077         * evaluation purposes.
078         * 
079         * @param propositionDefinition the proposition definition to translate
080         * 
081         * @return the translated expression for the given proposition, this
082         * expression, when evaluated, will return a Boolean.
083         */
084        protected Expression<Boolean> translateToExpression(PropositionDefinition propositionDefinition) {
085                LinkedList<Expression<? extends Object>> stack = new LinkedList<Expression<? extends Object>>();
086                for (PropositionParameter parameter : propositionDefinition.getParameters()) {
087                        PropositionParameterType parameterType = PropositionParameterType.fromCode(parameter.getParameterType());
088                        if (parameterType == PropositionParameterType.CONSTANT) {
089                                // TODO - need some way to define data type on the prop parameter as well?  Not all constants will actually be String values!!!
090                                stack.addFirst(new ConstantExpression<String>(parameter.getValue()));
091                        } else if (parameterType == PropositionParameterType.FUNCTION) {
092                                String functionId = parameter.getValue();
093                                FunctionDefinition functionDefinition = functionRepositoryService.getFunction(functionId);
094                                if (functionDefinition == null) {
095                                        throw new RepositoryDataException("Unable to locate function with the given id: " + functionId);
096                                }
097                                FunctionTypeService functionTypeService = typeResolver.getFunctionTypeService(functionDefinition);
098                                Function function = functionTypeService.loadFunction(functionDefinition);
099                                // TODO throw an exception if function is null?
100                                List<FunctionParameterDefinition> parameters = functionDefinition.getParameters();
101                                if (stack.size() < parameters.size()) {
102                                        throw new RepositoryDataException("Failed to initialize custom function '" + functionDefinition.getNamespace() + " " + functionDefinition.getName() +
103                                                        "'.  There were only " + stack.size() + " values on the stack but function requires at least " + parameters.size());
104                                }
105                                List<Expression<? extends Object>> arguments = new ArrayList<Expression<? extends Object>>();
106                                // work backward through the list to match params to the stack
107                                for (int index = parameters.size() - 1; index >= 0; index--) {
108                                        FunctionParameterDefinition parameterDefinition = parameters.get(index);
109                                        // TODO need to check types here? expression object probably needs a getType on it so that we can confirm that the types will be compatible?
110                    parameterDefinition.getParameterType();
111                                        Expression<? extends Object> argument = stack.removeFirst();
112                                        arguments.add(argument);
113                                }
114
115                String[] parameterTypes = getFunctionParameterTypes(functionDefinition);
116                                stack.addFirst(new FunctionExpression(function, parameterTypes, arguments, getComparisonOperatorService()));
117
118                        } else if (parameterType == PropositionParameterType.OPERATOR) {
119                                ComparisonOperator operator = ComparisonOperator.fromCode(parameter.getValue());
120                                if (stack.size() < 2) {
121                                        throw new RepositoryDataException("Failed to initialize expression for comparison operator " +
122                            operator + " because a sufficient number of arguments was not available on the stack.  "
123                            + "Current contents of stack: " + stack.toString());
124                                }
125                                Expression<? extends Object> rhs = stack.removeFirst();
126                                Expression<? extends Object> lhs = stack.removeFirst();
127                                stack.addFirst(new BinaryOperatorExpression(operator, lhs, rhs));
128                        } else if (parameterType == PropositionParameterType.TERM) {
129                                String termId = parameter.getValue();
130
131                                TermDefinition termDefinition = getTermRepositoryService().getTerm(termId);
132                                if (termDefinition == null) { throw new RepositoryDataException("unable to load term with id " + termId);}
133                                Term term = translateTermDefinition(termDefinition);
134                                
135                                stack.addFirst(new TermExpression(term));
136                        }
137                }
138                if (stack.size() != 1) {
139                        throw new RepositoryDataException("Final contents of expression stack are incorrect, there should only be one entry but was " + stack.size() +".  Current contents of stack: " + stack.toString());
140                }
141                return new BooleanValidatingExpression(stack.removeFirst());
142        }
143
144    private String[] getFunctionParameterTypes(FunctionDefinition functionDefinition) {
145        String [] argumentTypes = null;
146        List<FunctionParameterDefinition> functionParameters = functionDefinition.getParameters();
147        if (!CollectionUtils.isEmpty(functionParameters)) {
148            argumentTypes = new String[functionParameters.size()];
149
150            int argTypesIndex = 0;
151            for (FunctionParameterDefinition functionParameter : functionParameters) {
152                argumentTypes[argTypesIndex] = functionParameter.getParameterType();
153                argTypesIndex += 1;
154            }
155        }
156        return argumentTypes;
157    }
158
159    protected Term translateTermDefinition(TermDefinition termDefinition) {
160                if (termDefinition == null) {
161                        throw new RepositoryDataException("Given TermDefinition is null");
162                }
163                TermSpecificationDefinition termSpecificationDefinition = termDefinition.getSpecification();
164                if (termSpecificationDefinition == null) { throw new RepositoryDataException("term with id " + termDefinition.getId() + " has a null specification"); } 
165                
166                List<TermParameterDefinition> params = termDefinition.getParameters();
167                Map<String,String> paramsMap = new TreeMap<String,String>();
168                if (!CollectionUtils.isEmpty(params)) for (TermParameterDefinition param : params) {
169                        if (StringUtils.isBlank(param.getName())) { 
170                                throw new RepositoryDataException("TermParameterDefinition.name may not be blank"); 
171                        }
172                        paramsMap.put(param.getName(), param.getValue());
173                }
174                
175                return new Term(termSpecificationDefinition.getName(), paramsMap);
176        }
177
178    public void setFunctionRepositoryService(FunctionRepositoryService functionRepositoryService) {
179                this.functionRepositoryService = functionRepositoryService;
180        }
181        
182        public void setTypeResolver(KrmsTypeResolver typeResolver) {
183                this.typeResolver = typeResolver;
184        }
185
186    public ComparisonOperatorService getComparisonOperatorService() {
187        return comparisonOperatorService;
188    }
189
190    public void setComparisonOperatorService(ComparisonOperatorService comparisonOperatorService) {
191        this.comparisonOperatorService = comparisonOperatorService;
192    }
193
194    public TermRepositoryService getTermRepositoryService() {
195        return termRepositoryService;
196    }
197
198    public void setTermRepositoryService(TermRepositoryService termRepositoryService) {
199        this.termRepositoryService = termRepositoryService;
200    }
201}