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 org.kuali.rice.krms.impl.repository;
017    
018    import org.apache.commons.lang.StringUtils;
019    import org.kuali.rice.core.api.mo.common.Versioned;
020    import org.kuali.rice.core.api.util.tree.Node;
021    import org.kuali.rice.core.api.util.tree.Tree;
022    import org.kuali.rice.krad.data.DataObjectService;
023    import org.kuali.rice.krad.data.jpa.PortableSequenceGenerator;
024    import org.kuali.rice.krad.data.jpa.converters.BooleanYNConverter;
025    import org.kuali.rice.krad.service.KRADServiceLocator;
026    import org.kuali.rice.krms.api.repository.LogicalOperator;
027    import org.kuali.rice.krms.api.repository.action.ActionDefinition;
028    import org.kuali.rice.krms.api.repository.proposition.PropositionType;
029    import org.kuali.rice.krms.api.repository.rule.RuleDefinition;
030    import org.kuali.rice.krms.api.repository.rule.RuleDefinitionContract;
031    import org.kuali.rice.krms.api.repository.type.KrmsAttributeDefinition;
032    import org.kuali.rice.krms.impl.ui.CompoundOpCodeNode;
033    import org.kuali.rice.krms.impl.ui.CompoundPropositionEditNode;
034    import org.kuali.rice.krms.impl.ui.RuleTreeNode;
035    import org.kuali.rice.krms.impl.ui.SimplePropositionEditNode;
036    import org.kuali.rice.krms.impl.ui.SimplePropositionNode;
037    
038    import javax.persistence.CascadeType;
039    import javax.persistence.Column;
040    import javax.persistence.Convert;
041    import javax.persistence.Entity;
042    import javax.persistence.FetchType;
043    import javax.persistence.GeneratedValue;
044    import javax.persistence.Id;
045    import javax.persistence.JoinColumn;
046    import javax.persistence.ManyToOne;
047    import javax.persistence.OneToMany;
048    import javax.persistence.Table;
049    import javax.persistence.Transient;
050    import javax.persistence.Version;
051    import java.io.IOException;
052    import java.io.ObjectOutputStream;
053    import java.io.Serializable;
054    import java.util.ArrayList;
055    import java.util.HashMap;
056    import java.util.List;
057    import java.util.Map;
058    
059    @Entity
060    @Table(name = "KRMS_RULE_T")
061    public class RuleBo implements RuleDefinitionContract, Versioned, Serializable {
062    
063        private static final long serialVersionUID = 1L;
064    
065        public static final String RULE_SEQ_NAME = "KRMS_RULE_S";
066        static final RepositoryBoIncrementer ruleIdIncrementer = new RepositoryBoIncrementer(RULE_SEQ_NAME);
067        static final RepositoryBoIncrementer actionIdIncrementer = new RepositoryBoIncrementer("KRMS_ACTN_S");
068        static final RepositoryBoIncrementer ruleAttributeIdIncrementer = new RepositoryBoIncrementer("KRMS_RULE_ATTR_S");
069        static final RepositoryBoIncrementer actionAttributeIdIncrementer = new RepositoryBoIncrementer("KRMS_ACTN_ATTR_S");
070    
071        @PortableSequenceGenerator(name = RULE_SEQ_NAME)
072        @GeneratedValue(generator = RULE_SEQ_NAME)
073        @Id
074        @Column(name = "RULE_ID")
075        private String id;
076    
077        @Column(name = "NMSPC_CD")
078        private String namespace;
079    
080        @Column(name = "DESC_TXT")
081        private String description;
082    
083        @Column(name = "NM")
084        private String name;
085    
086        @Column(name = "TYP_ID", nullable = true)
087        private String typeId;
088    
089        @Column(name = "ACTV")
090        @Convert(converter = BooleanYNConverter.class)
091        private boolean active = true;
092    
093        @Column(name = "VER_NBR")
094        @Version
095        private Long versionNumber;
096    
097        @ManyToOne(targetEntity = PropositionBo.class, fetch = FetchType.LAZY, cascade = { CascadeType.REFRESH, CascadeType.REMOVE, CascadeType.PERSIST })
098        @JoinColumn(name = "PROP_ID", referencedColumnName = "PROP_ID", insertable = true, updatable = true)
099        private PropositionBo proposition;
100    
101        @OneToMany(mappedBy = "rule",
102                cascade = { CascadeType.MERGE, CascadeType.REMOVE, CascadeType.PERSIST })
103        private List<ActionBo> actions;
104    
105        @OneToMany(targetEntity = RuleAttributeBo.class, fetch = FetchType.LAZY, orphanRemoval = true, cascade = { CascadeType.REFRESH, CascadeType.REMOVE, CascadeType.PERSIST })
106        @JoinColumn(name = "RULE_ID", referencedColumnName = "RULE_ID", insertable = true, updatable = true)
107        private List<RuleAttributeBo> attributeBos;
108    
109        @Transient
110        private Tree<RuleTreeNode, String> propositionTree;
111    
112        @Transient
113        private String propositionSummary;
114    
115        @Transient
116        private StringBuffer propositionSummaryBuffer;
117    
118        @Transient
119        private String selectedPropositionId;
120    
121        public RuleBo() {
122            actions = new ArrayList<ActionBo>();
123            attributeBos = new ArrayList<RuleAttributeBo>();
124        }
125    
126        public PropositionBo getProposition() {
127            return proposition;
128        }
129    
130        public void setProposition(PropositionBo proposition) {
131            this.proposition = proposition;
132        }
133    
134        /**
135         * set the typeId.  If the parameter is blank, then this RuleBo's
136         * typeId will be set to null
137         *
138         * @param typeId
139         */
140        public void setTypeId(String typeId) {
141            if (StringUtils.isBlank(typeId)) {
142                this.typeId = null;
143            } else {
144                this.typeId = typeId;
145            }
146        }
147    
148        public Map<String, String> getAttributes() {
149            HashMap<String, String> attributes = new HashMap<String, String>();
150    
151            for (RuleAttributeBo attr : attributeBos) {
152                DataObjectService dataObjectService = KRADServiceLocator.getDataObjectService();
153                dataObjectService.wrap(attr).fetchRelationship("attributeDefinition", false, true);
154                attributes.put(attr.getAttributeDefinition().getName(), attr.getValue());
155            }
156    
157            return attributes;
158        }
159    
160        public void setAttributes(Map<String, String> attributes) {
161            this.attributeBos = new ArrayList<RuleAttributeBo>();
162    
163            if (!StringUtils.isBlank(this.typeId)) {
164                List<KrmsAttributeDefinition> attributeDefinitions = KrmsRepositoryServiceLocator.getKrmsAttributeDefinitionService().findAttributeDefinitionsByType(this.getTypeId());
165                Map<String, KrmsAttributeDefinition> attributeDefinitionsByName = new HashMap<String, KrmsAttributeDefinition>();
166    
167                if (attributeDefinitions != null) {
168                    for (KrmsAttributeDefinition attributeDefinition : attributeDefinitions) {
169                        attributeDefinitionsByName.put(attributeDefinition.getName(), attributeDefinition);
170                    }
171                }
172    
173                for (Map.Entry<String, String> attr : attributes.entrySet()) {
174                    KrmsAttributeDefinition attributeDefinition = attributeDefinitionsByName.get(attr.getKey());
175                    RuleAttributeBo attributeBo = new RuleAttributeBo();
176                    attributeBo.setRuleId(this.getId());
177                    attributeBo.setValue(attr.getValue());
178                    attributeBo.setAttributeDefinition(KrmsAttributeDefinitionBo.from(attributeDefinition));
179                    attributeBos.add(attributeBo);
180                }
181            }
182        }
183    
184        public String getPropositionSummary() {
185            if (this.propositionTree == null) {
186                this.propositionTree = refreshPropositionTree(false);
187            }
188    
189            return propositionSummaryBuffer.toString();
190        }
191    
192        /**
193         * This method is used by the RuleEditor to display the proposition in tree form.
194         *
195         * @return Tree representing a rule proposition.
196         */
197        public Tree getPropositionTree() {
198            if (this.propositionTree == null) {
199                this.propositionTree = refreshPropositionTree(false);
200            }
201    
202            return this.propositionTree;
203        }
204    
205        public void setPropositionTree(Tree<RuleTreeNode, String> tree) {
206            this.propositionTree.equals(tree);
207        }
208    
209        public Tree refreshPropositionTree(Boolean editMode) {
210            Tree myTree = new Tree<RuleTreeNode, String>();
211    
212            Node<RuleTreeNode, String> rootNode = new Node<RuleTreeNode, String>();
213            myTree.setRootElement(rootNode);
214    
215            propositionSummaryBuffer = new StringBuffer();
216            PropositionBo prop = this.getProposition();
217            if (prop!=null && StringUtils.isBlank(prop.getDescription())) {
218                prop.setDescription("");
219            }
220            buildPropTree(rootNode, prop, editMode);
221            this.propositionTree = myTree;
222            return myTree;
223        }
224    
225        /**
226         * This method builds a propositionTree recursively walking through the children of the proposition.
227         *
228         * @param sprout - parent tree node
229         * @param prop - PropositionBo for which to make the tree node
230         * @param editMode - Boolean determines the node type used to represent the proposition
231         * false: create a view only node text control
232         * true: create an editable node with multiple controls
233         * null:  use the proposition.editMode property to determine the node type
234         */
235        private void buildPropTree(Node sprout, PropositionBo prop, Boolean editMode) {
236            // Depending on the type of proposition (simple/compound), and the editMode,  
237            // Create a treeNode of the appropriate type for the node and attach it to the  
238            // sprout parameter passed in.  
239            // If the prop is a compound proposition, calls itself for each of the compoundComponents  
240            if (prop != null) {
241                if (PropositionType.SIMPLE.getCode().equalsIgnoreCase(prop.getPropositionTypeCode())) {
242                    // Simple Proposition  
243                    // add a node for the description display with a child proposition node  
244                    Node<RuleTreeNode, String> child = new Node<RuleTreeNode, String>();
245                    child.setNodeLabel(prop.getDescription());
246    
247                    if (prop.getEditMode()) {
248                        child.setNodeLabel("");
249                        child.setNodeType(SimplePropositionEditNode.NODE_TYPE);
250                        SimplePropositionEditNode pNode = new SimplePropositionEditNode(prop);
251                        child.setData(pNode);
252                    } else {
253                        child.setNodeType(SimplePropositionNode.NODE_TYPE);
254                        SimplePropositionNode pNode = new SimplePropositionNode(prop);
255                        child.setData(pNode);
256                    }
257    
258                    sprout.getChildren().add(child);
259                    propositionSummaryBuffer.append(prop.getParameterDisplayString());
260                } else if (PropositionType.COMPOUND.getCode().equalsIgnoreCase(prop.getPropositionTypeCode())) {
261                    // Compound Proposition  
262                    propositionSummaryBuffer.append(" ( ");
263                    Node<RuleTreeNode, String> aNode = new Node<RuleTreeNode, String>();
264                    aNode.setNodeLabel(prop.getDescription());
265    
266                    // editMode has description as an editable field  
267                    if (prop.getEditMode()) {
268                        aNode.setNodeLabel("");
269                        aNode.setNodeType("ruleTreeNode compoundNode editNode");
270                        CompoundPropositionEditNode pNode = new CompoundPropositionEditNode(prop);
271                        aNode.setData(pNode);
272                    } else {
273                        aNode.setNodeType("ruleTreeNode compoundNode");
274                        RuleTreeNode pNode = new RuleTreeNode(prop);
275                        aNode.setData(pNode);
276                    }
277    
278                    sprout.getChildren().add(aNode);
279                    boolean first = true;
280                    List<PropositionBo> allMyChildren = prop.getCompoundComponents();
281                    int compoundSequenceNumber = 0;
282    
283                    for (PropositionBo child : allMyChildren) {
284                        child.setCompoundSequenceNumber((compoundSequenceNumber = ++compoundSequenceNumber));
285    
286                        // start with 1
287                        // add an opcode node in between each of the children.  
288                        if (!first) {
289                            addOpCodeNode(aNode, prop);
290                        }
291    
292                        first = false;
293                        // call to build the childs node  
294                        buildPropTree(aNode, child, editMode);
295                    }
296    
297                    propositionSummaryBuffer.append(" ) ");
298                }
299            }
300        }
301    
302        /**
303         * This method adds an opCode Node to separate components in a compound proposition.
304         *
305         * @param currentNode
306         * @param prop
307         * @return
308         */
309        private void addOpCodeNode(Node currentNode, PropositionBo prop) {
310            String opCodeLabel = "";
311    
312            if (LogicalOperator.AND.getCode().equalsIgnoreCase(prop.getCompoundOpCode())) {
313                opCodeLabel = "AND";
314            } else if (LogicalOperator.OR.getCode().equalsIgnoreCase(prop.getCompoundOpCode())) {
315                opCodeLabel = "OR";
316            }
317    
318            propositionSummaryBuffer.append(" " + opCodeLabel + " ");
319            Node<RuleTreeNode, String> aNode = new Node<RuleTreeNode, String>();
320            aNode.setNodeLabel("");
321            aNode.setNodeType("ruleTreeNode compoundOpCodeNode");
322            aNode.setData(new CompoundOpCodeNode(prop));
323    
324            currentNode.getChildren().add(aNode);
325        }
326    
327        /**
328         * Converts a mutable bo to it's immutable counterpart
329         *
330         * @param bo the mutable business object
331         * @return the immutable object
332         */
333        public static RuleDefinition to(RuleBo bo) {
334            if (bo == null) {
335                return null;
336            }
337    
338            return RuleDefinition.Builder.create(bo).build();
339        }
340    
341        /**
342         * Converts a immutable object to it's mutable bo counterpart
343         *
344         * @param im immutable object
345         * @return the mutable bo
346         */
347        public static RuleBo from(RuleDefinition im) {
348            if (im == null) {
349                return null;
350            }
351    
352            RuleBo bo = new RuleBo();
353            bo.id = im.getId();
354            bo.namespace = im.getNamespace();
355            bo.name = im.getName();
356            bo.description = im.getDescription();
357            bo.typeId = im.getTypeId();
358            bo.active = im.isActive();
359    
360            if (im.getProposition() != null) {
361                PropositionBo propositionBo = PropositionBo.from(im.getProposition());
362                bo.proposition = propositionBo;
363                propositionBo.setRuleId(im.getId());
364            }
365    
366            bo.setVersionNumber(im.getVersionNumber());
367            bo.actions = new ArrayList<ActionBo>();
368    
369            for (ActionDefinition action : im.getActions()) {
370                ActionBo actionBo = ActionBo.from(action);
371                bo.actions.add(actionBo);
372                actionBo.setRule(bo);
373            }
374    
375            // build the set of agenda attribute BOs  
376            List<RuleAttributeBo> attrs = new ArrayList<RuleAttributeBo>();
377            // for each converted pair, build an RuleAttributeBo and add it to the set  
378            RuleAttributeBo attributeBo;
379            for (Map.Entry<String, String> entry : im.getAttributes().entrySet()) {
380                KrmsAttributeDefinitionBo attrDefBo = KrmsRepositoryServiceLocator.getKrmsAttributeDefinitionService().getKrmsAttributeBo(entry.getKey(), im.getNamespace());
381                attributeBo = new RuleAttributeBo();
382                attributeBo.setRuleId(im.getId());
383                attributeBo.setAttributeDefinition(attrDefBo);
384                attributeBo.setValue(entry.getValue());
385                attributeBo.setAttributeDefinition(attrDefBo);
386                attrs.add(attributeBo);
387            }
388    
389            bo.setAttributeBos(attrs);
390    
391            return bo;
392        }
393    
394        public static RuleBo copyRule(RuleBo existing) {
395            // create a simple proposition Bo  
396            RuleBo newRule = new RuleBo();
397            // copy simple fields  
398            newRule.setId(ruleIdIncrementer.getNewId());
399            newRule.setNamespace(existing.getNamespace());
400            newRule.setDescription(existing.getDescription());
401            newRule.setTypeId(existing.getTypeId());
402            newRule.setActive(true);
403    
404            PropositionBo newProp = PropositionBo.copyProposition(existing.getProposition());
405            newProp.setRuleId(newRule.getId());
406            newRule.setProposition(newProp);
407    
408            newRule.setAttributeBos(copyRuleAttributes(existing));
409            newRule.setActions(copyRuleActions(existing, newRule.getId()));
410    
411            return newRule;
412        }
413    
414        /**
415         * Returns a new copy of this rule with new ids.
416         *
417         * @param newRuleName name of the copied rule
418         * @return RuleBo a copy of the this rule, with new ids, and the given name
419         */
420        public RuleBo copyRule(String newRuleName) {
421            RuleBo copiedRule = RuleBo.copyRule(this);
422    
423            // Rule names cannot be the same, the error for being the same name is not displayed to the user, and the document is
424            // said to have been successfully submitted.  
425            //        copiedRule.setName(rule.getName());  
426            copiedRule.setName(newRuleName);
427    
428            return copiedRule;
429        }
430    
431        public static List<RuleAttributeBo> copyRuleAttributes(RuleBo existing) {
432            List<RuleAttributeBo> newAttributes = new ArrayList<RuleAttributeBo>();
433    
434            for (RuleAttributeBo attr : existing.getAttributeBos()) {
435                RuleAttributeBo newAttr = new RuleAttributeBo();
436                newAttr.setId(ruleAttributeIdIncrementer.getNewId());
437                newAttr.setRuleId(attr.getRuleId());
438                newAttr.setAttributeDefinition(attr.getAttributeDefinition());
439                newAttr.setValue(attr.getValue());
440                newAttributes.add(newAttr);
441            }
442    
443            return newAttributes;
444        }
445    
446        public static List<ActionAttributeBo> copyActionAttributes(ActionBo existing) {
447            List<ActionAttributeBo> newAttributes = new ArrayList<ActionAttributeBo>();
448    
449            for (ActionAttributeBo attr : existing.getAttributeBos()) {
450                ActionAttributeBo newAttr = new ActionAttributeBo();
451                newAttr.setId(actionAttributeIdIncrementer.getNewId());
452                newAttr.setAction(existing);
453                newAttr.setAttributeDefinition(attr.getAttributeDefinition());
454                newAttr.setValue(attr.getValue());
455                newAttributes.add(newAttr);
456            }
457    
458            return newAttributes;
459        }
460    
461        public static List<ActionBo> copyRuleActions(RuleBo existing, String ruleId) {
462            List<ActionBo> newActionList = new ArrayList<ActionBo>();
463    
464            for (ActionBo action : existing.getActions()) {
465                ActionBo newAction = new ActionBo();
466                newAction.setId(actionIdIncrementer.getNewId());
467                newAction.setRule(existing);
468                newAction.setDescription(action.getDescription());
469                newAction.setName(action.getName());
470                newAction.setNamespace(action.getNamespace());
471                newAction.setTypeId(action.getTypeId());
472                newAction.setSequenceNumber(action.getSequenceNumber());
473                newAction.setAttributeBos(copyActionAttributes(action));
474                newActionList.add(newAction);
475            }
476    
477            return newActionList;
478        }
479    
480        /*
481         * This is being done because there is a  major issue with lazy relationships, in ensuring that the relationship is
482             * still available after the object has been detached, or serialized. For most JPA providers, after serialization
483             * any lazy relationship that was not instantiated will be broken, and either throw an error when accessed,
484             * or return null.
485             */
486        private void writeObject(ObjectOutputStream stream) throws IOException, ClassNotFoundException {
487            if (proposition != null) {
488                proposition.getId();
489            }
490            stream.defaultWriteObject();
491        }
492    
493        public String getId() {
494            return id;
495        }
496    
497        public void setId(String id) {
498            this.id = id;
499        }
500    
501        public String getNamespace() {
502            return namespace;
503        }
504    
505        public void setNamespace(String namespace) {
506            this.namespace = namespace;
507        }
508    
509        public String getDescription() {
510            return description;
511        }
512    
513        public void setDescription(String description) {
514            this.description = description;
515        }
516    
517        public String getName() {
518            return name;
519        }
520    
521        public void setName(String name) {
522            this.name = name;
523        }
524    
525        public String getTypeId() {
526            return typeId;
527        }
528    
529        public String getPropId() {
530            if (proposition != null) {
531                return proposition.getId();
532            }
533    
534            return null;
535        }
536    
537        public boolean getActive() {
538            return active;
539        }
540    
541        public boolean isActive() {
542            return active;
543        }
544    
545        public void setActive(boolean active) {
546            this.active = active;
547        }
548    
549        public Long getVersionNumber() {
550            return versionNumber;
551        }
552    
553        public void setVersionNumber(Long versionNumber) {
554            this.versionNumber = versionNumber;
555        }
556    
557        public List<ActionBo> getActions() {
558            return actions;
559        }
560    
561        public void setActions(List<ActionBo> actions) {
562            this.actions = actions;
563        }
564    
565        public List<RuleAttributeBo> getAttributeBos() {
566            return attributeBos;
567        }
568    
569        public void setAttributeBos(List<RuleAttributeBo> attributeBos) {
570            this.attributeBos = attributeBos;
571        }
572    
573        public void setPropositionSummary(String propositionSummary) {
574            this.propositionSummary = propositionSummary;
575        }
576    
577        public String getSelectedPropositionId() {
578            return selectedPropositionId;
579        }
580    
581        public void setSelectedPropositionId(String selectedPropositionId) {
582            this.selectedPropositionId = selectedPropositionId;
583        }
584    
585    }