org.sakaiproject.evaluation.logic.EvalAuthoringServiceImpl.java Source code

Java tutorial

Introduction

Here is the source code for org.sakaiproject.evaluation.logic.EvalAuthoringServiceImpl.java

Source

/**
 * Copyright 2005 Sakai Foundation Licensed under the
 * Educational Community License, Version 2.0 (the "License"); you may
 * not use this file except in compliance with the License. You may
 * obtain a copy of the License at
 *
 * http://www.osedu.org/licenses/ECL-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an "AS IS"
 * BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
 * or implied. See the License for the specific language governing
 * permissions and limitations under the License.
 */
package org.sakaiproject.evaluation.logic;

import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.Map.Entry;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.sakaiproject.evaluation.constant.EvalConstants;
import org.sakaiproject.evaluation.dao.EvaluationDao;
import org.sakaiproject.evaluation.logic.exceptions.BlankRequiredFieldException;
import org.sakaiproject.evaluation.logic.externals.EvalExternalLogic;
import org.sakaiproject.evaluation.logic.externals.EvalSecurityChecksImpl;
import org.sakaiproject.evaluation.model.EvalEvaluation;
import org.sakaiproject.evaluation.model.EvalItem;
import org.sakaiproject.evaluation.model.EvalItemGroup;
import org.sakaiproject.evaluation.model.EvalScale;
import org.sakaiproject.evaluation.model.EvalTemplate;
import org.sakaiproject.evaluation.model.EvalTemplateItem;
import org.sakaiproject.evaluation.utils.ArrayUtils;
import org.sakaiproject.evaluation.utils.ComparatorsUtils;
import org.sakaiproject.evaluation.utils.EvalUtils;
import org.sakaiproject.evaluation.utils.TemplateItemUtils;
import org.sakaiproject.genericdao.api.search.Order;
import org.sakaiproject.genericdao.api.search.Restriction;
import org.sakaiproject.genericdao.api.search.Search;

/**
 * Implementation of the authoring logic
 * 
 * @author Aaron Zeckoski (aaron@caret.cam.ac.uk)
 */
public class EvalAuthoringServiceImpl implements EvalAuthoringService {

    private static final Log LOG = LogFactory.getLog(EvalAuthoringServiceImpl.class);

    // Event names cannot be over 32 chars long     // max-32:12345678901234567890123456789012
    private final String EVENT_TEMPLATE_CREATE = "eval.template.added";
    private final String EVENT_TEMPLATE_UPDATE = "eval.template.updated";
    private final String EVENT_TEMPLATE_DELETE = "eval.template.removed";

    private final String EVENT_SCALE_CREATE = "eval.scale.added";
    private final String EVENT_SCALE_UPDATE = "eval.scale.updated";
    private final String EVENT_SCALE_DELETE = "eval.scale.removed";

    private final String EVENT_ITEM_CREATE = "eval.item.added";
    private final String EVENT_ITEM_UPDATE = "eval.item.updated";
    private final String EVENT_ITEM_DELETE = "eval.item.removed";

    private final String EVENT_TEMPLATEITEM_CREATE = "eval.templateitem.added";
    private final String EVENT_TEMPLATEITEM_UPDATE = "eval.templateitem.updated";
    private final String EVENT_TEMPLATEITEM_DELETE = "eval.templateitem.removed";

    private EvaluationDao dao;

    public void setDao(EvaluationDao dao) {
        this.dao = dao;
    }

    private EvalCommonLogic commonLogic;

    public void setCommonLogic(EvalCommonLogic commonLogic) {
        this.commonLogic = commonLogic;
    }

    private EvalSettings settings;

    public void setSettings(EvalSettings settings) {
        this.settings = settings;
    }

    private EvalSecurityChecksImpl securityChecks;

    public void setSecurityChecks(EvalSecurityChecksImpl securityChecks) {
        this.securityChecks = securityChecks;
    }

    public void init() {
        // this method will help us to patch up the system if needed
        cleanupAuthoring();
    }

    protected void cleanupAuthoring() {
        String serverId = commonLogic.getConfigurationSetting(EvalExternalLogic.SETTING_SERVER_ID,
                "UNKNOWN_SERVER_ID");
        Boolean lockObtained = dao.obtainLock("authoring_cleanup", serverId, 60 * 1000);
        // only execute the code if we have an exclusive lock
        if (EvalUtils.safeBool(lockObtained)) {

            // fix up the scales with null modes
            List<EvalScale> scales = dao.findBySearch(EvalScale.class,
                    new Search(new Restriction("mode", "", Restriction.NULL)));
            if (scales.size() > 0) {
                LOG.debug("Found " + scales.size() + " scales with a null mode, fixing up data...");
                for (EvalScale scale : scales) {
                    // set this to the default then
                    scale.setMode(EvalConstants.SCALE_MODE_SCALE);
                }
                dao.saveSet(new HashSet<EvalScale>(scales));
                LOG.debug("Fixed " + scales.size() + " scales with a null mode, set to default SCALE_MODE...");
            }

            // fix up orphaned template items (template or item is null)
            dao.findBySearch(EvalTemplateItem.class,
                    new Search(new Restriction[] { new Restriction("template.id", "", Restriction.NULL),
                            new Restriction("item.id", "", Restriction.NULL) }));

        }
    }

    public EvalScale getScaleById(Long scaleId) {
        LOG.debug("scaleId: " + scaleId);
        // get the scale by passing in id
        EvalScale scale = (EvalScale) dao.findById(EvalScale.class, scaleId);
        return scale;
    }

    public EvalScale getScaleByEid(String eid) {
        LOG.debug("scale eid: " + eid);
        EvalScale evalScale = null;
        if (eid != null) {
            List<EvalScale> evalScales = dao.findBySearch(EvalScale.class, new Search("eid", eid));
            if (evalScales != null && evalScales.size() == 1) {
                evalScale = evalScales.get(0);
            }
        }
        return evalScale;
    }

    public void saveScale(EvalScale scale, String userId) {
        LOG.debug("userId: " + userId + ", scale: " + scale.getTitle());

        // set the date modified
        scale.setLastModified(new Date());

        // check for null or length 0 or 1 options
        if (scale.getOptions() == null || scale.getOptions().isEmpty()) {
            throw new IllegalArgumentException("Scale options cannot be null and must have at least 2 items");
        }

        // check the sharing constants
        if (scale.getSharing() == null) {
            scale.setSharing(EvalConstants.SHARING_PRIVATE);
        }
        EvalUtils.validateSharingConstant(scale.getSharing());
        if (EvalConstants.SHARING_OWNER.equals(scale.getSharing())) {
            throw new IllegalArgumentException("Invalid sharing constant (" + scale.getSharing()
                    + ") set for scale (" + scale.getTitle() + "), cannot use SHARING_OWNER");
        } else if (EvalConstants.SHARING_PUBLIC.equals(scale.getSharing())) {
            // test if non-admin trying to set public sharing
            if (!commonLogic.isUserAdmin(userId)) {
                throw new IllegalArgumentException(
                        "Only admins can set scale (" + scale.getTitle() + ") sharing to public");
            }
        }

        // check locking not changed
        boolean newScale = true;
        if (scale.getId() != null) {
            newScale = false;
            //         // existing scale, don't allow change to locked setting
            //
            //         // TODO - this does not work, it just gets the persistent scale from memory -AZ
            //         EvalScale existingScale = getScaleOrFail(scale.getId());
            //
            //         if (! existingScale.getLocked().equals(scale.getLocked())) {
            //            throw new IllegalArgumentException("Cannot change locked setting on existing scale (" + scale.getId() + ")");
            //         }
        }

        // check perms and save
        if (securityChecks.checkUserControlScale(userId, scale)) {
            // fill in any default values and nulls here
            if (scale.getMode() == null) {
                // set this to the default then
                scale.setMode(EvalConstants.SCALE_MODE_SCALE);
            }

            if (scale.getLocked() == null) {
                scale.setLocked(Boolean.FALSE);
            }

            // replace adhoc default title with a unique title
            if (EvalConstants.SCALE_ADHOC_DEFAULT_TITLE.equals(scale.getTitle())) {
                scale.setTitle("adhoc-" + EvalUtils.makeUniqueIdentifier(100));
            }

            // check for required title
            if (EvalUtils.isBlank(scale.getTitle())) {
                throw new BlankRequiredFieldException("Cannot save a scale with a blank title", "title");
            } else {
                // cleanup for XSS scripting and strings
                scale.setTitle(commonLogic.cleanupUserStrings(scale.getTitle()));
            }

            // now save the scale
            dao.save(scale);
            if (newScale) {
                commonLogic.registerEntityEvent(EVENT_SCALE_CREATE, scale);
            } else {
                commonLogic.registerEntityEvent(EVENT_SCALE_UPDATE, scale);
            }
            LOG.debug("User (" + userId + ") saved scale (" + scale.getId() + "), title: " + scale.getTitle());
            return;
        }

        // should not get here so die if we do
        throw new RuntimeException(
                "User (" + userId + ") could NOT save scale (" + scale.getId() + "), title: " + scale.getTitle());
    }

    public void deleteScale(Long scaleId, String userId) {
        LOG.debug("userId: " + userId + ", scaleId: " + scaleId);
        // get the scale by id
        EvalScale scale = getScaleOrFail(scaleId);

        // ADMIN CAN REMOVE EXPERT SCALES -AZ
        //    // cannot remove expert scales
        //    if (scale.getExpert().booleanValue()) {
        //    throw new IllegalStateException("Cannot remove expert scale: " + scaleId);
        //    }

        // check perms and remove
        if (securityChecks.checkUserControlScale(userId, scale)) {
            dao.delete(scale);
            commonLogic.registerEntityEvent(EVENT_SCALE_DELETE, scale);
            LOG.debug("User (" + userId + ") deleted scale (" + scale.getId() + "), title: " + scale.getTitle());
            return;
        }

        // should not get here so die if we do
        throw new RuntimeException("User (" + userId + ") could NOT delete scale (" + scale.getId() + ")");
    }

    /* (non-Javadoc)
     * @see org.sakaiproject.evaluation.logic.EvalScalesLogic#getScalesForUser(java.lang.String, java.lang.String)
     */
    public List<EvalScale> getScalesForUser(String userId, String sharingConstant) {
        LOG.debug("userId: " + userId + ", sharingConstant: " + sharingConstant);

        if (userId == null) {
            throw new IllegalArgumentException("Must include a userId");
        }

        // admin always gets all of the templates of a type
        if (commonLogic.isUserAdmin(userId)) {
            userId = null;
        }

        String[] sharingConstants = makeSharingConstantsArray(sharingConstant);

        // only get type standard templates
        String[] props = new String[] { "mode" };
        Object[] values = new Object[] { EvalConstants.SCALE_MODE_SCALE };
        int[] comparisons = new int[] { Restriction.EQUALS };

        String[] order = new String[] { "title" };
        String[] options = new String[] { "notHidden" };

        return dao.getSharedEntitiesForUser(EvalScale.class, userId, sharingConstants, props, values, comparisons,
                order, options, 0, 0);
    }

    // PERMISSIONS

    public boolean canModifyScale(String userId, Long scaleId) {
        LOG.debug("userId: " + userId + ", scaleId: " + scaleId);
        // get the scale by id
        EvalScale scale = getScaleOrFail(scaleId);

        // check perms and locked
        boolean allowed = false;
        try {
            allowed = securityChecks.checkUserControlScale(userId, scale);
        } catch (RuntimeException e) {
            LOG.debug(e.getMessage());
        }
        return allowed;
    }

    public boolean canRemoveScale(String userId, Long scaleId) {
        LOG.debug("userId: " + userId + ", scaleId: " + scaleId);
        // get the scale by id
        EvalScale scale = getScaleOrFail(scaleId);

        // can remove scales that are in use now
        //      // cannot remove scales that are in use
        //      if (dao.isUsedScale(scaleId)) {
        //         log.debug("Cannot remove scale ("+scaleId+") which is used in at least one item");
        //         return false;
        //      }

        // check perms and locked
        boolean allowed = false;
        try {
            allowed = securityChecks.checkUserControlScale(userId, scale);
        } catch (RuntimeException e) {
            LOG.debug(e.getMessage());
        }
        return allowed;
    }

    /**
     * Get a scale by id or die if not found
     * @param scaleId
     * @return the scale
     * @throws IllegalArgumentException if no scale found
     */
    protected EvalScale getScaleOrFail(Long scaleId) {
        EvalScale scale = getScaleById(scaleId);
        if (scale == null) {
            throw new IllegalArgumentException("Cannot find scale with id: " + scaleId);
        }
        return scale;
    }

    // ITEMS

    public EvalItem getItemById(Long itemId) {
        LOG.debug("itemId:" + itemId);
        EvalItem item = (EvalItem) dao.findById(EvalItem.class, itemId);
        return item;
    }

    public EvalItem getItemByEid(String eid) {
        EvalItem evalItem = null;
        if (eid != null) {
            LOG.debug("eid: " + eid);
            List<EvalItem> evalItems = dao.findBySearch(EvalItem.class, new Search("eid", eid));
            if (evalItems != null && evalItems.size() == 1) {
                evalItem = evalItems.get(0);
            }
        }
        return evalItem;
    }

    public void saveItem(EvalItem item, String userId) {
        LOG.debug("item:" + item.getId() + ", userId:" + userId);

        // set the date modified
        item.setLastModified(new Date());

        // check for required fields first
        if (EvalUtils.isBlank(item.getItemText())) {
            throw new BlankRequiredFieldException("Cannot save an item with a blank text", "itemText");
        }

        // validates the item based on the classification
        TemplateItemUtils.validateItemByClassification(item);

        // check the sharing constants
        EvalUtils.validateSharingConstant(item.getSharing());
        if (EvalConstants.SHARING_OWNER.equals(item.getSharing())) {
            throw new IllegalArgumentException("Invalid sharing constant (" + item.getSharing() + ") set for item ("
                    + item.getItemText() + "), cannot use SHARING_OWNER");
        } else if (EvalConstants.SHARING_PUBLIC.equals(item.getSharing())) {
            // test if non-admin trying to set public sharing
            if (!commonLogic.isUserAdmin(userId)) {
                throw new IllegalArgumentException(
                        "Only admins can set item (" + item.getItemText() + ") sharing to public");
            }
        }

        boolean newItem = true;
        if (item.getId() != null) {
            newItem = false;
            // existing item, don't allow change to locked setting
            // TODO this does not work, it just returns the same object that is already loaded
            EvalItem existingItem = getItemOrFail(item.getId());

            if (!existingItem.getLocked().equals(item.getLocked())) {
                throw new IllegalArgumentException(
                        "Cannot change locked setting on existing item (" + item.getId() + ")");
            }
        }

        if (securityChecks.checkUserControlItem(userId, item)) {
            // fill in the default settings for optional unspecified values
            if (item.getLocked() == null) {
                item.setLocked(Boolean.FALSE);
            }

            // check the NOT_AVAILABLE_ALLOWED system setting
            Boolean naAllowed = (Boolean) settings.get(EvalSettings.ENABLE_NOT_AVAILABLE);
            if (naAllowed) {
                // can set NA
                if (item.getUsesNA() == null) {
                    item.setUsesNA(Boolean.FALSE);
                }
            } else {
                item.setUsesNA(Boolean.FALSE);
            }

            if (item.getCategory() == null) {
                item.setCategory(EvalConstants.ITEM_CATEGORY_COURSE);
            }

            if (item.isCompulsory() == null) {
                item.setCompulsory(false);
            }

            // cleanup for XSS scripting and strings
            item.setItemText(commonLogic.cleanupUserStrings(item.getItemText()));
            item.setDescription(commonLogic.cleanupUserStrings(item.getDescription()));
            item.setExpertDescription(commonLogic.cleanupUserStrings(item.getExpertDescription()));

            // save the item
            dao.save(item);

            // ensure expert items have a category set
            if (EvalUtils.safeBool(item.getExpert())) {
                EvalItemGroup eig;
                if ((item.getItemGroupId() == null) || (item.getItemGroupId() == 0)) {
                    eig = dao.findOneBySearch(EvalItemGroup.class,
                            new Search("title", EvalConstants.EXPERT_ITEM_CATEGORY_TITLE));
                } else {
                    eig = getItemGroupById(item.getItemGroupId());
                }
                if (eig != null) {
                    Set<EvalItem> items = eig.getGroupItems();
                    if (items == null) {
                        items = new HashSet<>();
                        eig.setGroupItems(items);
                    }
                    if (!items.contains(item)) {
                        items.add(item);
                        dao.save(eig);
                        LOG.debug("Added expert item (" + item.getId() + ") to default item group: "
                                + EvalConstants.EXPERT_ITEM_CATEGORY_TITLE);
                    }
                }
            }

            if (newItem) {
                commonLogic.registerEntityEvent(EVENT_ITEM_CREATE, item);
            } else {
                commonLogic.registerEntityEvent(EVENT_ITEM_UPDATE, item);
            }
            LOG.debug("User (" + userId + ") saved item (" + item.getId() + "), title: " + item.getItemText());

            if (item.getLocked() == true && item.getScale() != null) {
                // lock associated scale
                LOG.debug("Locking scale (" + item.getScale().getTitle() + ") associated with new item ("
                        + item.getId() + ")");
                dao.lockScale(item.getScale(), Boolean.FALSE);
            }

            return;
        }

        // should not get here so die if we do
        throw new RuntimeException(
                "User (" + userId + ") could NOT save item (" + item.getId() + "), title: " + item.getItemText());
    }

    public void deleteItem(Long itemId, String userId) {
        LOG.debug("itemId:" + itemId + ", userId:" + userId);

        // get the item by id
        EvalItem item = getItemOrFail(itemId);

        // ADMIN USER CAN REMOVE EXPERT ITEMS -AZ
        //    // cannot remove expert items
        //    if (item.getExpert().booleanValue() == true) {
        //    throw new IllegalStateException("Cannot remove expert item ("+itemId+")");
        //    }

        if (securityChecks.checkUserControlItem(userId, item)) {

            EvalScale scale = item.getScale(); // LAZY LOAD
            String itemClassification = item.getClassification();
            dao.delete(item);
            commonLogic.registerEntityEvent(EVENT_ITEM_DELETE, item);
            LOG.debug("User (" + userId + ") removed item (" + item.getId() + "), title: " + item.getItemText());

            // unlock associated scales if there were any
            if (item.getLocked() && scale != null) {
                LOG.debug(
                        "Unlocking associated scale (" + scale.getTitle() + ") for removed item (" + itemId + ")");
                dao.lockScale(scale, Boolean.FALSE);
            }

            // now we remove the scale if this is MC or MA
            if (EvalConstants.ITEM_TYPE_MULTIPLEANSWER.equals(itemClassification)
                    || EvalConstants.ITEM_TYPE_MULTIPLECHOICE.equals(itemClassification)) {
                dao.delete(scale); // NOTE: does not use the main scale removal method since these are not really scales
            }

            return;
        }

        // should not get here so die if we do
        throw new RuntimeException("User (" + userId + ") could NOT delete item (" + item.getId() + ")");
    }

    public List<EvalItem> getItemsForUser(String userId, String sharingConstant, String filter,
            boolean includeExpert) {
        LOG.debug("sharingConstant:" + sharingConstant + ", userId:" + userId + ", filter:" + filter
                + ", includeExpert:" + includeExpert);

        if (userId == null) {
            throw new IllegalArgumentException("Must include a userId");
        }

        // admin always gets all of the templates of a type
        if (commonLogic.isUserAdmin(userId)) {
            userId = null;
        }

        String[] sharingConstants = makeSharingConstantsArray(sharingConstant);

        // leave out the block parent items
        String[] props = new String[] { "classification" };
        Object[] values = new Object[] { EvalConstants.ITEM_TYPE_BLOCK_PARENT };
        int[] comparisons = new int[] { Restriction.NOT_EQUALS };

        if (!includeExpert) {
            props = ArrayUtils.appendArray(props, "expert");
            values = ArrayUtils.appendArray(values, Boolean.TRUE);
            comparisons = ArrayUtils.appendArray(comparisons, Restriction.NOT_EQUALS);
        }

        if (filter != null && filter.length() > 0) {
            props = ArrayUtils.appendArray(props, "itemText");
            values = ArrayUtils.appendArray(values, "%" + filter + "%");
            comparisons = ArrayUtils.appendArray(comparisons, Restriction.LIKE);
        }

        String[] order = new String[] { "id" };
        String[] options = new String[] { "notHidden" };

        return dao.getSharedEntitiesForUser(EvalItem.class, userId, sharingConstants, props, values, comparisons,
                order, options, 0, 0);
    }

    public List<EvalItem> getItemsForTemplate(Long templateId, String userId) {
        LOG.debug("templateId:" + templateId + ", userId:" + userId);

        // TODO make this limit the items based on the user, currently it gets all items

        List<EvalItem> l = new ArrayList<>();
        List<EvalTemplateItem> etis = getTemplateItemsForTemplate(templateId, new String[] {}, new String[] {},
                new String[] {});
        for (EvalTemplateItem evalTemplateItem : etis) {
            l.add(evalTemplateItem.getItem());
        }
        return l;
    }

    public EvalTemplateItem getTemplateItemById(Long templateItemId) {
        LOG.debug("templateItemId:" + templateItemId);
        EvalTemplateItem templateItem = (EvalTemplateItem) dao.findById(EvalTemplateItem.class, templateItemId);
        if (templateItem != null) {
            if (TemplateItemUtils.isBlockParent(templateItem)) {
                // get the children
                List<EvalTemplateItem> children = getBlockChildTemplateItemsForBlockParent(templateItem.getId(),
                        false);
                templateItem.childTemplateItems = children;
            }
        }
        return templateItem;
    }

    public EvalTemplateItem getTemplateItemByEid(String eid) {
        LOG.debug("templateItemEid:" + eid);
        EvalTemplateItem evalTemplateItem = null;
        if (eid != null) {
            List<EvalTemplateItem> evalTemplateItems = dao.findBySearch(EvalTemplateItem.class,
                    new Search("eid", eid));
            if (evalTemplateItems != null && evalTemplateItems.size() == 1) {
                evalTemplateItem = evalTemplateItems.get(0);
            }
        }
        return evalTemplateItem;
    }

    @SuppressWarnings("unchecked")
    public void saveTemplateItem(EvalTemplateItem templateItem, String userId) {
        LOG.debug("templateItem:" + templateItem.getId() + ", userId:" + userId);

        // set the date modified
        templateItem.setLastModified(new Date());

        // get item and check it
        EvalItem item = templateItem.getItem();
        if (item == null) {
            throw new IllegalArgumentException("Item cannot be null");
        } else if (item.getId() == null) {
            throw new IllegalArgumentException("Item (" + item.getItemText() + ") must already be saved");
        }

        // validate the fields of this template item based on the classification of the contained item
        TemplateItemUtils.validateTemplateItemByClassification(templateItem);

        // get template and check it
        EvalTemplate template = templateItem.getTemplate();
        if (template == null) {
            throw new IllegalArgumentException("Template cannot be null");
        } else if (template.getId() == null) {
            throw new IllegalArgumentException("Template (" + template.getTitle() + ") must already be saved");
        }

        // check the template lock state and do not allow saves when template is locked
        if (template.getLocked()) {
            throw new IllegalStateException("This template (" + template.getId()
                    + ") is locked, templateItems and items cannot be changed");
        }

        // get the template items count to set display order for new templateItems
        int itemsCount = getItemCountForTemplate(template.getId());
        if (templateItem.getId() == null) {
            if (TemplateItemUtils.isBlockParent(templateItem) && templateItem.getDisplayOrder() != null) {
                // if this a block parent then we allow the display order to be set
            } else {
                // new item
                templateItem.setDisplayOrder(itemsCount + 1);
            }
        } else {
            // existing item
            if (templateItem.getDisplayOrder() == null) {
                //put items without display ids at the bottom of the template
                templateItem.setDisplayOrder(itemsCount + 2);
            }
            // TODO - check if the display orders are set to a value that is used already?
        }

        fixUpTemplateItem(templateItem);

        if (securityChecks.checkUserControlTemplateItem(userId, templateItem)) {

            if (templateItem.getId() == null) {
                // if this is a new templateItem then associate it with 
                // the existing item and template and save all together
                Set[] entitySets = new HashSet[3];

                Set tiSet = new HashSet();
                tiSet.add(templateItem);
                entitySets[0] = tiSet;

                if (item.getTemplateItems() == null) {
                    item.setTemplateItems(new HashSet());
                }
                item.getTemplateItems().add(templateItem);
                Set itemSet = new HashSet();
                itemSet.add(item);
                entitySets[1] = itemSet;

                if (template.getTemplateItems() == null) {
                    template.setTemplateItems(new HashSet());
                }
                template.getTemplateItems().add(templateItem);
                Set templateSet = new HashSet();
                templateSet.add(template);
                entitySets[2] = templateSet;

                dao.saveMixedSet(entitySets);
                commonLogic.registerEntityEvent(EVENT_TEMPLATEITEM_CREATE, templateItem);
            } else {
                // existing item so just save it
                // TODO - make sure the item and template do not change for existing templateItems

                dao.save(templateItem);
                commonLogic.registerEntityEvent(EVENT_TEMPLATEITEM_UPDATE, templateItem);
            }

            // Should not be locking this here -AZ
            //       // lock related item and associated scales
            //       log.debug("Locking item ("+item.getId()+") and associated scale");
            //       dao.lockItem(item, Boolean.TRUE);

            LOG.debug("User (" + userId + ") saved templateItem (" + templateItem.getId() + "), " + "linked item ("
                    + item.getId() + ") and template (" + template.getId() + ")");
            return;
        }

        // should not get here so die if we do
        throw new RuntimeException(
                "User (" + userId + ") could NOT save templateItem (" + templateItem.getId() + ")");
    }

    /**
     * Fixes up a templateItem before saving to ensure it is valid
     * 
     * @param templateItem
     */
    private void fixUpTemplateItem(EvalTemplateItem templateItem) {
        // set the default values for unspecified optional values
        EvalItem item = templateItem.getItem();
        if (templateItem.getCategory() == null) {
            if (item.getCategory() == null) {
                templateItem.setCategory(EvalConstants.ITEM_CATEGORY_COURSE);
            } else {
                templateItem.setCategory(item.getCategory());
            }
        }
        Boolean useResultSharing = (Boolean) settings.get(EvalSettings.ITEM_USE_RESULTS_SHARING);
        if (templateItem.getResultsSharing() == null) {
            if (useResultSharing) {
                templateItem.setResultsSharing(EvalConstants.SHARING_ADMIN);
            } else {
                templateItem.setResultsSharing(EvalConstants.SHARING_PUBLIC);
            }
        }
        Boolean naAllowed = (Boolean) settings.get(EvalSettings.ENABLE_NOT_AVAILABLE);
        if (naAllowed) {
            // can set NA
            if (templateItem.getUsesNA() == null) {
                templateItem.setUsesNA(item.getUsesNA() == null ? Boolean.FALSE : item.getUsesNA());
            }
        } else {
            templateItem.setUsesNA(Boolean.FALSE);
        }
        Boolean usesComments = (Boolean) settings.get(EvalSettings.ENABLE_ITEM_COMMENTS);
        if (usesComments) {
            // can use comments
            if (templateItem.getUsesComment() == null) {
                templateItem.setUsesComment(item.getUsesComment() == null ? Boolean.FALSE : item.getUsesComment());
            }
        } else {
            templateItem.setUsesComment(Boolean.FALSE);
        }
        // defaults for hierarchy level of template items
        if (templateItem.getHierarchyLevel() == null) {
            templateItem.setHierarchyLevel(EvalConstants.HIERARCHY_LEVEL_TOP);
        }
        if (templateItem.getHierarchyNodeId() == null) {
            templateItem.setHierarchyNodeId(EvalConstants.HIERARCHY_NODE_ID_NONE);
        }
    }

    /**
     * Used for determining next available displayOrder
     * 
     * @param templateId unique id for a template
     * @return a count of the non-child items in the template
     */
    protected int getItemCountForTemplate(Long templateId) {
        // only count items which are not children of a block
        int itemsCount = (int) dao.countBySearch(EvalTemplateItem.class, new Search(new Restriction[] {
                new Restriction("template.id", templateId), new Restriction("blockId", "", Restriction.NULL) }));
        return itemsCount;
    }

    public int getNonBlockItemCountForTemplate(Long templateId) {
        return getItemCountForTemplate(templateId);
    }

    /**
     * Used for determining next available displayOrder for in a block
     * 
     * @param templateId unique id for a template
     * @param blockId id for a template block
     * @return a count of the non-child items in the template
     */
    protected int getItemCountForTemplateItem(Long templateId, Long blockId) {
        // only count items which are not children of a block
        int itemsCount = (int) dao.countBySearch(EvalTemplateItem.class, new Search(new Restriction[] {
                new Restriction("template.id", templateId), new Restriction("blockId", blockId) }));
        return itemsCount;
    }

    public int getItemCountForTemplateItemBlock(Long templateId, Long blockId) {
        return getItemCountForTemplateItem(templateId, blockId);
    }

    public void deleteTemplateItem(Long templateItemId, String userId) {
        LOG.debug("templateItemId:" + templateItemId + ", userId:" + userId);

        if (userId == null) {
            userId = commonLogic.getCurrentUserId();
        }

        // get the templateItem by id
        EvalTemplateItem templateItem = getTemplateItemOrFail(templateItemId);

        // check if this user is allowed to delete template items
        if (!securityChecks.checkUserControlTemplateItem(userId, templateItem)) {
            throw new SecurityException(
                    "User (" + userId + ") cannot delete this template item (" + templateItemId + ")");
        }

        // get a list of all template items in this template
        List<EvalTemplateItem> allTemplateItems = getTemplateItemsForTemplate(templateItem.getTemplate().getId(),
                new String[] {}, new String[] {}, new String[] {});
        // get the list of items without child items included
        List<EvalTemplateItem> noChildList = TemplateItemUtils.getNonChildItems(allTemplateItems);

        // now remove the item and correct the display order
        int orderAdjust = 0;
        int removedItemDisplayOrder;
        if (TemplateItemUtils.isBlockParent(templateItem)) {
            // remove the parent item and free up the child items into individual items if the block parent is removed
            removedItemDisplayOrder = templateItem.getDisplayOrder();
            List<EvalTemplateItem> childList = TemplateItemUtils.getChildItems(allTemplateItems,
                    templateItem.getId());
            orderAdjust = childList.size();

            // delete parent template item and item
            Long itemId = templateItem.getItem().getId();
            simpleDeleteTemplateItem(templateItem);

            // if this parent is used elsewhere then this will cause exception - EVALSYS-559
            if (isUsedItem(itemId)) {
                LOG.debug("Cannot remove block parent item (" + itemId + ") - item is in use elsewhere");
            } else {
                try {
                    deleteItem(itemId, userId);
                } catch (SecurityException e) {
                    // this is ok, just means we are not allowed to remove the related item
                }
            }

            // modify block children template items
            for (int i = 0; i < childList.size(); i++) {
                EvalTemplateItem child = (EvalTemplateItem) childList.get(i);
                child.setBlockParent(null);
                child.setBlockId(null);
                child.setDisplayOrder(removedItemDisplayOrder + i);
                saveTemplateItem(child, userId);
            }

        } else { // non-block cases
            removedItemDisplayOrder = templateItem.getDisplayOrder();
            simpleDeleteTemplateItem(templateItem);
        }

        // shift display-order of items below removed item
        for (int i = removedItemDisplayOrder; i < noChildList.size(); i++) {
            EvalTemplateItem ti = (EvalTemplateItem) noChildList.get(i);
            int order = ti.getDisplayOrder();
            if (order > removedItemDisplayOrder) {
                ti.setDisplayOrder(order + orderAdjust - 1);
                saveTemplateItem(ti, userId);
            }
        }

        // fire event
        commonLogic.registerEntityEvent(EVENT_TEMPLATEITEM_DELETE, templateItem);
        // log
        LOG.debug("Eval: User (" + userId + ") deleted eval template item (" + templateItem.getId() + ")");
    }

    /**
     * Just deletes the template item without messing around with checks which have already been done
     * @param templateItem a template item to remove
     */
    private void simpleDeleteTemplateItem(EvalTemplateItem templateItem) {
        if (templateItem != null) {
            Long itemId = templateItem.getItem().getId();
            EvalItem item = getItemById(itemId);
            // remove the templateItem and update all linkages
            dao.removeTemplateItems(new EvalTemplateItem[] { templateItem });
            // attempt to unlock the related item
            dao.lockItem(item, Boolean.FALSE);
        }
    }

    public List<EvalTemplateItem> getTemplateItemsForTemplate(Long templateId, String[] nodeIds,
            String[] instructorIds, String[] groupIds) {
        LOG.debug("templateId:" + templateId);
        if (templateId == null) {
            throw new IllegalArgumentException("template id cannot be null");
        }
        return dao.getTemplateItemsByTemplate(templateId, nodeIds, instructorIds, groupIds);
    }

    public List<EvalTemplateItem> getTemplateItemsForEvaluation(Long evalId, String[] nodeIds,
            String[] instructorIds, String[] groupIds) {
        LOG.debug("evalId:" + evalId);
        if (evalId == null) {
            throw new IllegalArgumentException("evaluation id cannot be null");
        }
        return dao.getTemplateItemsByEvaluation(evalId, nodeIds, instructorIds, groupIds);
    }

    public List<EvalTemplateItem> getBlockChildTemplateItemsForBlockParent(Long parentId, boolean includeParent) {

        // get the templateItem by id to verify parent exists
        EvalTemplateItem templateItem = (EvalTemplateItem) dao.findById(EvalTemplateItem.class, parentId);
        if (templateItem == null) {
            throw new IllegalArgumentException("Cannot find block parent templateItem with id: " + parentId);
        }

        if (templateItem.getBlockParent() == null || templateItem.getBlockParent() == false) {
            throw new IllegalArgumentException(
                    "Cannot request child block items for a templateItem which is not a block parent: "
                            + templateItem.getId());
        }

        List<EvalTemplateItem> l = new ArrayList<>();
        if (includeParent) {
            l.add(templateItem);
        }

        l.addAll(dao.findBySearch(EvalTemplateItem.class,
                new Search(new Restriction("blockId", parentId), new Order("displayOrder"))));

        return l;
    }

    public boolean canModifyItem(String userId, Long itemId) {
        LOG.debug("itemId:" + itemId + ", userId:" + userId);
        // get the item by id
        EvalItem item = getItemOrFail(itemId);

        // check perms and locked
        boolean allowed = false;
        try {
            allowed = securityChecks.checkUserControlItem(userId, item);
        } catch (RuntimeException e) {
            LOG.debug(e.getMessage());
        }
        return allowed;
    }

    public boolean canRemoveItem(String userId, Long itemId) {
        LOG.debug("itemId:" + itemId + ", userId:" + userId);
        // get the item by id
        EvalItem item = getItemOrFail(itemId);

        // can remove items that are in use now
        //      // cannot remove items that are in use
        //      if (dao.isUsedItem(itemId)) {
        //         log.debug("Cannot remove item ("+itemId+") which is used in at least one template");
        //         return false;
        //      }

        // check perms and locked
        boolean allowed = false;
        try {
            allowed = securityChecks.checkUserControlItem(userId, item);
        } catch (RuntimeException e) {
            LOG.debug(e.getMessage());
        }
        return allowed;
    }

    public boolean canControlTemplateItem(String userId, Long templateItemId) {
        LOG.debug("templateItemId:" + templateItemId + ", userId:" + userId);
        // get the template item by id
        EvalTemplateItem templateItem = getTemplateItemOrFail(templateItemId);

        // check perms and locked
        boolean allowed = false;
        try {
            allowed = securityChecks.checkUserControlTemplateItem(userId, templateItem);
        } catch (RuntimeException e) {
            LOG.debug(e.getMessage());
        }
        return allowed;
    }

    /**
     * Get an item or throw exception if it does not exist
     * @param itemId
     * @return
     */
    private EvalItem getItemOrFail(Long itemId) {
        EvalItem item = getItemById(itemId);
        if (item == null) {
            throw new IllegalArgumentException("Cannot find item with id: " + itemId);
        }
        return item;
    }

    /**
     * Get a template item or throw exception if it does not exist
     * @param templateItemId
     * @return
     */
    private EvalTemplateItem getTemplateItemOrFail(Long templateItemId) {
        EvalTemplateItem templateItem = getTemplateItemById(templateItemId);
        if (templateItem == null) {
            throw new IllegalArgumentException("Cannot find templateItem with id: " + templateItemId);
        }
        return templateItem;
    }

    // ITEM GROUPS

    public List<EvalItemGroup> getAllItemGroups(String userId, boolean includeExpert) {
        LOG.debug("userId:" + userId + ", includeExpert:" + includeExpert);

        List<EvalItemGroup> eig = new ArrayList<>();

        List<EvalItemGroup> catItemGroups = dao.getItemGroups(null, userId, true, includeExpert);
        int numItemGroups = catItemGroups.size();
        for (int i = 0; i < numItemGroups; i++) {
            EvalItemGroup evalItemGroup = (EvalItemGroup) catItemGroups.get(i);
            eig.add((EvalItemGroup) catItemGroups.get(i));
            List<EvalItemGroup> objItemGroups = dao.getItemGroups(evalItemGroup.getId(), userId, true,
                    includeExpert);

            for (int j = 0; j < objItemGroups.size(); j++) {
                eig.add((EvalItemGroup) objItemGroups.get(j));
            }

        }

        return eig;
    }

    public EvalItemGroup getItemGroupById(Long itemGroupId) {
        LOG.debug("itemGroupId:" + itemGroupId);
        EvalItemGroup ig = (EvalItemGroup) dao.findById(EvalItemGroup.class, itemGroupId);
        return ig;
    }

    public List<EvalItemGroup> getItemGroups(Long parentItemGroupId, String userId, boolean includeEmpty,
            boolean includeExpert) {
        LOG.debug("parentItemGroupId:" + parentItemGroupId + ", userId:" + userId + ", includeEmpty:" + includeEmpty
                + ", includeExpert:" + includeExpert);

        // check this parent is real
        if (parentItemGroupId != null) {
            getItemGroupOrFail(parentItemGroupId);
        }

        return dao.getItemGroups(parentItemGroupId, userId, includeEmpty, includeExpert);
    }

    public List<EvalItem> getItemsInItemGroup(Long itemGroupId, boolean expertOnly) {
        LOG.debug("parentItemGroupId:" + itemGroupId + ", expertOnly:" + expertOnly);

        // get the item group by id
        EvalItemGroup itemGroup = getItemGroupOrFail(itemGroupId);

        List<EvalItem> items = new ArrayList<>();
        if (itemGroup.getGroupItems() != null) {
            items = new ArrayList<>(itemGroup.getGroupItems()); // LAZY LOAD
            Collections.sort(items, new ComparatorsUtils.ItemComparatorById());
        }

        if (expertOnly) {
            // get rid of the non-expert items
            for (Iterator<EvalItem> iter = items.iterator(); iter.hasNext();) {
                EvalItem item = (EvalItem) iter.next();
                if (!item.getExpert()) {
                    iter.remove();
                }
            }
        }

        return items;
    }

    public Long getItemGroupIdByItemId(Long itemId, String userId) {
        LOG.debug("itemId:" + itemId);
        //select eig.* from eval_itemgroup eig, eval_item ei where ei.IG_ITEM_ID = eig.id and ei.id=8
        Long eigId = dao.getItemGroupIdByItemId(itemId, userId);
        return eigId;
    }

    public void saveItemGroup(EvalItemGroup itemGroup, String userId) {
        LOG.debug("itemGroup:" + itemGroup.getId() + ", userId:" + userId);

        // set the date modified
        itemGroup.setLastModified(new Date());

        // fill in the default settings for optional unspecified values
        if (itemGroup.getExpert() == null) {
            itemGroup.setExpert(Boolean.FALSE);
        }

        // check only admin can create expert item groups
        if (itemGroup.getExpert() && !commonLogic.isUserAdmin(userId)) {
            throw new IllegalArgumentException("Only admins can create expert item groups");
        }

        // check that the type is valid
        if (itemGroup.getType() == null) {
            throw new IllegalArgumentException("Item group type cannot be null");
        }
        if (itemGroup.getExpert() && !checkItemGroupType(itemGroup.getType())) {
            throw new IllegalArgumentException("Invalid item group type for expert group: " + itemGroup.getType());
        }

        // check that the parent is set correctly
        if (EvalConstants.ITEM_GROUP_TYPE_OBJECTIVE.equals(itemGroup.getType()) && itemGroup.getParent() == null) {
            throw new IllegalArgumentException(
                    "Cannot have a null parent for an objective type item group: " + itemGroup.getType());
        } else if (EvalConstants.ITEM_GROUP_TYPE_CATEGORY.equals(itemGroup.getType())
                && itemGroup.getParent() != null) {
            throw new IllegalArgumentException(
                    "Cannot have a parent for a category type item group: " + itemGroup.getType());
        }

        // check user can create or update item group
        if (securityChecks.checkUserControlItemGroup(userId, itemGroup)) {
            dao.save(itemGroup);

            LOG.debug("User (" + userId + ") saved itemGroup (" + itemGroup.getId() + "), " + " of type ("
                    + itemGroup.getType() + ")");
            return;
        }

        // should not get here so die if we do
        throw new RuntimeException("User (" + userId + ") could NOT save itemGroup (" + itemGroup.getId() + ")");
    }

    public void removeItemGroup(Long itemGroupId, String userId, boolean removeNonEmptyGroup) {
        LOG.debug("itemGroupId:" + itemGroupId + ", userId:" + userId + ", removeNonEmptyGroup:"
                + removeNonEmptyGroup);

        // get the item by id
        EvalItemGroup itemGroup = getItemGroupOrFail(itemGroupId);

        // check user can create or update item group
        if (securityChecks.checkUserControlItemGroup(userId, itemGroup)) {

            if (!removeNonEmptyGroup) {
                // not empty cannot be removed
                List<EvalItemGroup> l = dao.getItemGroups(itemGroup.getId(), userId, true, true);
                if (l.size() > 0) {
                    throw new IllegalStateException("Cannot remove non-empty item group: " + itemGroupId);
                }
            }

            dao.delete(itemGroup);

            LOG.debug("User (" + userId + ") removed itemGroup (" + itemGroup.getId() + "), " + " of type ("
                    + itemGroup.getType() + ")");
            return;
        }

        // should not get here so die if we do
        throw new RuntimeException("User (" + userId + ") could NOT remove itemGroup (" + itemGroup.getId() + ")");
    }

    // PERMISSIONS

    public boolean canUpdateItemGroup(String userId, Long itemGroupId) {
        LOG.debug("itemGroupId:" + itemGroupId + ", userId:" + userId);

        EvalItemGroup itemGroup = getItemGroupOrFail(itemGroupId);

        // check perms
        boolean allowed = false;
        try {
            allowed = securityChecks.checkUserControlItemGroup(userId, itemGroup);
        } catch (RuntimeException e) {
            LOG.debug(e.getMessage());
        }
        return allowed;
    }

    public boolean canRemoveItemGroup(String userId, Long itemGroupId) {
        LOG.debug("itemGroupId:" + itemGroupId + ", userId:" + userId);

        EvalItemGroup itemGroup = getItemGroupOrFail(itemGroupId);

        // check perms
        boolean allowed = false;
        try {
            allowed = securityChecks.checkUserControlItemGroup(userId, itemGroup);
        } catch (RuntimeException e) {
            LOG.debug(e.getMessage());
        }
        return allowed;
    }

    /**
     * Check if an item group type is valid
     * @param itemGroupTypeConstant
     * @return true if valid, false otherwise
     */
    public static boolean checkItemGroupType(String itemGroupTypeConstant) {
        return EvalConstants.ITEM_GROUP_TYPE_CATEGORY.equals(itemGroupTypeConstant)
                || EvalConstants.ITEM_GROUP_TYPE_OBJECTIVE.equals(itemGroupTypeConstant);
    }

    /**
     * Get the itemGroup or throw exception if not found
     * @param itemGroupId
     * @return
     */
    private EvalItemGroup getItemGroupOrFail(Long itemGroupId) {
        EvalItemGroup itemGroup = getItemGroupById(itemGroupId);
        if (itemGroup == null) {
            throw new IllegalArgumentException("Cannot find parent itemGroup with id: " + itemGroupId);
        }
        return itemGroup;
    }

    // TEMPLATES

    public EvalTemplate getTemplateById(Long templateId) {
        LOG.debug("templateId: " + templateId);
        // get the template by id
        EvalTemplate template = (EvalTemplate) dao.findById(EvalTemplate.class, templateId);
        return template;
    }

    public EvalTemplate getTemplateByEid(String eid) {
        EvalTemplate evalTemplate = null;
        if (eid != null) {
            List<EvalTemplate> evalTemplates = dao.findBySearch(EvalTemplate.class, new Search("eid", eid));
            if (!evalTemplates.isEmpty()) {
                evalTemplate = evalTemplates.get(0);
            }
        }
        return evalTemplate;
    }

    public void saveTemplate(EvalTemplate template, String userId) {
        LOG.debug("template: " + template.getTitle() + ", userId: " + userId);

        boolean newTemplate = false;

        // set the date modified
        template.setLastModified(new Date());

        // check for required fields first
        if (EvalUtils.isBlank(template.getTitle())) {
            throw new BlankRequiredFieldException("Cannot save a template with a blank title", "title");
        }

        // check the sharing constants
        EvalUtils.validateSharingConstant(template.getSharing());
        if (EvalConstants.SHARING_OWNER.equals(template.getSharing())) {
            throw new IllegalArgumentException("Invalid sharing constant (" + template.getSharing()
                    + ") set for template (" + template.getTitle() + "), cannot use SHARING_OWNER");
        } else if (EvalConstants.SHARING_PUBLIC.equals(template.getSharing())) {
            // test if non-admin trying to set public sharing
            String system_sharing = (String) settings.get(EvalSettings.TEMPLATE_SHARING_AND_VISIBILITY);
            if (!EvalConstants.SHARING_PUBLIC.equals(system_sharing) && !commonLogic.isUserAdmin(userId)) {
                throw new IllegalArgumentException(
                        "Only admins can set template (" + template.getTitle() + ") sharing to public");
            }
        }

        if (template.getId() == null) {
            // new template
            newTemplate = true;
            if (!canCreateTemplate(userId)) {
                throw new SecurityException("User (" + userId + ") cannot create templates, invalid permissions");
            }
        } else {
            // does not work with hibernate
            //         // existing template, don't allow change to locked setting
            //         EvalTemplate existingTemplate = getTemplateOrFail(template.getId());
            //
            //         if (! existingTemplate.getLocked().equals(template.getLocked())) {
            //            throw new IllegalArgumentException("Cannot change locked setting on existing template (" + template.getId() + ")");
            //         }
        }

        if (securityChecks.checkUserControlTemplate(userId, template)) {
            // fill in any default values and nulls here
            if (template.getLocked() == null) {
                template.setLocked(Boolean.FALSE);
            }

            // cleanup for XSS scripting and strings
            template.setTitle(commonLogic.cleanupUserStrings(template.getTitle()));
            template.setDescription(commonLogic.cleanupUserStrings(template.getDescription()));
            template.setExpertDescription(commonLogic.cleanupUserStrings(template.getExpertDescription()));

            dao.save(template);
            LOG.debug("User (" + userId + ") saved template (" + template.getId() + "), title: "
                    + template.getTitle());

            if (newTemplate) {
                commonLogic.registerEntityEvent(EVENT_TEMPLATE_CREATE, template);
            } else {
                commonLogic.registerEntityEvent(EVENT_TEMPLATE_UPDATE, template);
            }

            // validate and save all related template items
            validateTemplateItemsForTemplate(template.getId());

            if (template.getLocked() == true) {
                // lock template and associated items
                LOG.debug("Locking template (" + template.getId() + ") and associated items");
                dao.lockTemplate(template, Boolean.TRUE);
            }

            return;
        }

        // should not get here so die if we do
        throw new RuntimeException("User (" + userId + ") could NOT save template (" + template.getId()
                + "), title: " + template.getTitle());
    }

    /**
     * Validates and saves all the template items related to this template
     * 
     * @param templateId the id of an {@link EvalTemplate}
     */
    private void validateTemplateItemsForTemplate(Long templateId) {
        List<EvalTemplateItem> templateItems = getTemplateItemsForTemplate(templateId, new String[] {},
                new String[] {}, new String[] {});
        if (templateItems.size() > 0) {
            for (EvalTemplateItem templateItem : templateItems) {
                TemplateItemUtils.validateTemplateItemByClassification(templateItem);
            }
            List<EvalTemplateItem> orderedItems = TemplateItemUtils.orderTemplateItems(templateItems, true);
            Set<EvalTemplateItem> s = new HashSet<>(orderedItems);
            dao.saveSet(s);
        }
    }

    public void deleteTemplate(Long templateId, String userId) {
        LOG.debug("templateId: " + templateId + ", userId: " + userId);
        // get the template by id
        EvalTemplate template = getTemplateOrFail(templateId);

        // cannot remove expert templates
        if (template.getExpert() == true) {
            throw new IllegalStateException("Cannot remove expert template (" + templateId + ")");
        }

        if (securityChecks.checkUserControlTemplate(userId, template)) {
            if (template.getLocked() == true) {
                // unlock template and associated items
                LOG.debug("Unlocking template (" + template.getId() + ") and associated items");
                dao.lockTemplate(template, Boolean.FALSE);
            }

            List<EvalTemplateItem> templateItems = getTemplateItemsForTemplate(template.getId(), new String[] {},
                    new String[] {}, new String[] {});
            // always remove templates with no items
            if (templateItems.size() > 0) {
                // first purge the template items (dis-associate all items automatically)
                EvalTemplateItem[] TIArray = templateItems.toArray(new EvalTemplateItem[templateItems.size()]);
                dao.removeTemplateItems(TIArray);

                // remove all unused children (items, and scales)
                Set<EvalItem> itemSet = new HashSet<>();
                Set<EvalScale> scaleSet = new HashSet<>();

                // loop through the TIs and fill the other sets with the persistent objects if they are unused
                for (EvalTemplateItem templateItem : templateItems) {
                    EvalItem item = templateItem.getItem(); // LAZY LOAD
                    if (!dao.isUsedItem(item.getId())) {
                        itemSet.add(item);
                        EvalScale scale = item.getScale(); // LAZY LOAD
                        if (scale != null) {
                            if (!dao.isUsedScale(scale.getId())) {
                                scaleSet.add(scale);
                            }
                        }
                    }
                }

                // remove items and then scales to avoid constraints
                dao.deleteSet(itemSet);
                dao.deleteSet(scaleSet);
            }

            dao.delete(template);
            // fire the template deleted event
            commonLogic.registerEntityEvent(EVENT_TEMPLATE_DELETE, template);
            return;
        }

        // should not get here so die if we do
        throw new RuntimeException("User (" + userId + ") could NOT delete template (" + templateId + ")");
    }

    public List<EvalTemplate> getTemplatesForUser(String userId, String sharingConstant, boolean includeEmpty) {
        LOG.debug("sharingConstant: " + sharingConstant + ", userId: " + userId);

        /*
         * TODO - Hierarchy
         * visible and shared sharing methods are meant to work by relating the hierarchy level of 
         * the owner with the sharing setting in the template, however, that was when 
         * we assumed there would only be one level per user. That is no longer anything 
         * we have control over (since we depend on data that comes from another API) 
         * so we will have to add in a table which will track the hierarchy levels and
         * link them to the template. This will be a very simple but necessary table.
         */

        if (userId == null) {
            throw new IllegalArgumentException("Must include a userId");
        }

        // admin always gets all of the templates of a type
        if (commonLogic.isUserAdmin(userId)) {
            userId = null;
        }

        String[] sharingConstants = makeSharingConstantsArray(sharingConstant);

        // only get type standard templates
        String[] props = new String[] { "type" };
        Object[] values = new Object[] { EvalConstants.TEMPLATE_TYPE_STANDARD };
        int[] comparisons = new int[] { Restriction.EQUALS };

        String[] order = new String[] { "sharing", "title" };
        String[] options;
        if (includeEmpty) {
            options = new String[] { "notHidden" };
        } else {
            options = new String[] { "notHidden", "notEmpty" };
        }

        return dao.getSharedEntitiesForUser(EvalTemplate.class, userId, sharingConstants, props, values,
                comparisons, order, options, 0, 0);
    }

    /**
     * Takes a single sharing constant and turns it into an array of sharing constants
     * based on the rule that null/owner means private and public
     * 
     * @param sharingConstant
     * @return array of sharing constants
     */
    private String[] makeSharingConstantsArray(String sharingConstant) {
        if (sharingConstant != null) {
            EvalUtils.validateSharingConstant(sharingConstant);
        }
        String[] sharingConstants = new String[] {};
        if (EvalConstants.SHARING_PRIVATE.equals(sharingConstant)) {
            // do private templates only
            sharingConstants = new String[] { EvalConstants.SHARING_PRIVATE };
        } else if (EvalConstants.SHARING_PUBLIC.equals(sharingConstant)) {
            // do public templates only
            sharingConstants = new String[] { EvalConstants.SHARING_PUBLIC };
        } else if (sharingConstant == null || EvalConstants.SHARING_OWNER.equals(sharingConstant)) {
            // do all templates visible to this user
            sharingConstants = new String[] { EvalConstants.SHARING_PRIVATE, EvalConstants.SHARING_PUBLIC };
        }
        return sharingConstants;
    }

    // PERMISSIONS

    public boolean canCreateTemplate(String userId) {
        LOG.debug("userId: " + userId);
        boolean allowed = false;
        if (commonLogic.isUserAdmin(userId)) {
            // the system super user can create templates always
            allowed = true;
        } else {
            /*
             * If the person is not an admin (super or any kind, currently we just have super admin) 
             * then system settings should be checked whether they can create templates 
             * or not - kahuja.
             * 
             * TODO - make this check system wide and not site/group specific - aaronz.
             */
            if (((Boolean) settings.get(EvalSettings.INSTRUCTOR_ALLOWED_CREATE_EVALUATIONS))
                    && commonLogic.countEvalGroupsForUser(userId, EvalConstants.PERM_WRITE_TEMPLATE) > 0) {
                allowed = true;
            }
        }
        return allowed;
    }

    public boolean canModifyTemplate(String userId, Long templateId) {
        LOG.debug("templateId: " + templateId + ", userId: " + userId);
        // get the template by id
        EvalTemplate template = getTemplateOrFail(templateId);

        // check perms and locked
        boolean allowed = false;
        try {
            allowed = securityChecks.checkUserControlTemplate(userId, template);
        } catch (RuntimeException e) {
            LOG.debug(e.getMessage());
        }
        return allowed;
    }

    public boolean canRemoveTemplate(String userId, Long templateId) {
        LOG.debug("templateId: " + templateId + ", userId: " + userId);
        // get the template by id
        EvalTemplate template = getTemplateOrFail(templateId);

        // can remove templates that are in use now since it should not happen
        //      // cannot remove templates that are in use
        //      if (dao.isUsedTemplate(templateId)) {
        //         log.debug("Cannot remove template ("+templateId+") which is used in at least one evaluation");
        //         return false;
        //      }

        // check perms and locked
        boolean allowed = false;
        try {
            allowed = securityChecks.checkUserControlTemplate(userId, template);
        } catch (RuntimeException e) {
            LOG.debug(e.getMessage());
        }
        return allowed;
    }

    /**
     * Get a template or throw exception
     * @param templateId
     * @return
     */
    private EvalTemplate getTemplateOrFail(Long templateId) {
        EvalTemplate template = getTemplateById(templateId);
        if (template == null) {
            throw new IllegalArgumentException("Cannot find template with id: " + templateId);
        }
        return template;
    }

    // COPYING

    /* (non-Javadoc)
     * @see org.sakaiproject.evaluation.logic.EvalAuthoringService#copyScales(java.lang.Long[], java.lang.String, java.lang.String, boolean)
     */
    public Long[] copyScales(Long[] scaleIds, String title, String ownerId, boolean hidden) {
        if (scaleIds == null || scaleIds.length == 0) {
            throw new IllegalArgumentException("Invalid scaleIds array, cannot be null or empty");
        }

        Set<EvalScale> copiedScales = copyScalesInternal(scaleIds, title, ownerId, hidden);

        Long[] copiedIds = new Long[copiedScales.size()];
        int counter = 0;
        for (EvalScale copiedScale : copiedScales) {
            copiedIds[counter] = copiedScale.getId();
            counter++;
        }
        return copiedIds;
    }

    /**
     * Internal method: allows us to get to the set of all copies
     * @return the set of copies of the scales
     */
    private Set<EvalScale> copyScalesInternal(Long[] scaleIds, String title, String ownerId, boolean hidden) {
        if (ownerId == null || ownerId.length() == 0) {
            throw new IllegalArgumentException("Invalid ownerId, cannot be null or empty string");
        }

        Set<EvalScale> copiedScales = new HashSet<>();
        if (scaleIds != null && scaleIds.length > 0) {
            scaleIds = ArrayUtils.unique(scaleIds);
            List<EvalScale> scales = dao.findBySearch(EvalScale.class, new Search("id", scaleIds));
            if (scales.size() != scaleIds.length) {
                throw new IllegalArgumentException("Invalid scaleIds in the scaleIds array: " + scaleIds);
            }

            for (EvalScale original : scales) {
                String newTitle = title;
                if (newTitle == null || newTitle.length() == 0) {
                    newTitle = original.getTitle() + " (copy)";
                }
                EvalScale copy = new EvalScale(ownerId, newTitle, original.getMode(), EvalConstants.SHARING_PRIVATE,
                        false, null, original.getIdeal(), new ArrayList<String>(original.getOptions()), false);
                copy.setCopyOf(original.getId());
                copy.setHidden(hidden);
                copiedScales.add(copy);
            }
            dao.saveSet(copiedScales);
        }
        return copiedScales;
    }

    /* (non-Javadoc)
     * @see org.sakaiproject.evaluation.logic.EvalAuthoringService#copyItems(java.lang.Long[], java.lang.String, boolean, boolean)
     */
    public Long[] copyItems(Long[] itemIds, String ownerId, boolean hidden, boolean includeChildren) {
        if (itemIds == null || itemIds.length == 0) {
            throw new IllegalArgumentException("Invalid itemIds array, cannot be null or empty");
        }

        Set<EvalItem> copiedItems = copyItemsInternal(itemIds, ownerId, hidden, includeChildren);

        Long[] copiedIds = new Long[copiedItems.size()];
        int counter = 0;
        for (EvalItem copiedItem : copiedItems) {
            copiedIds[counter] = copiedItem.getId();
            counter++;
        }
        return copiedIds;
    }

    /**
     * Internal method: allows us to get to the set of all copies
     * @return the set of copies of the items
     */
    private Set<EvalItem> copyItemsInternal(Long[] itemIds, String ownerId, boolean hidden,
            boolean includeChildren) {
        if (ownerId == null || ownerId.length() == 0) {
            throw new IllegalArgumentException("Invalid ownerId, cannot be null or empty string");
        }

        Set<EvalItem> copiedItems = new HashSet<>();
        if (itemIds != null && itemIds.length > 0) {
            itemIds = ArrayUtils.unique(itemIds);
            List<EvalItem> items = dao.findBySearch(EvalItem.class, new Search("id", itemIds));
            if (items.size() != itemIds.length) {
                throw new IllegalArgumentException("Invalid itemIds in array: " + itemIds);
            }

            for (EvalItem original : items) {
                EvalItem copy = new EvalItem(ownerId, original.getItemText(), original.getDescription(),
                        EvalConstants.SHARING_PRIVATE, original.getClassification(), false, null,
                        original.getScale(), null, original.getUsesNA(), original.getUsesComment(), false,
                        original.getDisplayRows(), original.getScaleDisplaySetting(), original.getCategory(),
                        false);
                // NOTE: no longer copying scales here - EVALSYS-689
                copy.setCopyOf(original.getId());
                copy.setHidden(hidden);
                copy.setCompulsory(original.isCompulsory());
                copiedItems.add(copy);
            }

            if (includeChildren) {
                // make a copy of all Scales and put them into the Items to replace the originals
                HashSet<Long> scaleIdSet = new HashSet<>();
                for (EvalItem item : copiedItems) {
                    if (item.getScale() != null) {
                        Long scaleId = item.getScale().getId();
                        scaleIdSet.add(scaleId);
                    }
                }
                Long[] scaleIds = scaleIdSet.toArray(new Long[scaleIdSet.size()]);
                // do the scales copy
                // https://bugs.caret.cam.ac.uk/browse/CTL-1531 - hide all the internal things which are copied (do not pass through the hidden variable)
                Set<EvalScale> copiedScales = copyScalesInternal(scaleIds, null, ownerId, true);
                HashMap<Long, EvalScale> originalIdToCopy = new HashMap<>(copiedItems.size());
                for (EvalScale scale : copiedScales) {
                    originalIdToCopy.put(scale.getCopyOf(), scale);
                }
                // insert the copied items into the copied template items (update the foreign keys when we save)
                for (EvalItem item : copiedItems) {
                    if (item.getScale() != null) {
                        Long scaleId = item.getScale().getId(); // original id
                        EvalScale copy = originalIdToCopy.get(scaleId);
                        if (copy != null) {
                            item.setScale(copy);
                        }
                    }
                }
            }
            dao.saveSet(copiedItems);
        }
        return copiedItems;
    }

    /* (non-Javadoc)
     * @see org.sakaiproject.evaluation.logic.EvalAuthoringService#copyTemplateItems(java.lang.Long[], java.lang.String, boolean, java.lang.Long, boolean)
     */
    public Long[] copyTemplateItems(Long[] templateItemIds, String ownerId, boolean hidden, Long toTemplateId,
            boolean includeChildren) {
        if (ownerId == null || ownerId.length() == 0) {
            throw new IllegalArgumentException("Invalid ownerId, cannot be null or empty string");
        }
        if (templateItemIds == null || templateItemIds.length == 0) {
            throw new IllegalArgumentException("Invalid templateItemIds array, cannot be null or empty");
        }

        templateItemIds = ArrayUtils.unique(templateItemIds);
        EvalTemplate toTemplate = null;
        if (toTemplateId != null) {
            toTemplate = getTemplateById(toTemplateId);
            if (toTemplate == null) {
                throw new IllegalArgumentException(
                        "Invalid toTemplateId, cannot find the template by this id: " + toTemplateId);
            }
        }

        List<EvalTemplateItem> templateItemsList = dao.findBySearch(EvalTemplateItem.class,
                new Search("id", templateItemIds));
        if (templateItemsList.size() != templateItemIds.length) {
            throw new IllegalArgumentException("Invalid templateItemIds in array: " + templateItemIds);
        }

        // now we check that copying into the originating template is correct and ensure the toTemplate is set
        if (toTemplate == null) {
            // all templateItems must be from the same template if this is the case
            for (EvalTemplateItem templateItem : templateItemsList) {
                Long templateId = templateItem.getTemplate().getId();
                if (toTemplate == null) {
                    toTemplate = getTemplateById(templateId);
                } else {
                    if (!toTemplate.getId().equals(templateId)) {
                        throw new IllegalArgumentException(
                                "All templateItems must be from the same template when doing a copy within a template, "
                                        + "if you want to copy templateItems from multiple templates into the same templates they are currently in you must "
                                        + "do it in batches where each set if from one template");
                    }
                }
            }
        }

        // sort the list of template items
        templateItemsList = TemplateItemUtils.orderTemplateItems(templateItemsList, false);

        int itemCount = 1; // start at display order 1
        if (toTemplateId == null && toTemplate != null) {
            // copying inside one template so start at the item count + 1
            // get the count of items in the destination template so we know where to start displayOrder from
            itemCount = getItemCountForTemplate(toTemplate.getId()) + 1;
        }

        /* http://bugs.sakaiproject.org/jira/browse/EVALSYS-689
         * need to track the copied items and scales to avoid copying them more than once
         */
        LinkedHashSet<EvalTemplateItem> copiedTemplateItems = new LinkedHashSet<>(templateItemsList.size());

        // shallow copy all block parents first so we can know their new IDs, then later we will update them
        List<EvalTemplateItem> parentItems = TemplateItemUtils.getParentItems(templateItemsList);
        HashMap<Long, EvalTemplateItem> parentIdToCopy = new HashMap<>(parentItems.size());
        if (!parentItems.isEmpty()) {
            for (EvalTemplateItem original : parentItems) {
                Long originalBlockParentId = original.getId();
                List<EvalTemplateItem> childItems = TemplateItemUtils.getChildItems(templateItemsList,
                        originalBlockParentId);
                if (childItems.size() > 0) {
                    // only copy this if it has children, lone parents do not get copied
                    EvalTemplateItem copy = copyTemplateItem(original, toTemplate, ownerId, hidden);
                    parentIdToCopy.put(originalBlockParentId, copy);
                }
            }
            HashSet<EvalTemplateItem> parentItemsToSave = new HashSet<>(parentIdToCopy.values());
            dao.saveSet(parentItemsToSave);
        }

        // check for block items
        List<EvalTemplateItem> nonChildItems = TemplateItemUtils.getNonChildItems(templateItemsList);

        // iterate though in display order and copy the template items
        int displayOrder = 0;
        for (EvalTemplateItem original : nonChildItems) {
            templateItemsList.remove(original); // take this out of the list
            if (TemplateItemUtils.isBlockParent(original)) {
                // this is a block parent so copy it and its children
                Long originalBlockParentId = original.getId();
                if (parentIdToCopy.containsKey(originalBlockParentId)) {
                    EvalTemplateItem copyParent = parentIdToCopy.get(originalBlockParentId);
                    copyParent.setDisplayOrder(itemCount + displayOrder); // fix up display order
                    copyParent.setBlockId(null);
                    copyParent.setBlockParent(true);
                    //dao.save(copyParent);
                    copiedTemplateItems.add(copyParent);
                    Long blockParentId = copyParent.getId();

                    // loop through and copy all the children and assign them to the parent
                    List<EvalTemplateItem> childItems = TemplateItemUtils.getChildItems(templateItemsList,
                            originalBlockParentId);
                    for (int j = 0; j < childItems.size(); j++) {
                        EvalTemplateItem child = childItems.get(j);
                        templateItemsList.remove(child); // take this out of the list
                        // copy the child item
                        EvalTemplateItem copy = copyTemplateItem(child, toTemplate, ownerId, hidden);
                        copy.setDisplayOrder(j); // fix up display order
                        copy.setBlockId(blockParentId);
                        copy.setBlockParent(false);
                        //dao.save(copy);
                        copiedTemplateItems.add(copy);
                    }
                }
            } else {
                // not a block parent
                EvalTemplateItem copy = copyTemplateItem(original, toTemplate, ownerId, hidden);
                copy.setDisplayOrder(itemCount + displayOrder); // fix up display order
                //dao.save(copy);
                copiedTemplateItems.add(copy);
            }
            displayOrder++;
        }

        // now copy any remaining orphaned block children into normal items
        for (EvalTemplateItem original : templateItemsList) {
            displayOrder++;
            EvalTemplateItem copy = copyTemplateItem(original, toTemplate, ownerId, hidden);
            copy.setDisplayOrder(itemCount + displayOrder); // fix up display order
            //dao.save(copy);
            copiedTemplateItems.add(copy);
        }

        if (includeChildren) {
            // make a copy of all items and put them into the TIs to replace the originals
            HashSet<Long> itemIdSet = new HashSet<>();
            for (EvalTemplateItem eti : copiedTemplateItems) {
                if (eti.getItem() != null) {
                    Long itemId = eti.getItem().getId();
                    itemIdSet.add(itemId);
                }
            }
            Long[] itemIds = itemIdSet.toArray(new Long[itemIdSet.size()]);
            // do the items copy
            Set<EvalItem> copiedItems = copyItemsInternal(itemIds, ownerId, hidden, includeChildren);
            HashMap<Long, EvalItem> originalIdToCopy = new HashMap<>(copiedItems.size());
            for (EvalItem evalItem : copiedItems) {
                originalIdToCopy.put(evalItem.getCopyOf(), evalItem);
            }
            // insert the copied items into the copied template items (update the foreign keys when we save)
            for (EvalTemplateItem eti : copiedTemplateItems) {
                if (eti.getItem() != null) {
                    Long itemId = eti.getItem().getId(); // original id
                    EvalItem copy = originalIdToCopy.get(itemId);
                    if (copy != null) {
                        eti.setItem(copy);
                    }
                }
            }
        }
        // save the template items
        dao.saveSet(copiedTemplateItems);

        Long[] copiedIds = new Long[copiedTemplateItems.size()];
        int counter = 0;
        for (EvalTemplateItem copiedTemplateItem : copiedTemplateItems) {
            copiedIds[counter] = copiedTemplateItem.getId();
            counter++;
        }
        return copiedIds;
    }

    /**
     * Makes a non-persistent copy of a templateItem, nulls out the block fields,
     * inherits the display order from the original
     * 
     * @param original the original item to copy
     * @param toTemplate the template to copy this templateItem to
     * @param ownerId set as the owner of this copy
     * @param hidden if true then the resulting copy will be marked as hidden 
     * @return the copy of the templateItem (not persisted)
     */
    private EvalTemplateItem copyTemplateItem(EvalTemplateItem original, EvalTemplate toTemplate, String ownerId,
            boolean hidden) {
        EvalTemplateItem copy = TemplateItemUtils.makeCopyOfTemplateItem(original, toTemplate, ownerId, hidden);
        fixUpTemplateItem(copy); // fix up to ensure fields are set correctly
        return copy;
    }

    public Long copyTemplate(Long templateId, String title, String ownerId, boolean hidden,
            boolean includeChildren) {
        if (ownerId == null || ownerId.length() == 0) {
            throw new IllegalArgumentException("Invalid ownerId, cannot be null or empty string");
        }
        if (templateId == null || templateId == 0) {
            throw new IllegalArgumentException("Invalid templateId, cannot be null or 0");
        }

        EvalTemplate original = getTemplateById(templateId);
        if (original == null) {
            throw new IllegalArgumentException(
                    "Invalid templateId submitted (" + templateId + "), could not retrieve the template");
        }

        String newTitle = title;
        if (newTitle == null || newTitle.length() == 0) {
            newTitle = original.getTitle() + " (copy)";
        }
        EvalTemplate copy = new EvalTemplate(ownerId, original.getType(), newTitle, original.getDescription(),
                EvalConstants.SHARING_PRIVATE, false, null, null, false, false);
        // set the other copy fields
        copy.setCopyOf(original.getId());
        copy.setHidden(hidden);

        dao.save(copy);

        if (original.getTemplateItems() != null && original.getTemplateItems().size() > 0) {
            // now copy the template items and save the new linkages
            Long[] originalTIIds = TemplateItemUtils.makeTemplateItemsIdsArray(original.getTemplateItems());
            // https://bugs.caret.cam.ac.uk/browse/CTL-1531 - hide all the internal things which are copied (do not pass through the hidden variable)
            Long[] templateItemIds = copyTemplateItems(originalTIIds, ownerId, true, copy.getId(), includeChildren);
            List<EvalTemplateItem> templateItemsList = dao.findBySearch(EvalTemplateItem.class,
                    new Search("id", templateItemIds));
            copy.setTemplateItems(new HashSet<>(templateItemsList));
            dao.save(copy);
        }

        return copy.getId();
    }

    public List<EvalItem> getItemsUsingScale(Long scaleId) {
        if (getScaleById(scaleId) == null) {
            throw new IllegalArgumentException("Invalid scaleId, no scale found with this id: " + scaleId);
        }
        List<EvalItem> items = dao.findBySearch(EvalItem.class, new Search("scale.id", scaleId));
        return items;
    }

    public List<EvalTemplate> getTemplatesUsingItem(Long itemId) {
        if (getItemById(itemId) == null) {
            throw new IllegalArgumentException("Invalid itemId, no item found with this id: " + itemId);
        }
        List<EvalTemplateItem> templateItems = dao.findBySearch(EvalTemplateItem.class,
                new Search("item.id", itemId));
        Set<Long> ids = new HashSet<>();
        for (EvalTemplateItem templateItem : templateItems) {
            ids.add(templateItem.getTemplate().getId());
        }
        List<EvalTemplate> templates = new ArrayList<>();
        for (Long templateId : ids) {
            templates.add(getTemplateById(templateId));
        }
        return templates;
    }

    /* (non-Javadoc)
     * @see org.sakaiproject.evaluation.logic.EvalAuthoringService#getAutoUseTemplateItems(java.lang.String, java.lang.String, java.lang.String)
     */
    public List<EvalTemplateItem> getAutoUseTemplateItems(String templateAutoUseTag, String templateItemAutoUseTag,
            String itemAutoUseTag) {

        List<EvalTemplateItem> items = new ArrayList<>();
        // first add in the templates items
        if (!EvalUtils.isBlank(templateAutoUseTag)) {
            List<EvalTemplate> templates = dao.findBySearch(EvalTemplate.class,
                    new Search(new Restriction("autoUseTag", templateAutoUseTag), new Order("id")));
            for (EvalTemplate template : templates) {
                List<EvalTemplateItem> templateItemsList = getTemplateItemsForTemplate(template.getId(),
                        new String[] {}, null, null); // only hierarchy nodes
                items.addAll(TemplateItemUtils.orderTemplateItems(templateItemsList, false));
            }
        }

        // now the template items (only if not already there)
        if (!EvalUtils.isBlank(templateItemAutoUseTag)) {
            List<EvalTemplateItem> templateItems = dao.findBySearch(EvalTemplateItem.class,
                    new Search(new Restriction[] { new Restriction("autoUseTag", templateItemAutoUseTag) },
                            new Order[] { new Order("displayOrder"), new Order("id") }));
            for (EvalTemplateItem templateItem : templateItems) {
                if (!items.contains(templateItem)) {
                    items.add(templateItem);
                }
            }
        }

        // finally put in the items wrapper in a templateItem
        if (!EvalUtils.isBlank(itemAutoUseTag)) {
            List<EvalItem> evalItems = dao.findBySearch(EvalItem.class,
                    new Search(new Restriction("autoUseTag", itemAutoUseTag), new Order("id")));
            for (EvalItem evalItem : evalItems) {
                items.add(TemplateItemUtils.makeTemplateItem(evalItem));
            }
        }

        return items;
    }

    /* (non-Javadoc)
     * @see org.sakaiproject.evaluation.logic.EvalAuthoringService#doAutoUseInsertion(java.lang.String, java.lang.Long, java.lang.String, boolean)
     */
    public List<EvalTemplateItem> doAutoUseInsertion(String autoUseTag, Long templateId,
            String insertionPointConstant, boolean saveAll) {
        List<EvalTemplateItem> allTemplateItems = null;
        // get all the autoUse items
        List<EvalTemplateItem> autoUseItems = getAutoUseTemplateItems(autoUseTag, autoUseTag, autoUseTag);
        if (autoUseItems.size() > 0) {
            LOG.debug("Found " + autoUseItems.size() + " autoUse items to insert for tag (" + autoUseTag
                    + ") into template (id=" + templateId + ")");
            allTemplateItems = new ArrayList<>();
            EvalTemplate template = getTemplateOrFail(templateId);
            String ownerId = template.getOwner();

            // get all current template items sorted
            List<EvalTemplateItem> currentItems;
            List<EvalTemplateItem> currentTemplateItems = TemplateItemUtils.orderTemplateItems(
                    getTemplateItemsForTemplate(templateId, new String[] {}, new String[] {}, new String[] {}),
                    false);
            if (saveAll) {
                currentItems = currentTemplateItems;
            } else {
                // make copies of all the current items as well since we are not saving them
                currentItems = new ArrayList<>();
                for (EvalTemplateItem original : currentTemplateItems) {
                    EvalTemplateItem copy = TemplateItemUtils.makeCopyOfTemplateItem(original, template,
                            template.getOwner(), true);
                    currentItems.add(copy);
                }
            }

            // copy and update all insertion items to have the correct value in the insertion field
            List<EvalTemplateItem> insertionItems = new ArrayList<>();
            Long[] copiedTIIds = new Long[0];
            if (saveAll) {
                // filter out the non-persistent TIs and make lists of all ids per template
                List<EvalTemplateItem> unsavedTIs = new ArrayList<>();
                Map<Long, List<Long>> templateToTIsMap = new HashMap<>();
                for (EvalTemplateItem templateItem : autoUseItems) {
                    Long templateItemId = templateItem.getId();
                    if (templateItem.getId() != null) {
                        Long currentTemplateId = templateItem.getTemplate().getId();
                        if (templateToTIsMap.containsKey(currentTemplateId)) {
                            templateToTIsMap.get(currentTemplateId).add(templateItemId);
                        } else {
                            List<Long> templateTIIds = new ArrayList<>();
                            templateTIIds.add(templateItemId);
                            templateToTIsMap.put(currentTemplateId, templateTIIds);
                        }
                    } else {
                        unsavedTIs.add(templateItem);
                    }
                }
                // use the copy method to make persistent copies of all template items and children
                for (Entry<Long, List<Long>> entry : templateToTIsMap.entrySet()) {
                    List<Long> TIIds = entry.getValue();
                    Long[] copiedIds = copyTemplateItems(TIIds.toArray(new Long[] {}), ownerId, true,
                            template.getId(), true);
                    copiedTIIds = ArrayUtils.appendArrays(copiedTIIds, copiedIds);
                }
                // fetch the new copies based on the ids
                List<EvalTemplateItem> templateItemsList = dao.findBySearch(EvalTemplateItem.class,
                        new Search("id", copiedTIIds));
                // now put the copied items into the list in the order of the copied ids
                for (Long id : copiedTIIds) {
                    for (EvalTemplateItem templateItem : templateItemsList) {
                        if (id.equals(templateItem.getId())) {
                            insertionItems.add(templateItem);
                            templateItemsList.remove(templateItem);
                            break;
                        }
                    }
                }
                // save all unsaved items
                for (EvalTemplateItem templateItem : unsavedTIs) {
                    templateItem.setTemplate(template);
                    templateItem.setOwner(ownerId);
                    saveTemplateItem(templateItem, ownerId);
                    insertionItems.add(templateItem);
                }
            } else {
                // just make a simple non-persistent copy of all the items
                for (EvalTemplateItem original : autoUseItems) {
                    EvalTemplateItem copy = TemplateItemUtils.makeCopyOfTemplateItem(original, template,
                            template.getOwner(), true);
                    // preserve the block data
                    copy.setBlockId(original.getBlockId());
                    copy.setBlockParent(original.getBlockParent());
                    // now add in the autouse data
                    copy.setAutoUseInsertionTag(autoUseTag);
                    insertionItems.add(copy);
                }
            }

            // set the autoUse insertion value
            for (EvalTemplateItem insertedTemplateItem : insertionItems) {
                insertedTemplateItem.setAutoUseInsertionTag(autoUseTag);
            }

            if (EvalConstants.EVALUATION_AUTOUSE_INSERTION_BEFORE.equals(insertionPointConstant)) {
                // inserting autoUse items before the existing ones
                allTemplateItems.addAll(insertionItems);
                allTemplateItems.addAll(currentItems);
            } else if (EvalConstants.EVALUATION_AUTOUSE_INSERTION_AFTER.equals(insertionPointConstant)) {
                // inserting autoUse items after the existing ones
                allTemplateItems.addAll(currentItems);
                allTemplateItems.addAll(insertionItems);
            } else {
                throw new IllegalArgumentException(
                        "Do not know how to handle autoUse insertion point of: " + insertionPointConstant);
            }

            // now we update the displayOrders to the current list order (which should be correct)
            int displayOrder = 1;
            for (EvalTemplateItem templateItem : allTemplateItems) {
                if (!TemplateItemUtils.isBlockChild(templateItem)) {
                    // only update the order of non-block children
                    templateItem.setDisplayOrder(displayOrder++);
                }
            }

            // save if set and then return the list of all copied items with corrected display order
            if (saveAll) {
                // save all the templateItems
                Set<EvalTemplateItem> allItems = new HashSet<>(allTemplateItems);
                dao.saveSet(allItems);
                // add the full list to the template and save it
                template.setTemplateItems(allItems);
                dao.save(template);
                LOG.debug("Saved and inserted " + autoUseItems.size() + " autoUse items for tag (" + autoUseTag
                        + ") into template (id=" + templateId + ")");
            }
        } else {
            LOG.debug("No autoUse items can be found to insert for tag: " + autoUseTag);
        }
        return allTemplateItems;
    }

    /**
     * @param itemId the unique id for an {@link EvalScale}
     * @return true if this scale is used in any items
     */
    public boolean isUsedItem(Long itemId) {
        return dao.isUsedItem(itemId);
    }

    /**
     * @param scaleId the unique id for an {@link EvalItem}
     * @return true if this item is used in any template (i.e. connected to any template item)
     */
    public boolean isUsedScale(Long scaleId) {
        return dao.isUsedScale(scaleId);
    }

    /**
     * @param templateId the unique id for an {@link EvalTemplate}
     * @return true if this template is used in any evalautions
     */
    public boolean isUsedTemplate(Long templateId) {
        return dao.isUsedTemplate(templateId);
    }

    /* (non-Javadoc)
     * @see org.sakaiproject.evaluation.logic.EvalAuthoringService#saveTemplateItemOrder(java.util.Map, java.lang.String)
     */
    public void saveTemplateItemOrder(Map<Long, Integer> orderingMap, String currentUserId) {
        /* TODO - warning! this method does not even check to see if these template items are in the same template
         * This is very dangerous and was written poorly by the original author, this should be rewritten
         */
        Iterator<Map.Entry<Long, Integer>> selector = orderingMap.entrySet().iterator();
        while (selector.hasNext()) {
            Map.Entry<Long, Integer> pairs = selector.next();
            Long templateItemId = pairs.getKey();
            Integer order = pairs.getValue();

            EvalTemplateItem templateItem = getTemplateItemById(templateItemId);
            templateItem.setDisplayOrder(order);

            saveTemplateItem(templateItem, currentUserId);

        }
    }

    public List<EvalTemplateItem> getCompulsoryTemplateItems(List<EvalTemplateItem> templateItemsList,
            EvalEvaluation evaluation) {
        List<EvalTemplateItem> compulsoryItemsList = new ArrayList<EvalTemplateItem>();

        List<EvalTemplateItem> orderedItems = TemplateItemUtils.orderTemplateItems(templateItemsList, false);

        for (int i = 0; i < orderedItems.size(); i++) {
            EvalTemplateItem templateItem = (EvalTemplateItem) orderedItems.get(i);
            if (!isCompulsory(templateItem, null)) {
                continue;
            }
            compulsoryItemsList.add(templateItem);
        }
        return compulsoryItemsList;
    }

    public boolean isCompulsory(EvalTemplateItem templateItem, EvalEvaluation evaluation) {
        boolean result = false;
        if (TemplateItemUtils.isAnswerable(templateItem)) {
            String type = TemplateItemUtils.getTemplateItemType(templateItem);
            if (EvalConstants.ITEM_TYPE_TEXT.equals(type)) {
                //check global setting w.r.t should we enforce compulsory text items
                Boolean textItemsCanBeRequired = (Boolean) settings.get(EvalSettings.ENABLE_TEXT_ITEM_REQUIRED);
                if (textItemsCanBeRequired == null && evaluation != null) {
                    //Global setting is CONFIGURABLE so consider eval-specific settings
                    result = EvalUtils.safeBool(evaluation.getCompulsoryTextItemsAllowed(), false)
                            && EvalUtils.safeBool(templateItem.isCompulsory());
                } else if (textItemsCanBeRequired != null && textItemsCanBeRequired
                        && EvalUtils.safeBool(templateItem.isCompulsory())) {
                    result = true;
                } else if (textItemsCanBeRequired == null && evaluation == null) {
                    result = true;
                }
            } else if (EvalUtils.safeBool(templateItem.isCompulsory())) {
                result = true;
            }
        }
        return result;
    }

}