org.slc.sli.dashboard.manager.component.impl.CustomizationAssemblyFactoryImpl.java Source code

Java tutorial

Introduction

Here is the source code for org.slc.sli.dashboard.manager.component.impl.CustomizationAssemblyFactoryImpl.java

Source

/*
 * Copyright 2012 Shared Learning Collaborative, LLC
 *
 * Licensed under the Apache 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.apache.org/licenses/LICENSE-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.slc.sli.dashboard.manager.component.impl;

import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;

import org.slc.sli.dashboard.entity.Config;
import org.slc.sli.dashboard.entity.Config.Item;
import org.slc.sli.dashboard.entity.Config.Type;
import org.slc.sli.dashboard.entity.EdOrgKey;
import org.slc.sli.dashboard.entity.GenericEntity;
import org.slc.sli.dashboard.entity.ModelAndViewConfig;
import org.slc.sli.dashboard.manager.ConfigManager;
import org.slc.sli.dashboard.manager.Manager;
import org.slc.sli.dashboard.manager.Manager.EntityMappingManager;
import org.slc.sli.dashboard.manager.UserEdOrgManager;
import org.slc.sli.dashboard.manager.component.CustomizationAssemblyFactory;
import org.slc.sli.dashboard.util.DashboardException;
import org.slc.sli.dashboard.util.ExecutionTimeLogger.LogExecutionTime;
import org.slc.sli.dashboard.util.SecurityUtil;

/**
 * Implementation of the CustomizationAssemblyFactory
 * @author agrebneva
 *
 */
public class CustomizationAssemblyFactoryImpl implements CustomizationAssemblyFactory, ApplicationContextAware {
    public static final Class<?>[] ENTITY_REFERENCE_METHOD_EXPECTED_SIGNATURE = new Class[] { String.class,
            Object.class, Config.Data.class };
    public static final String SUBSTITUTE_TOKEN_PATTERN = "\\$\\{([^}]+)\\}";
    private Logger logger = LoggerFactory.getLogger(getClass());
    private ApplicationContext applicationContext;
    private ConfigManager configManager;
    private UserEdOrgManager userEdOrgManager;
    private Map<String, InvokableSet> entityReferenceToManagerMethodMap;

    public void setConfigManager(ConfigManager configManager) {
        this.configManager = configManager;
    }

    public void setUserEdOrgManager(UserEdOrgManager userEdOrgManager) {
        this.userEdOrgManager = userEdOrgManager;
    }

    protected String getTokenId() {
        return SecurityUtil.getToken();
    }

    protected Config getConfig(String componentId) {
        EdOrgKey edOrg = userEdOrgManager.getUserEdOrg(getTokenId());
        if (edOrg == null) {
            throw new DashboardException(
                    "No data is available for you to view. Please contact your IT administrator.");
        }
        return configManager.getComponentConfig(getTokenId(), edOrg, componentId);
    }

    @Override
    public Collection<Config> getWidgetConfigs() {
        return configManager.getWidgetConfigs(getTokenId(), userEdOrgManager.getUserEdOrg(getTokenId()));
    }

    /**
     * Check declared condition against the entity
     * @param config - config for the component
     * @param entity - entity for the component
     * @return true if condition passes and false otherwise
     */
    @SuppressWarnings("unchecked")
    public final boolean checkCondition(Config parentConfig, Config config, GenericEntity entity) {
        if (config != null && config.getCondition() != null) {

            if (entity == null) {
                return true;
            }

            Config.Condition condition = config.getCondition();
            Object[] values = condition.getValue();
            // for simplicity always treat as an array
            List<GenericEntity> listOfEntities = (parentConfig != null && parentConfig.getRoot() != null)
                    ? entity.getList(parentConfig.getRoot())
                    : Arrays.asList(entity);
            Object childEntity;
            // condition is equivalent to exists in the list
            for (GenericEntity oneEntity : listOfEntities) {
                childEntity = getValue(oneEntity, condition.getField());
                // if null and value is null, it's allowed, otherwise it's not
                if (childEntity == null) {
                    return values.length == 0;
                }
                if (childEntity instanceof Number) {
                    double childNumber = ((Number) childEntity).doubleValue();
                    for (Object n : values) {
                        if (childNumber == ((Number) n).doubleValue()) {
                            return true;
                        }
                    }
                } else if (childEntity instanceof String) {
                    String childString = (String) childEntity;
                    for (Object n : values) {
                        if (childString.equalsIgnoreCase((String) n)) {
                            return true;
                        }
                    }
                } else {
                    throw new DashboardException(
                            "Unsupported data type for condition. Only allow string and numbers");
                }
            }
            return false;
        }
        return true;
    }

    /**
     * Get value from the entity model map where sub-entities are identified by a dot
     * "data.history.id"
     * @param entity
     * @param dataField
     * @return
     */
    private Object getValue(GenericEntity entity, String dataField) {
        String[] pathTokens = dataField.split("\\.");
        pathTokens = (pathTokens.length == 0) ? new String[] { dataField } : pathTokens;
        Object childEntity = entity;
        for (String token : pathTokens) {
            if (childEntity == null || !(childEntity instanceof GenericEntity)) {
                return null;
            }
            childEntity = ((GenericEntity) childEntity).get(token);
        }
        return childEntity;
    }

    /**
     * Traverse the config tree and populate the necessary entity and config objects
     * @param model - model to populate
     * @param componentId - current component to explore
     * @param entityKey - entityKey
     * @param parentEntity - parent entity
     * @param depth - depth of the recursion
     */
    private Config populateModelRecursively(ModelAndViewConfig model, String componentId, Object entityKey,
            Config.Item parentToComponentConfigRef, Config panelConfig, GenericEntity parentEntity, int depth,
            boolean lazyOverride) {
        if (depth > 5) {
            throw new DashboardException("The items hierarchy is too deep - only allow 5 elements");
        }
        Config config = parentToComponentConfigRef;
        GenericEntity entity = parentEntity;
        if (parentToComponentConfigRef == null || parentToComponentConfigRef.getType().hasOwnConfig()) {
            config = getConfig(componentId);
            if (config == null) {
                throw new DashboardException("Unable to find config for " + componentId + " and entity id "
                        + entityKey + ", config " + componentId);
            }
            Config.Data dataConfig = config.getData();
            if (dataConfig != null) {
                entity = model.getDataForAlias(dataConfig.getCacheKey());
                if ((!dataConfig.isLazy() || lazyOverride) && entity == null) {
                    entity = getDataComponent(componentId, entityKey, dataConfig);
                    model.addData(dataConfig.getCacheKey(), entity);
                }
            }
            if (!checkCondition(config, config, entity)) {
                return null;
            }
        }
        if (config.getItems() != null) {
            List<Config.Item> items = new ArrayList<Config.Item>();
            depth++;
            Config newConfig;
            Collection<Config.Item> expandedItems;
            // get items, go through all of them and update config as need according to conditions and template substitutions
            for (Config.Item item : getUpdatedDynamicHeaderTemplate(config, entity)) {
                if (checkCondition(config, item, entity)) {
                    newConfig = populateModelRecursively(model, item.getId(), entityKey, item, config, entity,
                            depth, lazyOverride);
                    if (newConfig != null) {
                        item = (Item) item.cloneWithItems(newConfig.getItems());
                    }
                    // if needs expansion, expand and add all columns, otherwise add the item
                    expandedItems = getExpandedColumns(item, entity);
                    if (expandedItems != null) {
                        items.addAll(expandedItems);
                    } else {
                        items.add(item);
                    }
                    if (config.getType().isLayoutItem()) {
                        model.addLayoutItem(newConfig);
                    }
                }
            }
            config = config.cloneWithItems(items.toArray(new Config.Item[0]));
        }
        if (componentId != null) {
            model.addConfig(componentId, config);
        }
        return config;
    }

    /**
     * Replace tokens in headers with values from entity's internal metadata
     * @param config
     * @param entity
     */
    protected Config.Item[] getUpdatedDynamicHeaderTemplate(Config config, GenericEntity entity) {
        if (entity != null) {
            Pattern p = Pattern.compile(SUBSTITUTE_TOKEN_PATTERN);
            Matcher matcher;
            String name, value;
            Collection<Config.Item> newItems = new ArrayList<Config.Item>();
            for (Config.Item item : config.getItems()) {
                name = item.getName();
                if (name != null) {
                    matcher = p.matcher(name);
                    while (matcher.find()) {
                        value = (String) getValue(entity, matcher.group(1));
                        if (value != null) {
                            name = name.replace(matcher.group(), value);
                        }
                    }
                    item = item.cloneWithName(name);
                }
                newItems.add(item);
            }
            return newItems.toArray(new Config.Item[0]);
        }
        return config.getItems();
    }

    /**
     * Dynamic column functionality which expands the columns by looking for an array in the entity that drives the expansion
     * Expand the columns if root attribute is present
     * @param config - config for field item
     * @param entity - entity for the component
     * @return expanded array
     */
    protected Collection<Config.Item> getExpandedColumns(Config.Item config, GenericEntity entity) {
        if (entity == null) {
            return null;
        }
        // if there is root for field, expand the columns
        if (config.getType() == Type.EXPAND && config.getRoot() != null) {
            @SuppressWarnings("unchecked")
            Collection<String> expandMapperList = entity.getList(config.getRoot());
            if (expandMapperList == null) {
                logger.error("Expand map is not available in the entity for config " + config);
                return null;
            }
            List<Config.Item> expandedItems = new ArrayList<Config.Item>();
            for (String lookupName : expandMapperList) {
                expandedItems.add(config.cloneWithParams(lookupName, lookupName));
            }
            return expandedItems;
        }
        return null;
    }

    @Override
    public ModelAndViewConfig getModelAndViewConfig(String componentId, Object entityKey) {
        return getModelAndViewConfig(componentId, entityKey, false);
    }

    @Override
    public ModelAndViewConfig getModelAndViewConfig(String componentId, Object entityKey, boolean lazyOverride) {
        ModelAndViewConfig modelAndViewConfig = new ModelAndViewConfig();
        populateModelRecursively(modelAndViewConfig, componentId, entityKey, null, null, null, 0, lazyOverride);
        modelAndViewConfig.setWidgetConfig(getWidgetConfigs());
        return modelAndViewConfig;
    }

    /**
     * Internal convenience class for method caching
     * @author agrebneva
     *
     */
    private class InvokableSet {
        Object manager;
        Method method;

        InvokableSet(Object manager, Method method) {
            this.manager = manager;
            this.method = method;
        }

        public Object getManager() {
            return manager;
        }

        public Method getMethod() {
            return method;
        }
    }

    private void populateEntityReferenceToManagerMethodMap() {
        Map<String, InvokableSet> entityReferenceToManagerMethodMap = new HashMap<String, InvokableSet>();

        boolean foundInterface = false;
        for (Object manager : applicationContext.getBeansWithAnnotation(EntityMappingManager.class).values()) {
            logger.info(manager.getClass().getCanonicalName());
            // managers can be advised (proxied) so original annotation are not seen on the method but
            // still available on the interface
            foundInterface = false;
            for (Class<?> type : manager.getClass().getInterfaces()) {
                if (type.isAnnotationPresent(EntityMappingManager.class)) {
                    foundInterface = true;
                    findEntityReferencesForType(entityReferenceToManagerMethodMap, type, manager);
                }
            }
            if (!foundInterface) {
                findEntityReferencesForType(entityReferenceToManagerMethodMap, manager.getClass(), manager);
            }
        }
        this.entityReferenceToManagerMethodMap = Collections.unmodifiableMap(entityReferenceToManagerMethodMap);
    }

    private final void findEntityReferencesForType(Map<String, InvokableSet> entityReferenceToManagerMethodMap,
            Class<?> type, Object instance) {
        Manager.EntityMapping entityMapping;
        for (Method m : type.getDeclaredMethods()) {
            entityMapping = m.getAnnotation(Manager.EntityMapping.class);
            if (entityMapping != null) {
                if (entityReferenceToManagerMethodMap.containsKey(entityMapping.value())) {
                    throw new DashboardException(
                            "Duplicate entity mapping references found for " + entityMapping.value() + ". Fix!!!");
                }
                if (!Arrays.equals(ENTITY_REFERENCE_METHOD_EXPECTED_SIGNATURE, m.getParameterTypes())) {
                    throw new DashboardException("Wrong signature for the method for " + entityMapping.value()
                            + ". Expected is " + Arrays.asList(ENTITY_REFERENCE_METHOD_EXPECTED_SIGNATURE) + "!!!");
                }
                entityReferenceToManagerMethodMap.put(entityMapping.value(), new InvokableSet(instance, m));
            }
        }
    }

    protected InvokableSet getInvokableSet(String entityRef) {
        return this.entityReferenceToManagerMethodMap.get(entityRef);
    }

    /**
     * For UTs
     * @param entityRef
     * @return
     */
    public boolean hasCachedEntityMapperReference(String entityRef) {
        return this.entityReferenceToManagerMethodMap.containsKey(entityRef);
    }

    @Override
    public GenericEntity getDataComponent(String componentId, Object entityKey) {
        return getDataComponent(componentId, entityKey, getConfig(componentId).getData());
    }

    @LogExecutionTime
    protected GenericEntity getDataComponent(String componentId, Object entityKey, Config.Data config) {
        if (config == null) {
            return null;
        }
        InvokableSet set = this.getInvokableSet(config.getEntityRef());
        if (set == null) {
            throw new DashboardException(
                    "No entity mapping references found for " + config.getEntityRef() + ". Fix!!!");
        }
        try {
            return (GenericEntity) set.getMethod().invoke(set.getManager(), getTokenId(), entityKey, config);
        } catch (Exception e) {
            logger.error("Unable to invoke population manager for " + componentId + " and entity id " + entityKey
                    + ", config " + componentId, e);
        }
        return null;
    }

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        this.applicationContext = applicationContext;
        populateEntityReferenceToManagerMethodMap();
    }
}