001    /**
002     * Copyright 2005-2014 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     */
016    package edu.sampleu.krms.impl;
017    
018    import org.kuali.rice.core.api.resourceloader.GlobalResourceLoader;
019    import org.kuali.rice.core.api.uif.RemotableAttributeError;
020    import org.kuali.rice.krms.api.KrmsApiServiceLocator;
021    import org.kuali.rice.krms.api.repository.function.FunctionDefinition;
022    import org.kuali.rice.krms.api.repository.function.FunctionRepositoryService;
023    import org.kuali.rice.krms.api.repository.operator.CustomOperator;
024    import org.kuali.rice.krms.api.repository.type.KrmsTypeDefinition;
025    import org.kuali.rice.krms.api.repository.type.KrmsTypeRepositoryService;
026    import org.kuali.rice.krms.framework.engine.Function;
027    import org.kuali.rice.krms.framework.type.FunctionTypeService;
028    import org.kuali.rice.krms.impl.repository.FunctionBoService;
029    
030    import java.text.MessageFormat;
031    import java.util.ArrayList;
032    import java.util.Collection;
033    import java.util.Collections;
034    import java.util.List;
035    
036    /**
037     * An example CustomOperator to demonstrate this functionality in of the KRMS UI and engine.
038     *
039     * <p>This one also does the FunctionTypeService duties to produce the engine executable, though that responsibility
040     * could be extracted to a separate class if desired, though that would require more wiring and database configuration.
041     * </p>
042     *
043     * <p>This implementation cheats a bit for easy configuration by persisting the FunctionDefinition on first access.
044     * The only prerequisite configuration to use it: 1) add a KRMS type with namespace "KR-SAP", name "contains operator",
045     * and the serviceName "sampleAppContainsOperatorService".  2) add a type relation of type 'A' (usage allowed) from
046     * your KRMS type to the context type you want to use it with.</p>
047     *
048     * <p>This service implementation is wired up in Spring and exported to the service bus.</p>
049     *
050     * <!-- NOTE: all ID and FK columns in the example SQL below should be replaced appropriately, but you knew that. -->
051     *
052     * <!-- sample SQL for creating the KRMS type:
053     * insert into krms_typ_t (typ_id, nm, nmspc_cd, srvc_nm, actv, ver_nbr)
054     * values ('OPERATOR-KRMS-TYPE-ID', 'contains operator', 'KR-SAP', 'sampleAppContainsOperatorService', 'Y', 1);
055     * -->
056     *
057     * <!-- sample SQL for creating the type relation:
058     * insert into krms_typ_reln_t (typ_reln_id, from_typ_id, to_typ_id, reln_typ, seq_no)
059     * values ('A-UNIQUE-ID', 'CONTEXT-TYPE-ID', 'OPERATOR-KRMS-TYPE-ID', 'A', 1);
060     * -->
061     *
062     * @author Kuali Rice Team (rice.collab@kuali.org)
063     */
064    public class ContainsOperator implements CustomOperator, FunctionTypeService {
065    
066        /**
067         * Returns the FunctionDefinition for the custom function the executable portion of this CustomOperator will
068         * call.
069         *
070         * <p>If the FunctionDefinition hasn't been persisted yet, this method will persist it and then return it.</p>
071         *
072         * <p>Note that having the KRMS type for this </p>
073         *
074         * @return
075         */
076        @Override
077        public FunctionDefinition getOperatorFunctionDefinition() {
078            FunctionBoService functionBoService =
079                    (FunctionBoService) GlobalResourceLoader.getService("functionRepositoryService");
080    
081            KrmsTypeRepositoryService typeRepository = KrmsApiServiceLocator.getKrmsTypeRepositoryService();
082            KrmsTypeDefinition containsOperatorType = typeRepository.getTypeByName("KR-SAP", "contains operator");
083    
084            if (containsOperatorType == null) {
085                throw new IllegalStateException("There must be a persisted KRMS type with namespace 'KR-SAP', "
086                        + "name 'contains operator', and serviceName 'sampleAppContainsOperatorService'");
087            }
088    
089            FunctionDefinition containsFunction = functionBoService.getFunctionByNameAndNamespace("contains", "KR-SAP");
090    
091            if (containsFunction == null) {
092                FunctionDefinition.Builder functionBuilder =
093                        FunctionDefinition.Builder.create("KR-SAP", "contains", "java.lang.Boolean", containsOperatorType.getId());
094    
095                containsFunction = functionBoService.createFunction(functionBuilder.build());
096            }
097    
098            return containsFunction;
099        }
100    
101        /**
102         * Validate the argument types.  This operator supports (String, String), and (Collection, Object).  For other
103         * classes, errors will be produced.  Note that at the current time (Rice 2.3.1), the rhsClassName will always be
104         * "java.lang.String" since the UI is constrained to set the second arg as a constant value.
105         *
106         * @param lhsClassName the class name for the left hand side operand
107         * @param rhsClassName the class name for the right hand side operand
108         * @return errors
109         */
110        @Override
111        public List<RemotableAttributeError> validateOperandClasses(String lhsClassName, String rhsClassName) {
112            List<RemotableAttributeError> errors = new ArrayList<RemotableAttributeError>();
113    
114            if (String.class.getName().equals(lhsClassName)) {
115                if (!String.class.getName().equals(rhsClassName)) {
116                    RemotableAttributeError.Builder errorBuilder =
117                            RemotableAttributeError.Builder.create("operator", "right hand operand is not a String");
118                    errors.add(errorBuilder.build());
119                }
120            } else {
121                try {
122                    Class<?> clazz = getClass().getClassLoader().loadClass(lhsClassName);
123    
124                    if (!Collection.class.isAssignableFrom(clazz)) {
125                        RemotableAttributeError.Builder errorBuilder =
126                                RemotableAttributeError.Builder.create("operator", "left hand operand is not a Collection");
127                        errors.add(errorBuilder.build());
128                    }
129                } catch (ClassNotFoundException e) {
130                    // it is not a class available to the class loader so we can't determine if it extends Collection or not
131                }
132            }
133    
134            return errors;
135        }
136    
137        /**
138         * Loads the Function object that the KRMS engine can execute during rule evaluation
139         *
140         * @param functionDefinition {@link FunctionDefinition} to create the {@link Function} from.
141         * @return
142         */
143        @Override
144        public Function loadFunction(FunctionDefinition functionDefinition) {
145            if (!"contains".equals(functionDefinition.getName()) || "KR-SAP".equals(functionDefinition.getNamespace())) {
146                throw new IllegalArgumentException("oops, you have the wrong type service, I can't load this function");
147            }
148    
149            // return our KRMS engine executable operator function:
150            return new Function() {
151                @Override
152                public Object invoke(Object... arguments) {
153                    if (arguments.length != 2) {
154                        throw new IllegalArgumentException("contains operator expects 2 arguments");
155                    }
156    
157                    if (arguments[0] instanceof String) {
158                        if (arguments[1] instanceof String) {
159                            return Boolean.valueOf(((String)arguments[0]).contains((String)arguments[1]));
160                        } else {
161                            throw new IllegalArgumentException("if the first argument is a String, the second argument "
162                                    + "must be as well");
163                        }
164                    } else if (arguments[0] instanceof Collection) {
165                        return Boolean.valueOf(((Collection)arguments[0]).contains(arguments[1]));
166                    }
167    
168                    throw new IllegalArgumentException("argument types must be either (String, String) or (Collection, Object)");
169                }
170            };
171        }
172    }