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