/*
* 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();
}
}