/* * #%L * Alfresco Repository * %% * Copyright (C) 2005 - 2017 Alfresco Software Limited * %% * This file is part of the Alfresco software. * If the software was purchased under a paid Alfresco license, the terms of * the paid license agreement will prevail. Otherwise, the software is * provided under the following open source license terms: * * Alfresco is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * Alfresco is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with Alfresco. If not, see . * #L% */ package org.alfresco.repo.client.config; import org.alfresco.util.PropertyCheck; import org.apache.commons.lang.StringUtils; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.springframework.context.ApplicationEvent; import org.springframework.extensions.surf.util.AbstractLifecycleBean; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.Map; import java.util.Properties; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; /** * This class picks up all the loaded properties passed to it and uses a naming * convention to isolate the client's name and the related values. *

* The naming convention must confirm to the following: *

* repo.client-app.{@literal .} *

* Also, the client-name or property name ({@literal }) must not contain a dot {@literal ('.')} *

* Note also, that any property without a value is ignored and the client will not be registered * if all the properties of that client have no values. *

* So, if a new client (e.g. MyClientName) is required to send a shared-link email and the service or the API requires, * for example, sharedLinkBaseUrl and templateAssetsUrl properties, then the following * needs to be put into a properties file. *

* The default property file is alfresco/client/config/repo-clients-apps.properties which * could be overridden (or add new clients) by alfresco-global properties file. * * @author Jamal Kaabi-Mofrad */ public class ClientAppConfig extends AbstractLifecycleBean { private static final Log logger = LogFactory.getLog(ClientAppConfig.class); public static final String PREFIX = "repo.client-app."; public static final String PROP_TEMPLATE_ASSETS_URL = "templateAssetsUrl"; private Properties defaultProperties; private Properties globalProperties; private ConcurrentMap clients = new ConcurrentHashMap<>(); public ClientAppConfig() { } public void setDefaultProperties(Properties defaultProperties) { this.defaultProperties = defaultProperties; } public void setGlobalProperties(Properties globalProperties) { this.globalProperties = globalProperties; } public void init() { PropertyCheck.mandatory(this, "defaultProperties", defaultProperties); PropertyCheck.mandatory(this, "globalProperties", globalProperties); } /** * Returns an unmodifiable view of the clients map. Never null. */ public Map getClients() { return Collections.unmodifiableMap(clients); } /** * Returns the named client or null if no client exists with the given name. * * @param name the name of the client to retrieve */ public ClientApp getClient(String name) { return clients.get(name); } @Override protected void onBootstrap(ApplicationEvent event) { Map mergedProperties = getAndMergeProperties(); Set clientsNames = new HashSet<>(); Set propsNames = new HashSet<>(); processPropertyKeys(mergedProperties, clientsNames, propsNames); clients.putAll(processClients(clientsNames, propsNames, mergedProperties)); if (logger.isDebugEnabled()) { logger.debug("All bootstrapped repo clients apps: " + clients); } } @Override protected void onShutdown(ApplicationEvent event) { // nothing to do } /** * Processes the property's key and extracts the clients' names. * * @param allProps the merged properties * @param clientsNames a set of strings which will be populated with client names * @param propsNames a set of strings which will be populated with properties names (i.e.the property after the client name) */ protected void processPropertyKeys(Map allProps, Set clientsNames, Set propsNames) { for (String key : allProps.keySet()) { String propKey = key; if (propKey.startsWith(PREFIX)) { propKey = propKey.substring(PREFIX.length()); // Find the client name int clientNameControlDot = propKey.indexOf('.'); if (clientNameControlDot < 1) { logMalformedPropertyKey(key); continue; } int propNameLength = (propKey.length() - clientNameControlDot) - 1; // Length of characters between dots if (propNameLength < 1) { logMalformedPropertyKey(key); continue; } String clientName = propKey.substring(0, clientNameControlDot); String propName = propKey.substring((clientNameControlDot + 1)); // the property name (the property after the client name) must not contain a '.' if (propName.indexOf('.') == -1) { clientsNames.add(clientName); propsNames.add(propName); } else { logMalformedPropertyKey(key, "The property name " + propName + " must not contain a '.'"); } } else { logMalformedPropertyKey(propKey); } } } /** * Processes the given properties and if the properties' values are valid, creates * a map of {@code ClientApp} with the client's name as the key. * * @param clientsNames the processed clients' names * @param propsNames the processed properties names * @param allProps the merged properties * @return a map of {@code ClientApp} with the client's name as the key. */ protected Map processClients(Set clientsNames, Set propsNames, Map allProps) { Map clientApps = new HashMap<>(clientsNames.size()); for (String clientName : clientsNames) { Map config = new HashMap<>(); String templateAssetsUrl = null; for (String propName : propsNames) { String propKey = getPropertyKey(clientName, propName); if (!allProps.containsKey(propKey)) { // if the constructed property key does not exist, skip this iteration. continue; } String propValue = allProps.get(propKey); if (StringUtils.isEmpty(propValue)) { logInvalidPropertyValue(propKey, propValue); continue; } if (PROP_TEMPLATE_ASSETS_URL.equals(propName)) { templateAssetsUrl = propValue; } else { config.put(propName, propValue); } } if (StringUtils.isEmpty(templateAssetsUrl) && config.isEmpty()) { logger.warn("Client-app [" + clientName + "] can not be registered as it needs at least one property with a valid value."); continue; } // As the required values are valid, create the client data ClientApp client = new ClientApp(clientName, templateAssetsUrl, config); clientApps.put(clientName, client); } return clientApps; } /** * Converts and merges the given Java properties into a {@code java.util.Map}. */ protected Map getAndMergeProperties() { Map allProperties = new HashMap<>(); for (String propKey : defaultProperties.stringPropertyNames()) { allProperties.put(propKey, defaultProperties.getProperty(propKey)); } // Add new clients or override the default values from other properties files for (String propKey : globalProperties.stringPropertyNames()) { if (propKey.startsWith(PREFIX)) { String value = globalProperties.getProperty(propKey); // before overriding the key, validate the property value if (StringUtils.isEmpty(value)) { logInvalidPropertyValue(propKey, value); continue; } allProperties.put(propKey, value); } } return allProperties; } private void logMalformedPropertyKey(String propName, String reason) { reason = (StringUtils.isBlank(reason)) ? "" : " " + reason; logger.warn("Ignoring client app config (malformed property key) [" + propName + "]." + reason); } private void logMalformedPropertyKey(String propName) { logMalformedPropertyKey(propName, null); } private void logInvalidPropertyValue(String propName, String propValue) { if (logger.isDebugEnabled()) { logger.debug("Ignoring client app config (invalid value) [" + propValue + "] for the property:" + propName); } } private String getPropertyKey(String clientName, String clientProp) { return PREFIX + clientName + '.' + clientProp; } public static class ClientApp { private final String name; private final String templateAssetsUrl; private final Map properties; public ClientApp(String name, String templateAssetsUrl, Map properties) { this.name = name; this.templateAssetsUrl = templateAssetsUrl; this.properties = new HashMap<>(properties); } public String getName() { return name; } public String getTemplateAssetsUrl() { return templateAssetsUrl; } public Map getProperties() { return Collections.unmodifiableMap(properties); } public String getProperty(String propName) { return properties.get(propName); } @Override public boolean equals(Object o) { if (this == o) { return true; } if (!(o instanceof ClientApp)) { return false; } ClientApp clientApp = (ClientApp) o; return getName().equals(clientApp.getName()); } @Override public int hashCode() { return getName().hashCode(); } @Override public String toString() { final StringBuilder sb = new StringBuilder(250); sb.append("ClientApp [name=").append(name) .append(", templateAssetsUrl=").append(templateAssetsUrl) .append(", properties=").append(properties) .append(']'); return sb.toString(); } } }