/* * 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.impl; import java.io.File; import java.io.FileReader; import java.lang.reflect.Method; import java.net.URL; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Set; import org.apache.commons.io.IOUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.cache.annotation.CacheEvict; import org.springframework.cache.annotation.Cacheable; import org.slc.sli.dashboard.client.APIClient; import org.slc.sli.dashboard.entity.Config; import org.slc.sli.dashboard.entity.ConfigMap; import org.slc.sli.dashboard.entity.EdOrgKey; import org.slc.sli.dashboard.entity.GenericEntity; import org.slc.sli.dashboard.manager.ApiClientManager; import org.slc.sli.dashboard.manager.ConfigManager; import org.slc.sli.dashboard.util.CacheableConfig; import org.slc.sli.dashboard.util.Constants; import org.slc.sli.dashboard.util.DashboardException; import org.slc.sli.dashboard.util.JsonConverter; /** * * ConfigManager allows other classes, such as controllers, to access and * persist view configurations. * Given a user, it will obtain view configuration at each level of the user's * hierarchy, and merge them into one set for the user. * * @author dwu */ public class ConfigManagerImpl extends ApiClientManager implements ConfigManager { private Logger logger = LoggerFactory.getLogger(getClass()); private String driverConfigLocation; private String userConfigLocation; private static final String LAYOUT_NAME = "layoutName"; private static final String LAYOUT = "layout"; private static final String DEFAULT = "default"; private static final String TYPE = "type"; public ConfigManagerImpl() { } /** * this method should be called by Spring Framework * set location of config file to be read. If the directory does not exist, * create it. * * @param configLocation * reading from properties file panel.config.driver.dir */ public void setDriverConfigLocation(String configLocation) { URL url = Config.class.getClassLoader().getResource(configLocation); if (url == null) { File f = new File(Config.class.getClassLoader().getResource("") + "/" + configLocation); f.mkdir(); this.driverConfigLocation = f.getAbsolutePath(); } else { this.driverConfigLocation = url.getPath(); } } /** * this method should be called by Spring Framework * set location of config file to be read. If the directory does not exist, * create it. * * @param configLocation * reading from properties file panel.config.custom.dir */ public void setUserConfigLocation(String configLocation) { if (!configLocation.startsWith("/")) { URL url = Config.class.getClassLoader().getResource(configLocation); if (url == null) { File f = new File(Config.class.getClassLoader().getResource("").getPath() + configLocation); f.mkdir(); configLocation = f.getAbsolutePath(); } else { configLocation = url.getPath(); } } this.userConfigLocation = configLocation; } /** * return the absolute file path of domain specific config file * * @param path * can be district ID name or state ID name * @param componentId * profile name * @return the absolute file path of domain specific config file */ public String getComponentConfigLocation(String path, String componentId) { return userConfigLocation + "/" + path + "/" + componentId + ".json"; } /** * return the absolute file path of default config file * * @param path * can be district ID name or state ID name * @param componentId * profile name * @return the absolute file path of default config file */ public String getDriverConfigLocation(String componentId) { return this.driverConfigLocation + "/" + componentId + ".json"; } /** * Find the lowest organization hierarchy config file. If the lowest * organization hierarchy * config file does not exist, it returns default (Driver) config file. * If the Driver config file does not exist, it is in a critical situation. * It will throw an * exception. * * @param apiCustomConfig * custom configuration uploaded by admininistrator. * @param customPath * abslute directory path where a config file exist. * @param componentId * name of the profile * @return proper Config to be used for the dashboard */ private Config getConfigByPath(Config customConfig, String componentId) { Config driverConfig = null; try { String driverId = componentId; // if custom config exist, read the config file if (customConfig != null) { driverId = customConfig.getParentId(); } // read Driver (default) config. File f = new File(getDriverConfigLocation(driverId)); driverConfig = loadConfig(f); if (customConfig != null) { return driverConfig.overWrite(customConfig); } return driverConfig; } catch (Exception ex) { logger.error("Unable to read config for " + componentId, ex); throw new DashboardException("Unable to read config for " + componentId); } } private Config loadConfig(File f) throws Exception { if (f.exists()) { FileReader fr = new FileReader(f); try { return JsonConverter.fromJson(fr, Config.class); } finally { IOUtils.closeQuietly(fr); } } return null; } @Override @CacheableConfig public Config getComponentConfig(String token, EdOrgKey edOrgKey, String componentId) { Config customComponentConfig = null; GenericEntity edOrg = null; GenericEntity parentEdOrg = null; EdOrgKey parentEdOrgKey = null; String id = edOrgKey.getSliId(); List<EdOrgKey> edOrgKeys = new ArrayList<EdOrgKey>(); edOrgKeys.add(edOrgKey); // keep reading EdOrg until it hits the top. APIClient apiClient = getApiClient(); do { if (apiClient != null) { edOrg = apiClient.getEducationalOrganization(token, id); if (edOrg != null) { parentEdOrg = apiClient.getParentEducationalOrganization(token, edOrg); if (parentEdOrg != null) { id = parentEdOrg.getId(); parentEdOrgKey = new EdOrgKey(id); edOrgKeys.add(parentEdOrgKey); } } else { // if edOrg is null, it means no parent edOrg either. parentEdOrg = null; } } } while (parentEdOrg != null); for (EdOrgKey key : edOrgKeys) { ConfigMap configMap = getCustomConfig(token, key); // if api has config if (configMap != null && !configMap.isEmpty()) { Config edOrgComponentConfig = configMap.getComponentConfig(componentId); if (edOrgComponentConfig != null) { if (customComponentConfig == null) { customComponentConfig = edOrgComponentConfig; } else { // edOrgComponentConfig overwrites customComponentConfig customComponentConfig = edOrgComponentConfig.overWrite(customComponentConfig); } } } } return getConfigByPath(customComponentConfig, componentId); } @Override @Cacheable(value = Constants.CACHE_USER_WIDGET_CONFIG) public Collection<Config> getWidgetConfigs(String token, EdOrgKey edOrgKey) { Map<String, String> attrs = new HashMap<String, String>(); attrs.put("type", Config.Type.WIDGET.toString()); return getConfigsByAttribute(token, edOrgKey, attrs); } @Override public Collection<Config> getConfigsByAttribute(String token, EdOrgKey edOrgKey, Map<String, String> attrs) { return getConfigsByAttribute(token, edOrgKey, attrs, true); } @Override public Collection<Config> getConfigsByAttribute(String token, EdOrgKey edOrgKey, Map<String, String> attrs, boolean overwriteCustomConfig) { // id to config map Map<String, Config> configs = new HashMap<String, Config>(); Config config; // list files in driver dir File driverConfigDir = new File(this.driverConfigLocation); File[] driverConfigFiles = driverConfigDir.listFiles(); if (driverConfigFiles == null) { logger.error("Unable to read config directory"); throw new DashboardException("Unable to read config directory!!!!"); } for (File f : driverConfigFiles) { try { config = loadConfig(f); } catch (Exception e) { logger.error("Unable to read config " + f.getName() + ". Skipping file", e); continue; } // check the config params. if they all match, add to the config map. boolean matchAll = true; for (String attrName : attrs.keySet()) { String methodName = ""; try { // use reflection to call the right config object method methodName = "get" + Character.toUpperCase(attrName.charAt(0)) + attrName.substring(1); Method method = config.getClass().getDeclaredMethod(methodName, new Class[] {}); Object ret = method.invoke(config, new Object[] {}); // compare the result to the desired result if (!(ret.toString().equals(attrs.get(attrName)))) { matchAll = false; break; } } catch (Exception e) { matchAll = false; logger.error("Error calling config method: " + methodName); } } // add to config map if (matchAll) { configs.put(config.getId(), config); } } // get custom configs if (overwriteCustomConfig) { for (String id : configs.keySet()) { configs.put(id, getComponentConfig(token, edOrgKey, id)); } } return configs.values(); } /** * Get the user's educational organization's custom configuration. * * @param token * The user's authentication token. * @return The education organization's custom configuration */ @Override public ConfigMap getCustomConfig(String token, EdOrgKey edOrgKey) { try { return getApiClient().getEdOrgCustomData(token, edOrgKey.getSliId()); } catch (Exception e) { // it's a valid scenario when there is no district specific config. Default will be used // in this case. logger.debug( "No district specific config is available, the default config will be used" ); return null; } } /** * Put or save the user's educational organization's custom configuration. * * @param token * The user's authentication token. * @param customConfigJson * The education organization's custom configuration JSON. */ @Override @CacheEvict(value = Constants.CACHE_USER_PANEL_CONFIG, allEntries = true) public void putCustomConfig(String token, EdOrgKey edOrgKey, ConfigMap configMap) { getApiClient().putEdOrgCustomData(token, edOrgKey.getSliId(), configMap); } /** * Save one custom configuration for an ed-org * */ @Override @CacheEvict(value = Constants.CACHE_USER_PANEL_CONFIG, allEntries = true) public void putCustomConfig(String token, EdOrgKey edOrgKey, Config config) { // get current custom config map from api ConfigMap configMap = getCustomConfig(token, edOrgKey); if (configMap == null) { configMap = new ConfigMap(); configMap.setConfig(new HashMap<String, Config>()); } // update with new config ConfigMap newConfigMap = configMap.cloneWithNewConfig(config); // write new config map getApiClient().putEdOrgCustomData(token, edOrgKey.getSliId(), newConfigMap); } /** * To find Config is PANEL types and belong to the layout. * * @param config * @param layoutName * @return */ private boolean doesBelongToLayout(Config config, String layoutName) { boolean belongConfig = true; // first filter by type. // Target TYPEs are PANEL, GRID, TREE, and REPEAT_HEADER_GRID Config.Type type = config.getType(); if (type != null && (type.equals(Config.Type.PANEL) || type.equals(Config.Type.GRID) || type.equals(Config.Type.TREE) || type .equals(Config.Type.REPEAT_HEADER_GRID))) { // if a client requests specific layout, // then filter layout. if (layoutName != null) { belongConfig = false; Map<String, Object> configParams = config.getParams(); if (configParams != null) { List<String> layouts = (List<String>) configParams.get(LAYOUT); if (layouts != null) { if (layouts.contains(layoutName)) { belongConfig = true; } } } } } else { belongConfig = false; } return belongConfig; } @Override public Map<String, Collection<Config>> getAllConfigByType(String token, EdOrgKey edOrgKey, Map<String, String> params) { Map<String, Collection<Config>> allConfigs = new HashMap<String, Collection<Config>>(); // configIdLookup is used to check parentId from Custom Config exists in Driver Config Set<String> configIdLookup = new HashSet<String>(); Map<String, String> attribute = new HashMap<String, String>(); String layoutName = params.get(LAYOUT_NAME); if (params.containsKey(TYPE)) { attribute.put(TYPE, params.get(TYPE)); } // get Driver Config by specified attribute Collection<Config> driverConfigs = getConfigsByAttribute(token, edOrgKey, attribute, false); // filter config by layout name // and // build lookup index Iterator<Config> configIterator = driverConfigs.iterator(); while (configIterator.hasNext()) { Config driverConfig = configIterator.next(); if (doesBelongToLayout(driverConfig, layoutName)) { configIdLookup.add(driverConfig.getId()); } else { configIterator.remove(); } } // add DriverConfig to a returning object allConfigs.put(DEFAULT, driverConfigs); // read edOrg custom config recursively APIClient apiClient = getApiClient(); while (edOrgKey != null) { // customConfigByType will be added to a returning object Collection<Config> customConfigByType = new LinkedList<Config>(); // get current edOrg custom config ConfigMap customConfigMap = getCustomConfig(token, edOrgKey); if (customConfigMap != null) { Map<String, Config> configByMap = customConfigMap.getConfig(); if (configByMap != null) { Collection<Config> customConfigs = configByMap.values(); if (customConfigs != null) { for (Config customConfig : customConfigs) { // if parentId from customConfig does not exist in DriverConfig, // then ignore. String parentId = customConfig.getParentId(); if (parentId != null && configIdLookup.contains(parentId)) { if (doesBelongToLayout(customConfig, layoutName)) { customConfigByType.add(customConfig); } } } } } } // find current EdOrg entity GenericEntity edOrg = apiClient.getEducationalOrganization(token, edOrgKey.getSliId()); List<String> organizationCategories = (List<String>) edOrg.get(Constants.ATTR_ORG_CATEGORIES); if (organizationCategories != null && !organizationCategories.isEmpty()) { for (String educationAgency : organizationCategories) { if (educationAgency != null) { allConfigs.put(educationAgency, customConfigByType); break; } } } // find parent EdOrg edOrgKey = null; edOrg = apiClient.getParentEducationalOrganization(token, edOrg); if (edOrg != null) { String id = edOrg.getId(); if (id != null && !id.isEmpty()) { edOrgKey = new EdOrgKey(id); } } } return allConfigs; } }