001/**
002 * Copyright 2005-2013 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 java.util.ArrayList;
019import java.util.Collection;
020import java.util.Collections;
021import java.util.HashMap;
022import java.util.HashSet;
023import java.util.List;
024import java.util.Map;
025import java.util.Map.Entry;
026import java.util.Set;
027
028import org.apache.commons.lang.StringUtils;
029import org.kuali.rice.core.api.CoreApiServiceLocator;
030import org.kuali.rice.core.api.criteria.CriteriaLookupService;
031import org.kuali.rice.core.api.criteria.GenericQueryResults;
032import org.kuali.rice.core.api.criteria.Predicate;
033import org.kuali.rice.core.api.criteria.QueryByCriteria;
034import org.kuali.rice.core.api.exception.RiceIllegalArgumentException;
035import org.kuali.rice.core.api.exception.RiceIllegalStateException;
036import org.kuali.rice.core.api.mo.ModelObjectUtils;
037import org.kuali.rice.core.impl.services.CoreImplServiceLocator;
038import org.kuali.rice.coreservice.api.CoreServiceApiServiceLocator;
039import org.kuali.rice.coreservice.framework.CoreFrameworkServiceLocator;
040import org.kuali.rice.krad.service.BusinessObjectService;
041import org.kuali.rice.krad.service.KRADServiceLocator;
042import org.kuali.rice.krad.service.SequenceAccessorService;
043import org.kuali.rice.krms.api.repository.agenda.AgendaDefinition;
044import org.kuali.rice.krms.api.repository.agenda.AgendaItemDefinition;
045import org.kuali.rice.krms.api.repository.type.KrmsAttributeDefinition;
046import org.kuali.rice.krms.impl.util.KrmsImplConstants.PropertyNames;
047import org.springframework.util.CollectionUtils;
048
049import static org.kuali.rice.core.api.criteria.PredicateFactory.in;
050
051/**
052 * Implementation of the interface for accessing KRMS repository Agenda related
053 * business objects. 
054 *
055 * @author Kuali Rice Team (rice.collab@kuali.org)
056 *
057 */
058public final class KSAgendaBoServiceImpl implements AgendaBoService {
059
060    private BusinessObjectService businessObjectService;
061    private CriteriaLookupService criteriaLookupService;
062    private KrmsAttributeDefinitionService attributeDefinitionService;
063    private SequenceAccessorService sequenceAccessorService;
064
065    // used for converting lists of BOs to model objects
066    private static final ModelObjectUtils.Transformer<AgendaItemBo, AgendaItemDefinition> toAgendaItemDefinition =
067            new ModelObjectUtils.Transformer<AgendaItemBo, AgendaItemDefinition>() {
068                public AgendaItemDefinition transform(AgendaItemBo input) {
069                    return AgendaItemBo.to(input);
070                };
071            };
072
073    // used for converting lists of BOs to model objects
074    private static final ModelObjectUtils.Transformer<AgendaBo, AgendaDefinition> toAgendaDefinition =
075            new ModelObjectUtils.Transformer<AgendaBo, AgendaDefinition>() {
076                public AgendaDefinition transform(AgendaBo input) {
077                    return AgendaBo.to(input);
078                };
079            };
080
081
082    /**
083     * This overridden method creates a KRMS Agenda in the repository
084     */
085    @Override
086    public AgendaDefinition createAgenda(AgendaDefinition agenda) {
087        if (agenda == null){
088            throw new RiceIllegalArgumentException("agenda is null");
089        }
090        final String nameKey = agenda.getName();
091        final String contextId = agenda.getContextId();
092        final AgendaDefinition existing = getAgendaByNameAndContextId(nameKey, contextId);
093        if (existing != null){
094            throw new IllegalStateException("the agenda to create already exists: " + agenda);
095        }
096
097        AgendaBo agendaBo = from(agenda);
098        businessObjectService.save(agendaBo);
099        return to(agendaBo);
100    }
101
102    /**
103     * This overridden method updates an existing Agenda in the repository
104     */
105    @Override
106    public void updateAgenda(AgendaDefinition agenda) {
107        if (agenda == null){
108            throw new RiceIllegalArgumentException("agenda is null");
109        }
110
111        // must already exist to be able to update
112        final String agendaIdKey = agenda.getId();
113        final AgendaBo existing = businessObjectService.findBySinglePrimaryKey(AgendaBo.class, agendaIdKey);
114        if (existing == null) {
115            throw new IllegalStateException("the agenda does not exist: " + agenda);
116        }
117        final AgendaDefinition toUpdate;
118        if (existing.getId().equals(agenda.getId())) {
119            toUpdate = agenda;
120        } else {
121            // if passed in id does not match existing id, correct it
122            final AgendaDefinition.Builder builder = AgendaDefinition.Builder.create(agenda);
123            builder.setId(existing.getId());
124            toUpdate = builder.build();
125        }
126
127        // copy all updateable fields to bo
128        AgendaBo boToUpdate = from(toUpdate);
129
130        // delete any old, existing attributes
131        Map<String,String> fields = new HashMap<String,String>(1);
132        fields.put(PropertyNames.Agenda.AGENDA_ID, toUpdate.getId());
133        businessObjectService.deleteMatching(AgendaAttributeBo.class, fields);
134
135        // update new agenda and create new attributes
136        businessObjectService.save(boToUpdate);
137    }
138
139    @Override
140    public void deleteAgenda(String agendaId) {
141        if (agendaId == null){ throw new RiceIllegalArgumentException("agendaId is null"); }
142        final AgendaDefinition existing = getAgendaByAgendaId(agendaId);
143        if (existing == null){ throw new IllegalStateException("the Agenda to delete does not exists: " + agendaId);}
144
145        List<AgendaItemDefinition> agendaItems = this.getAgendaItemsByAgendaId(existing.getId());
146        for( AgendaItemDefinition agendaItem : agendaItems) {
147            businessObjectService.delete(AgendaItemBo.from(agendaItem));
148        }
149
150        // delete any old, existing attributes
151        Map<String,String> fields = new HashMap<String,String>(1);
152        fields.put(PropertyNames.Agenda.AGENDA_ID, existing.getId());
153        businessObjectService.deleteMatching(AgendaAttributeBo.class, fields);
154
155        businessObjectService.delete(from(existing));
156    }
157
158    /**
159     * This overridden method retrieves an Agenda from the repository
160     */
161    @Override
162    public AgendaDefinition getAgendaByAgendaId(String agendaId) {
163        if (StringUtils.isBlank(agendaId)){
164            throw new RiceIllegalArgumentException("agenda id is null or blank");
165        }
166        AgendaBo bo = businessObjectService.findBySinglePrimaryKey(AgendaBo.class, agendaId);
167        return to(bo);
168    }
169
170    /**
171     * This overridden method retrieves an agenda from the repository
172     */
173    @Override
174    public AgendaDefinition getAgendaByNameAndContextId(String name, String contextId) {
175        if (StringUtils.isBlank(name)) {
176            throw new RiceIllegalArgumentException("name is blank");
177        }
178        if (StringUtils.isBlank(contextId)) {
179            throw new RiceIllegalArgumentException("contextId is blank");
180        }
181
182        final Map<String, Object> map = new HashMap<String, Object>();
183        map.put("name", name);
184        map.put("contextId", contextId);
185
186        AgendaBo myAgenda = businessObjectService.findByPrimaryKey(AgendaBo.class, Collections.unmodifiableMap(map));
187        return to(myAgenda);
188    }
189
190    /**
191     * This overridden method retrieves a set of agendas from the repository
192     */
193    @Override
194    public List<AgendaDefinition> getAgendasByContextId(String contextId) {
195        if (StringUtils.isBlank(contextId)){
196            throw new RiceIllegalArgumentException("context ID is null or blank");
197        }
198        final Map<String, Object> map = new HashMap<String, Object>();
199        map.put("contextId", contextId);
200        List<AgendaBo> bos = (List<AgendaBo>) businessObjectService.findMatching(AgendaBo.class, map);
201        return convertAgendaBosToImmutables(bos);
202    }
203
204    /**
205     * This overridden method creates a new Agenda in the repository
206     */
207    @Override
208    public AgendaItemDefinition createAgendaItem(AgendaItemDefinition agendaItem) {
209        if (agendaItem == null){
210            throw new RiceIllegalArgumentException("agendaItem is null");
211        }
212        if (agendaItem.getId() != null){
213            final AgendaDefinition existing = getAgendaByAgendaId(agendaItem.getId());
214            if (existing != null){
215                throw new IllegalStateException("the agendaItem to create already exists: " + agendaItem);
216            }
217        }
218
219        AgendaItemBo bo = AgendaItemBo.from(agendaItem);
220        businessObjectService.save(bo);
221        return AgendaItemBo.to(bo);
222    }
223
224    /**
225     * This overridden method updates an existing Agenda in the repository
226     */
227    @Override
228    public void updateAgendaItem(AgendaItemDefinition agendaItem) {
229        if (agendaItem == null){
230            throw new RiceIllegalArgumentException("agendaItem is null");
231        }
232        final String agendaItemIdKey = agendaItem.getId();
233        final AgendaItemDefinition existing = getAgendaItemById(agendaItemIdKey);
234        if (existing == null) {
235            throw new IllegalStateException("the agenda item does not exist: " + agendaItem);
236        }
237        final AgendaItemDefinition toUpdate;
238        if (existing.getId().equals(agendaItem.getId())) {
239            toUpdate = agendaItem;
240        } else {
241            final AgendaItemDefinition.Builder builder = AgendaItemDefinition.Builder.create(agendaItem);
242            builder.setId(existing.getId());
243            toUpdate = builder.build();
244        }
245
246        AgendaItemBo aiBo = AgendaItemBo.from(toUpdate);
247        updateActionAttributes(aiBo);
248        businessObjectService.save(aiBo);
249    }
250
251    private void updateActionAttributes(AgendaItemBo aiBo) {
252        if(aiBo.getRule()!=null){
253            updateActionAttributes(aiBo.getRule().getActions());
254        }
255        if(aiBo.getWhenTrue()!=null){
256            updateActionAttributes(aiBo.getWhenTrue());
257        }
258        if(aiBo.getWhenFalse()!=null){
259            updateActionAttributes(aiBo.getWhenFalse());
260        }
261        if(aiBo.getAlways()!=null){
262            updateActionAttributes(aiBo.getAlways());
263        }
264    }
265
266    private void updateActionAttributes(List<ActionBo> actionBos) {
267        for (ActionBo action : actionBos) {
268            for (ActionAttributeBo aa : action.getAttributeBos()) {
269                final Map<String, Object> map = new HashMap<String, Object>();
270                map.put("actionId", action.getId());
271
272                List<ActionAttributeBo> aaBos = (List<ActionAttributeBo>) businessObjectService.findMatching(ActionAttributeBo.class, Collections.unmodifiableMap(map));
273                for (ActionAttributeBo aaBo : aaBos) {
274                    if (aaBo.getAttributeDefinitionId().equals(aa.getAttributeDefinitionId())) {
275                        aa.setId(aaBo.getId());
276                        aa.setVersionNumber(aaBo.getVersionNumber());
277                    }
278                }
279            }
280        }
281    }
282
283    /**
284     * This overridden method adds a new AgendaItemDefinition to the repository
285     */
286    @Override
287    public void addAgendaItem(AgendaItemDefinition agendaItem, String parentId, Boolean position) {
288        if (agendaItem == null){
289            throw new RiceIllegalArgumentException("agendaItem is null");
290        }
291        AgendaItemDefinition parent = null;
292        if (parentId != null){
293            parent = getAgendaItemById(parentId);
294            if (parent == null){
295                throw new IllegalStateException("parent agendaItem does not exist in repository. parentId = " + parentId);
296            }
297        }
298        // create new AgendaItemDefinition
299        final AgendaItemDefinition toCreate;
300        if (agendaItem.getId() == null) {
301            SequenceAccessorService sas = getSequenceAccessorService();
302            final AgendaItemDefinition.Builder builder = AgendaItemDefinition.Builder.create(agendaItem);
303            final String newId =sas.getNextAvailableSequenceNumber(
304                    "KRMS_AGENDA_ITM_S", AgendaItemBo.class).toString();
305            builder.setId(newId);
306            toCreate = builder.build();
307        } else {
308            toCreate = agendaItem;
309        }
310        createAgendaItem(toCreate);
311
312        // link it to it's parent (for whenTrue/whenFalse, sibling for always
313        if (parentId != null) {
314            final AgendaItemDefinition.Builder builder = AgendaItemDefinition.Builder.create(parent);
315            if (position == null){
316                builder.setAlwaysId( toCreate.getId() );
317            } else if (position.booleanValue()){
318                builder.setWhenTrueId( toCreate.getId() );
319            } else if (!position.booleanValue()){
320                builder.setWhenFalseId( toCreate.getId() );
321            }
322            final AgendaItemDefinition parentToUpdate = builder.build();
323            updateAgendaItem( parentToUpdate );
324        }
325    }
326
327    /**
328     * This overridden method retrieves an AgendaItemDefinition from the repository
329     */
330    @Override
331    public AgendaItemDefinition getAgendaItemById(String id) {
332        if (StringUtils.isBlank(id)){
333            throw new RiceIllegalArgumentException("agenda item id is null or blank");
334        }
335        AgendaItemBo bo = businessObjectService.findBySinglePrimaryKey(AgendaItemBo.class, id);
336        return AgendaItemBo.to(bo);
337    }
338
339    @Override
340    public List<AgendaItemDefinition> getAgendaItemsByAgendaId(String agendaId) {
341        if (StringUtils.isBlank(agendaId)){
342            throw new RiceIllegalArgumentException("agenda id is null or null");
343        }
344        List<AgendaItemDefinition> results = null;
345
346        Collection<AgendaItemBo> bos = businessObjectService.findMatching(AgendaItemBo.class, Collections.singletonMap("agendaId", agendaId));
347
348        if (CollectionUtils.isEmpty(bos)) {
349            results = Collections.emptyList();
350        } else {
351            results = Collections.unmodifiableList(ModelObjectUtils.transform(bos, toAgendaItemDefinition));
352        }
353
354        return results;
355    }
356
357    @Override
358    public List<AgendaDefinition> getAgendasByType(String typeId) throws RiceIllegalArgumentException {
359        if (StringUtils.isBlank(typeId)){
360            throw new RiceIllegalArgumentException("type ID is null or blank");
361        }
362        final Map<String, Object> map = new HashMap<String, Object>();
363        map.put("typeId", typeId);
364        List<AgendaBo> bos = (List<AgendaBo>) businessObjectService.findMatching(AgendaBo.class, map);
365        return convertAgendaBosToImmutables(bos);
366    }
367
368    @Override
369    public List<AgendaDefinition> getAgendasByTypeAndContext(String typeId,
370                                                             String contextId) throws RiceIllegalArgumentException {
371        if (StringUtils.isBlank(typeId)){
372            throw new RiceIllegalArgumentException("type ID is null or blank");
373        }
374        if (StringUtils.isBlank(contextId)){
375            throw new RiceIllegalArgumentException("context ID is null or blank");
376        }
377        final Map<String, Object> map = new HashMap<String, Object>();
378        map.put("typeId", typeId);
379        map.put("contextId", contextId);
380        Collection<AgendaBo> bos = businessObjectService.findMatching(AgendaBo.class, map);
381        return convertAgendaBosToImmutables(bos);
382    }
383
384    @Override
385    public List<AgendaItemDefinition> getAgendaItemsByType(String typeId) throws RiceIllegalArgumentException {
386        return findAgendaItemsForAgendas(getAgendasByType(typeId));
387    }
388
389    @Override
390    public List<AgendaItemDefinition> getAgendaItemsByContext(String contextId) throws RiceIllegalArgumentException {
391        return findAgendaItemsForAgendas(getAgendasByContextId(contextId));
392    }
393
394    @Override
395    public List<AgendaItemDefinition> getAgendaItemsByTypeAndContext(String typeId,
396                                                                     String contextId) throws RiceIllegalArgumentException {
397        return findAgendaItemsForAgendas(getAgendasByTypeAndContext(typeId, contextId));
398    }
399
400    @Override
401    public void deleteAgendaItem(String agendaItemId) throws RiceIllegalArgumentException {
402        if (StringUtils.isBlank(agendaItemId)) {
403            throw new RiceIllegalArgumentException("agendaItemId must not be blank or null");
404        }
405
406        businessObjectService.deleteMatching(AgendaItemBo.class, Collections.singletonMap("id", agendaItemId));
407    }
408
409    private List<AgendaItemDefinition> findAgendaItemsForAgendas(List<AgendaDefinition> agendaDefinitions) {
410        List<AgendaItemDefinition> results = null;
411
412        if (!CollectionUtils.isEmpty(agendaDefinitions)) {
413            List<AgendaItemBo> boResults = new ArrayList<AgendaItemBo>(agendaDefinitions.size());
414
415            List<String> agendaIds = new ArrayList<String>(20);
416            for (AgendaDefinition agendaDefinition : agendaDefinitions) {
417                agendaIds.add(agendaDefinition.getId());
418
419                if (agendaIds.size() == 20) {
420                    // fetch batch
421
422                    Predicate predicate = in("agendaId", agendaIds.toArray());
423                    QueryByCriteria criteria = QueryByCriteria.Builder.fromPredicates(predicate);
424                    GenericQueryResults<AgendaItemBo> batch = this.getCriteriaLookupService().lookup(AgendaItemBo.class, criteria);
425
426                    boResults.addAll(batch.getResults());
427
428                    // reset agendaIds
429                    agendaIds.clear();
430                }
431            }
432
433            if (agendaIds.size() > 0) {
434                Predicate predicate = in("agendaId", agendaIds.toArray());
435                QueryByCriteria criteria = QueryByCriteria.Builder.fromPredicates(predicate);
436                GenericQueryResults<AgendaItemBo> batch = this.getCriteriaLookupService().lookup(AgendaItemBo.class, criteria);
437
438                boResults.addAll(batch.getResults());
439            }
440
441            results = Collections.unmodifiableList(ModelObjectUtils.transform(boResults, toAgendaItemDefinition));
442        } else {
443            results = Collections.emptyList();
444        }
445
446        return results;
447    }
448
449    public CriteriaLookupService getCriteriaLookupService() {
450        if (criteriaLookupService == null) {
451            criteriaLookupService = KrmsRepositoryServiceLocator.getCriteriaLookupService();
452        }
453        return criteriaLookupService;
454    }
455
456    public void setCriteriaLookupService(CriteriaLookupService criteriaLookupService) {
457        this.criteriaLookupService = criteriaLookupService;
458    }
459
460    /**
461     * Sets the businessObjectService attribute value.
462     *
463     * @param businessObjectService The businessObjectService to set.
464     */
465    public void setBusinessObjectService(final BusinessObjectService businessObjectService) {
466        this.businessObjectService = businessObjectService;
467    }
468
469    protected BusinessObjectService getBusinessObjectService() {
470        if ( businessObjectService == null ) {
471            businessObjectService = KRADServiceLocator.getBusinessObjectService();
472        }
473        return businessObjectService;
474    }
475
476    /**
477     * Sets the sequenceAccessorService attribute value.
478     *
479     * @param sequenceAccessorService The sequenceAccessorService to set.
480     */
481    public void setSequenceAccessorService(final SequenceAccessorService sequenceAccessorService) {
482        this.sequenceAccessorService = sequenceAccessorService;
483    }
484
485    protected SequenceAccessorService getSequenceAccessorService() {
486        if ( sequenceAccessorService == null ) {
487            sequenceAccessorService = KRADServiceLocator.getSequenceAccessorService();
488        }
489        return sequenceAccessorService;
490    }
491
492    protected KrmsAttributeDefinitionService getAttributeDefinitionService() {
493        if (attributeDefinitionService == null) {
494            attributeDefinitionService = KrmsRepositoryServiceLocator.getKrmsAttributeDefinitionService();
495        }
496        return attributeDefinitionService;
497    }
498
499    public void setAttributeDefinitionService(KrmsAttributeDefinitionService attributeDefinitionService) {
500        this.attributeDefinitionService = attributeDefinitionService;
501    }
502
503    /**
504     * Converts a Set<AgendaBo> to an Unmodifiable Set<Agenda>
505     *
506     * @param agendaBos a mutable Set<AgendaBo> to made completely immutable.
507     * @return An unmodifiable Set<Agenda>
508     */
509    public List<AgendaDefinition> convertAgendaBosToImmutables(final Collection<AgendaBo> agendaBos) {
510        if (CollectionUtils.isEmpty(agendaBos)) {
511            return Collections.emptyList();
512        }
513        return Collections.unmodifiableList(ModelObjectUtils.transform(agendaBos, toAgendaDefinition));
514    }
515
516    /**
517     * Converts a mutable bo to it's immutable counterpart
518     * @param bo the mutable business object
519     * @return the immutable object
520     */
521    @Override
522    public AgendaDefinition to(AgendaBo bo) {
523        if (bo == null) { return null; }
524        return org.kuali.rice.krms.api.repository.agenda.AgendaDefinition.Builder.create(bo).build();
525    }
526
527
528    /**
529     * Converts a immutable object to it's mutable bo counterpart
530     * @param im immutable object
531     * @return the mutable bo
532     */
533    @Override
534    public AgendaBo from(AgendaDefinition im) {
535        if (im == null) { return null; }
536
537        AgendaBo bo = new AgendaBo();
538        bo.setId(im.getId());
539        bo.setName( im.getName() );
540        bo.setTypeId( im.getTypeId() );
541        bo.setContextId( im.getContextId() );
542        bo.setFirstItemId( im.getFirstItemId() );
543        bo.setVersionNumber( im.getVersionNumber() );
544        bo.setActive(im.isActive());
545        Set<AgendaAttributeBo> attributes = buildAgendaAttributeBo(im);
546
547        bo.setAttributeBos(attributes);
548
549        return bo;
550    }
551
552    private Set<AgendaAttributeBo> buildAgendaAttributeBo(AgendaDefinition im) {
553        Set<AgendaAttributeBo> attributes = new HashSet<AgendaAttributeBo>();
554
555        // build a map from attribute name to definition
556        Map<String, KrmsAttributeDefinition> attributeDefinitionMap = new HashMap<String, KrmsAttributeDefinition>();
557
558        List<KrmsAttributeDefinition> attributeDefinitions =
559                getAttributeDefinitionService().findAttributeDefinitionsByType(im.getTypeId());
560
561        for (KrmsAttributeDefinition attributeDefinition : attributeDefinitions) {
562            attributeDefinitionMap.put(attributeDefinition.getName(), attributeDefinition);
563        }
564
565        // for each entry, build an AgendaAttributeBo and add it to the set
566        for (Entry<String,String> entry  : im.getAttributes().entrySet()){
567            KrmsAttributeDefinition attrDef = attributeDefinitionMap.get(entry.getKey());
568
569            if (attrDef != null) {
570                AgendaAttributeBo attributeBo = new AgendaAttributeBo();
571                attributeBo.setAgendaId( im.getId() );
572                attributeBo.setAttributeDefinitionId(attrDef.getId());
573                attributeBo.setValue(entry.getValue());
574                attributeBo.setAttributeDefinition(KrmsAttributeDefinitionBo.from(attrDef));
575                attributes.add( attributeBo );
576            } else {
577                throw new RiceIllegalStateException("there is no attribute definition with the name '" +
578                        entry.getKey() + "' that is valid for the agenda type with id = '" + im.getTypeId() +"'");
579            }
580        }
581        return attributes;
582    }
583
584}