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.repository;
017
018import org.apache.commons.lang.StringUtils;
019import org.kuali.rice.core.api.exception.RiceIllegalStateException;
020import org.kuali.rice.krad.data.DataObjectService;
021import org.kuali.rice.krad.data.PersistenceOption;
022import org.kuali.rice.krms.api.repository.action.ActionDefinition;
023import org.kuali.rice.krms.api.repository.rule.RuleDefinition;
024import org.kuali.rice.krms.api.repository.type.KrmsAttributeDefinition;
025
026import java.util.ArrayList;
027import java.util.Collection;
028import java.util.Collections;
029import java.util.HashMap;
030import java.util.HashSet;
031import java.util.LinkedList;
032import java.util.List;
033import java.util.ListIterator;
034import java.util.Map;
035import java.util.Set;
036
037import static org.kuali.rice.krms.impl.repository.BusinessObjectServiceMigrationUtils.findSingleMatching;
038
039public class RuleBoServiceImpl implements RuleBoService {
040
041        private DataObjectService dataObjectService;
042    private KrmsAttributeDefinitionService attributeDefinitionService;
043
044    /**
045     * This overridden creates a KRMS Rule in the repository
046     *
047     * @see org.kuali.rice.krms.impl.repository.RuleBoService#createRule(org.kuali.rice.krms.api.repository.rule.RuleDefinition)
048     */
049    @Override
050    public RuleDefinition createRule(RuleDefinition rule) {
051        if (rule == null){
052            throw new IllegalArgumentException("rule is null");
053        }
054
055        final String nameKey = rule.getName();
056        final String namespaceKey = rule.getNamespace();
057        final RuleDefinition existing = getRuleByNameAndNamespace(nameKey, namespaceKey);
058
059        if (existing != null){
060            throw new IllegalStateException("the rule to create already exists: " + rule);
061        }
062
063        RuleBo ruleBo = RuleBo.from(rule);
064        ruleBo = dataObjectService.save(ruleBo, PersistenceOption.FLUSH);
065
066        return RuleBo.to(ruleBo);
067    }
068
069    /**
070     * This overridden updates an existing Rule in the Repository
071     *
072     * @see org.kuali.rice.krms.impl.repository.RuleBoService#updateRule(org.kuali.rice.krms.api.repository.rule.RuleDefinition)
073     */
074    @Override
075    public RuleDefinition updateRule(RuleDefinition rule) {
076        if (rule == null){
077            throw new IllegalArgumentException("rule is null");
078        }
079
080        // must already exist to be able to update
081        final String ruleIdKey = rule.getId();
082        final RuleBo existing = dataObjectService.find(RuleBo.class, ruleIdKey);
083
084        if (existing == null) {
085            throw new IllegalStateException("the rule does not exist: " + rule);
086        }
087
088        final RuleDefinition toUpdate;
089        String existingPropositionId = null;
090
091        if (existing.getProposition() != null){
092            existingPropositionId = existing.getProposition().getId();
093        }
094
095        if (!existing.getId().equals(rule.getId())){
096            // if passed in id does not match existing id, correct it
097            final RuleDefinition.Builder builder = RuleDefinition.Builder.create(rule);
098            builder.setId(existing.getId());
099            toUpdate = builder.build();
100        } else {
101            toUpdate = rule;
102        }
103
104        RuleBo boToUpdate = RuleBo.from(toUpdate);
105        reconcileActionAttributes(boToUpdate.getActions(), existing.getActions());
106
107        // update the rule and create new attributes
108        RuleBo updatedData = dataObjectService.save(boToUpdate, PersistenceOption.FLUSH);
109
110        //delete the orphan proposition
111        if (updatedData.getProposition() != null && StringUtils.isNotBlank(existingPropositionId)){
112           if (!(updatedData.getProposition().getId().equals(existingPropositionId))) {
113              dataObjectService.delete(existing.getProposition());
114           }
115        }
116
117        return RuleBo.to(updatedData);
118    }
119
120    /**
121     * Transfer any ActionAttributeBos that still apply from the existing actions, while updating their values.
122     *
123     * <p>This method is side effecting, it replaces elements in the passed in toUpdateActionBos collection. </p>
124     *
125     * @param toUpdateActionBos the new ActionBos which will (later) be persisted
126     * @param existingActionBos the ActionBos which have been fetched from the database
127     */
128    private void reconcileActionAttributes(List<ActionBo> toUpdateActionBos, List<ActionBo> existingActionBos) {
129        for (ActionBo toUpdateAction : toUpdateActionBos) {
130
131            ActionBo matchingExistingAction = findMatchingExistingAction(toUpdateAction, existingActionBos);
132
133            if (matchingExistingAction == null) { continue; }
134
135            ListIterator<ActionAttributeBo> toUpdateAttributesIter = toUpdateAction.getAttributeBos().listIterator();
136
137            while (toUpdateAttributesIter.hasNext()) {
138                ActionAttributeBo toUpdateAttribute = toUpdateAttributesIter.next();
139
140                ActionAttributeBo matchingExistingAttribute =
141                        findMatchingExistingAttribute(toUpdateAttribute, matchingExistingAction.getAttributeBos());
142
143                if (matchingExistingAttribute == null) { continue; }
144
145                // set the new value into the existing attribute, then replace the new attribute with the existing one
146                matchingExistingAttribute.setValue(toUpdateAttribute.getValue());
147                toUpdateAttributesIter.set(matchingExistingAttribute);
148            }
149        }
150    }
151
152    /**
153     * Returns the action in existingActionBos that has the same ID as existingAction, or null if none matches.
154     *
155     * @param toUpdateAction
156     * @param existingActionBos
157     * @return the matching action, or null if none match.
158     */
159    private ActionBo findMatchingExistingAction(ActionBo toUpdateAction, List<ActionBo> existingActionBos) {
160        for (ActionBo existingAction : existingActionBos) {
161            if (existingAction.getId().equals(toUpdateAction.getId())) {
162                return existingAction;
163            }
164        }
165
166        return null;
167    }
168
169    /**
170     * Returns the attribute in existingAttributeBos that has the same attributeDefinitionId as toUpdateAttribute, or
171     * null if none matches.
172     *
173     * @param toUpdateAttribute
174     * @param existingAttributeBos
175     * @return the matching attribute, or null if none match.
176     */
177    private ActionAttributeBo findMatchingExistingAttribute(ActionAttributeBo toUpdateAttribute,
178            List<ActionAttributeBo> existingAttributeBos) {
179        for (ActionAttributeBo existingAttribute : existingAttributeBos) {
180            if (existingAttribute.getAttributeDefinitionId().equals(toUpdateAttribute.getAttributeDefinitionId())) {
181                return existingAttribute;
182            }
183        }
184
185        return null;
186    }
187
188    @Override
189    public void deleteRule(String ruleId) {
190        if (ruleId == null) {
191            throw new IllegalArgumentException("ruleId is null");
192        }
193
194        final RuleDefinition existing = getRuleByRuleId(ruleId);
195
196        if (existing == null) {
197            throw new IllegalStateException("the Rule to delete does not exists: " + ruleId);
198        }
199
200        dataObjectService.delete(from(existing));
201    }
202
203    /**
204     * This method retrieves a rule from the repository given the rule id.
205     *
206     * @see org.kuali.rice.krms.impl.repository.RuleBoService#getRuleByRuleId(java.lang.String)
207     */
208    @Override
209    public RuleDefinition getRuleByRuleId(String ruleId) {
210        if (StringUtils.isBlank(ruleId)){
211            throw new IllegalArgumentException("rule id is null");
212        }
213
214        RuleBo bo = dataObjectService.find(RuleBo.class, ruleId);
215
216        return RuleBo.to(bo);
217    }
218
219    /**
220     * This method retrieves a rule from the repository given the name of the rule
221     * and namespace.
222     *
223     * @see org.kuali.rice.krms.impl.repository.RuleBoService#getRuleByRuleId(java.lang.String)
224     */
225    @Override
226    public RuleDefinition getRuleByNameAndNamespace(String name, String namespace) {
227        if (StringUtils.isBlank(name)) {
228            throw new IllegalArgumentException("name is null or blank");
229        }
230        if (StringUtils.isBlank(namespace)) {
231            throw new IllegalArgumentException("namespace is null or blank");
232        }
233
234        final Map<String, Object> map = new HashMap<String, Object>();
235        map.put("name", name);
236        map.put("namespace", namespace);
237
238        RuleBo myRule = findSingleMatching(dataObjectService, RuleBo.class, Collections.unmodifiableMap(map));
239
240        return RuleBo.to(myRule);
241    }
242
243    /**
244     * Gets a rule attribute by its ID
245     *
246     * @param attrId the rule attribute's ID
247     * @return the rule attribute
248     */
249    public RuleAttributeBo getRuleAttributeById(String attrId) {
250        if (StringUtils.isBlank(attrId)){
251            return null;
252        }
253
254        RuleAttributeBo bo = dataObjectService.find(RuleAttributeBo.class, attrId);
255
256        return bo;
257    }
258
259    /**
260     * Converts a immutable {@link RuleDefinition} to its mutable {@link RuleBo} counterpart.
261     * @param rule the immutable object.
262     * @return a {@link RuleBo} the mutable RuleBo.
263     *
264     */
265    public RuleBo from(RuleDefinition rule) {
266        if (rule == null) { return null; }
267
268        RuleBo ruleBo = new RuleBo();
269        ruleBo.setName(rule.getName());
270        ruleBo.setDescription(rule.getDescription());
271        ruleBo.setNamespace(rule.getNamespace());
272        ruleBo.setTypeId(rule.getTypeId());
273        ruleBo.setProposition(PropositionBo.from(rule.getProposition()));
274        ruleBo.setId(rule.getId());
275        ruleBo.setActive(rule.isActive());
276        ruleBo.setVersionNumber(rule.getVersionNumber());
277        ruleBo.setActions(buildActionBoList(rule));
278        ruleBo.setAttributeBos(buildAttributeBoList(rule));
279
280        return ruleBo;
281    }
282
283    private Set<RuleAttributeBo> buildAttributeBo(RuleDefinition im) {
284        Set<RuleAttributeBo> attributes = new HashSet<RuleAttributeBo>();
285
286        // build a map from attribute name to definition
287        Map<String, KrmsAttributeDefinition> attributeDefinitionMap = new HashMap<String, KrmsAttributeDefinition>();
288
289        List<KrmsAttributeDefinition> attributeDefinitions = getAttributeDefinitionService().findAttributeDefinitionsByType(im.getTypeId());
290
291        for (KrmsAttributeDefinition attributeDefinition : attributeDefinitions) {
292            attributeDefinitionMap.put(attributeDefinition.getName(), attributeDefinition);
293        }
294
295        // for each entry, build a RuleAttributeBo and add it to the set
296        if (im.getAttributes() != null) {
297            for (Map.Entry<String,String> entry  : im.getAttributes().entrySet()) {
298                KrmsAttributeDefinition attrDef = attributeDefinitionMap.get(entry.getKey());
299
300                if (attrDef != null) {
301                    RuleAttributeBo attributeBo = new RuleAttributeBo();
302                    attributeBo.setRule( RuleBo.from(im) );
303                    attributeBo.setValue(entry.getValue());
304                    attributeBo.setAttributeDefinition(KrmsAttributeDefinitionBo.from(attrDef));
305                    attributes.add( attributeBo );
306                } else {
307                    throw new RiceIllegalStateException("there is no attribute definition with the name '" +
308                            entry.getKey() + "' that is valid for the rule type with id = '" + im.getTypeId() +"'");
309                }
310            }
311        }
312
313        return attributes;
314    }
315
316    private List<RuleAttributeBo> buildAttributeBoList(RuleDefinition im) {
317        List<RuleAttributeBo> attributes = new LinkedList<RuleAttributeBo>();
318
319        // build a map from attribute name to definition
320        Map<String, KrmsAttributeDefinition> attributeDefinitionMap = new HashMap<String, KrmsAttributeDefinition>();
321
322        List<KrmsAttributeDefinition> attributeDefinitions = getAttributeDefinitionService().findAttributeDefinitionsByType(im.getTypeId());
323
324        for (KrmsAttributeDefinition attributeDefinition : attributeDefinitions) {
325            attributeDefinitionMap.put(attributeDefinition.getName(), attributeDefinition);
326        }
327
328        // for each entry, build a RuleAttributeBo and add it to the set
329        if (im.getAttributes() != null) {
330            for (Map.Entry<String,String> entry  : im.getAttributes().entrySet()) {
331                KrmsAttributeDefinition attrDef = attributeDefinitionMap.get(entry.getKey());
332
333                if (attrDef != null) {
334                    RuleAttributeBo attributeBo = new RuleAttributeBo();
335                    attributeBo.setRule(RuleBo.from(im));
336                    attributeBo.setValue(entry.getValue());
337                    attributeBo.setAttributeDefinition(KrmsAttributeDefinitionBo.from(attrDef));
338                    attributes.add( attributeBo );
339                } else {
340                    throw new RiceIllegalStateException("there is no attribute definition with the name '" +
341                            entry.getKey() + "' that is valid for the rule type with id = '" + im.getTypeId() +"'");
342                }
343            }
344        }
345        return attributes;
346    }
347
348    private List<ActionBo> buildActionBoList(RuleDefinition im) {
349        List<ActionBo> actions = new LinkedList<ActionBo>();
350
351        for (ActionDefinition actionDefinition : im.getActions()) {
352            actions.add(ActionBo.from(actionDefinition));
353        }
354
355        return actions;
356    }
357
358    /**
359     * Sets the dataObjectService attribute value.
360     *
361     * @param dataObjectService The dataObjectService to set.
362     */
363    public void setDataObjectService(final DataObjectService dataObjectService) {
364        this.dataObjectService = dataObjectService;
365    }
366
367    /**
368     * Converts a List<RuleBo> to an Unmodifiable List<Rule>
369     *
370     * @param ruleBos a mutable List<RuleBo> to made completely immutable.
371     * @return An unmodifiable List<Rule>
372     */
373    public List<RuleDefinition> convertListOfBosToImmutables(final Collection<RuleBo> ruleBos) {
374        ArrayList<RuleDefinition> rules = new ArrayList<RuleDefinition>();
375
376        for (RuleBo bo : ruleBos) {
377            RuleDefinition rule = RuleBo.to(bo);
378            rules.add(rule);
379        }
380
381        return Collections.unmodifiableList(rules);
382    }
383
384
385    protected KrmsAttributeDefinitionService getAttributeDefinitionService() {
386        if (attributeDefinitionService == null) {
387            attributeDefinitionService = KrmsRepositoryServiceLocator.getKrmsAttributeDefinitionService();
388        }
389
390        return attributeDefinitionService;
391    }
392}