/* * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF licenses this file to You 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.apache.stanbol.enhancer.nlp.utils; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.Dictionary; import java.util.HashMap; import java.util.HashSet; import java.util.Map; import java.util.Set; import org.apache.commons.collections.map.CompositeMap; import org.apache.commons.collections.map.CompositeMap.MapMutator; import org.osgi.framework.ServiceReference; import org.osgi.service.cm.ConfigurationException; /** * Utility that supports the configuration of languages and language * specific parameters. * <h3>Language configuration</h3> * Languages are configured as follows: * <pre> * de,en </pre> * or * <pre> * !fr,!cn,*</pre> * The '<code>!{lang}</code>' is used to {@link #getExplicitlyExcluded() * explicitly exclude} an language. '<code>*</code>' can be used to * specify that all languages are allowed. '<code>{lang}</code>' * {@link #getExplicitlyIncluded() explicitly includes} a language. * '<code>,</code>' is used as separator between multiple configurations * however this class also supports the usage of <code>String[]</code> and * {@link Collection<?>} (in case of Collections the * {@link Object#toString()} method is used to obtain the configuration). * If an array or a collection is used for the configuration, than comma * is NOT used as separator! * <p> * <h3>Parameter Support</h3> * This class supports the parsing of language specific parameters by * the followng syntax * <pre> * {language};{param-name}={param-value};{param-name}={param-value}</pre> * Parameters that apply to all {languages} with no configuration can be * either set for the '<code>*</code>' or an empty language tag. Here * is an example * <pre> * *;myParam=myValue * ;myParam=myValue</pre> * Multiple default configurations will cause a {@link ConfigurationException}. * <p> * The {@link #getParameters(String)} and {@link #getParameters(String,String)} * will return values of the {@link #getDefaultParameters()} if no * language specific parameters are present for the requested language. However * the default configuration is not merged but replaced by language specific * parameter declarations. Applications that want to use the default configuration * as fallback to language specific settings can implement this by * using the properties provided by {@link #getDefaultParameters()}. * <p> * <b>Notes</b> <ul> * <li>only the first occurrence of '<code>=</code>' within an * parameter is used as separator between the param name and value. This * means that the {param-name} is allowed to contain '='. * <li>in case a comma separated string is used for the lanugage * configuration parameter declaration MUST NOT contain * '<code>,</code>' (comma) values. In case a <code>String[]</code> or an * {@link Collection} is used this is not the case. * </ul> * * @author Rupert Westenthaler * */ public class LanguageConfiguration { private static final Map<String,String> EMPTY_PARAMS = Collections.emptyMap(); private final String property; private final Collection<String> defaultConfig; //Langauge configuration private Map<String,Map<String,String>> configuredLanguages = new HashMap<String,Map<String,String>>(); private Set<String> excludedLanguages = new HashSet<String>(); private boolean allowAll; private Map<String,String> defaultParameters = EMPTY_PARAMS; @SuppressWarnings("unchecked") public LanguageConfiguration(String property, String[] defaultConfig){ if(property == null || property.isEmpty()){ throw new IllegalArgumentException("The parsed property MUST NOT be NULL nor empty!"); } this.property = property; this.defaultConfig = defaultConfig != null ? Arrays.asList(defaultConfig) : Collections.EMPTY_LIST; try { parseConfiguration(this.defaultConfig); } catch (ConfigurationException e) { throw new IllegalArgumentException("Inalied default configuration " + e.getMessage()); } } public String getProperty() { return property; } /** * Reads the config for the configured {@link #getProperty() property} * from the parsed configuration. <p> * This implementation supports * <code>null</code> (sets the default), <code>String[]</code>, * <code>Collections<?></code> (Object{@link #toString() toString()} is called * on members) and comma separated {@link String}. * @param configuration the configuration */ public void setConfiguration(Dictionary<?,?> configuration) throws ConfigurationException { processConfiguration(configuration.get(property)); } /** * Reads the configuration for the configured {@link #getProperty()} from * the properties of the parsed {@link ServiceReference}.<p> * This implementation supports * <code>null</code> (sets the default), <code>String[]</code>, * <code>Collections<?></code> (Object{@link #toString() toString()} is called * on members) and comma separated {@link String}. * @param ref the SerivceRefernece * @throws ConfigurationException */ public void setConfiguration(ServiceReference ref) throws ConfigurationException { processConfiguration(ref.getProperty(property)); } /** * Reads the configuration for the parsed value. <p> * This implementation supports * <code>null</code> (sets the default), <code>String[]</code>, * <code>Collections<?></code> (Object{@link #toString() toString()} is called * on members) and comma separated {@link String}. * @param value the value * @throws ConfigurationException if the configuration of is invalid */ protected void processConfiguration(Object value) throws ConfigurationException { Collection<?> config; if(value == null){ config = defaultConfig; } else if (value instanceof String[]){ config = Arrays.asList((String[]) value); } else if (value instanceof Collection<?>){ config = (Collection<?>)value; } else if (value instanceof String){ config = Arrays.asList(value.toString().split(",")); } else { throw new ConfigurationException(property, "Values of type '" + value.getClass() +"' are not supported (supported are " + "String[], Collection<?>, comma separated String and " + "NULL to reset to the default configuration)!"); } parseConfiguration(config); } private void parseConfiguration(Collection<?> config) throws ConfigurationException { if(config == null){ config = defaultConfig; } //rest values configuredLanguages.clear(); excludedLanguages.clear(); defaultParameters = EMPTY_PARAMS; //do not change values in multi threaded environments for(Object value : config) { if(value == null){ continue; //ignore null values } String line = value.toString().trim(); int sepIndex = line.indexOf(';'); String lang = sepIndex < 0 ? line : line.substring(0, sepIndex).trim(); //lang = lang.toLowerCase(); //country codes are upper case if(lang.length() > 0 && lang.charAt(0) == '!'){ //exclude lang = lang.substring(1); if(configuredLanguages.containsKey(lang)){ throw new ConfigurationException(property, "Langauge '"+lang+"' is both included and excluded (config: " + config+")"); } if(sepIndex >= 0){ throw new ConfigurationException(property, "The excluded Langauge '"+lang+"' MUST NOT define parameters (config: " + config+")"); } excludedLanguages.add(lang); } else if("*".equals(lang)){ allowAll = true; parsedDefaultParameters(line, sepIndex+1); } else if(!lang.isEmpty()){ if(excludedLanguages.contains(lang)){ throw new ConfigurationException(property, "Langauge '"+lang+"' is both included and excluded (config: " + config+")"); } configuredLanguages.put(lang,sepIndex >= 0 && sepIndex < line.length()-2 ? parseParameters(line.substring(sepIndex+1, line.length()).trim()) : EMPTY_PARAMS); } else { //language tag is empty (line starts with an ';' //this indicates that this is used to configure the default parameters parsedDefaultParameters(line, sepIndex+1); } } } /** * Parsed the {@link #defaultParameters} and also checks that not multiple * (non empty) of such configurations are present * @param line the current line * @param sepIndex the index of first ';' in the configuration line * @throws ConfigurationException if multiple default configurations are present or * if the parameters are illegal formatted. */ private void parsedDefaultParameters(String line, int sepIndex) throws ConfigurationException { if(!defaultParameters.isEmpty()){ throw new ConfigurationException(property, "Language Configuration MUST NOT " + "contain multiple default property configurations. This are configurations " + "of properties for the wildcard '*;{properties}' or the empty language " + "';{properties}'."); } defaultParameters = sepIndex >= 0 && sepIndex < line.length()-2 ? parseParameters(line.substring(sepIndex, line.length()).trim()) : EMPTY_PARAMS; } /** * Parses optional parameters <code>{key}[={value}];{key2}[={value2}]</code>. Using * the same key multiple times will override the previouse value * @param paramString * @return * @throws ConfigurationException */ private Map<String,String> parseParameters(String paramString) throws ConfigurationException { Map<String,String> params = new HashMap<String,String>(); for(String param : paramString.split(";")){ param = param.trim(); int equalsPos = param.indexOf('='); if(equalsPos == 0){ throw new ConfigurationException(property, "Parameter '"+param+"' has empty key!"); } String key = equalsPos > 0 ? param.substring(0, equalsPos).trim() : param; String value; if(equalsPos > 0){ if(equalsPos < param.length()-2) { value = param.substring(equalsPos+1).trim(); } else { value = ""; } } else { value = null; } params.put(key, value); } return params.isEmpty() ? EMPTY_PARAMS : Collections.unmodifiableMap(params); } private class LangState{ protected final boolean state; protected final String lang; protected LangState(boolean state, String lang){ this.state = state; this.lang = lang; } } private LangState getLanguageState(String language){ int countrySepPos = language == null ? -1 : language.indexOf('-'); boolean excluded = excludedLanguages.contains(language); boolean included = configuredLanguages.containsKey(language); if(countrySepPos >= 2 && !excluded && ! included){ //search without language specific part String baseLang = language.substring(0, countrySepPos); return new LangState(allowAll ? !excludedLanguages.contains(baseLang) : configuredLanguages.containsKey(baseLang), baseLang); } else { return new LangState(allowAll ? !excluded : included,language); } } /** * Checks if the parsed language is included in the configuration * @param language the language * @return the state */ public boolean isLanguage(String language){ return getLanguageState(language).state; } /** * The explicitly configured languages * @return */ public Set<String> getExplicitlyIncluded(){ return configuredLanguages.keySet(); } /** * The explicitly excluded (e.g. !de) languages * @return */ public Set<String> getExplicitlyExcluded(){ return excludedLanguages; } /** * If the '*' was used in the configuration to allow * all lanugages. * @return */ public boolean useWildcard(){ return allowAll; } /** * Returns configured parameters if <code>{@link #isLanguage(String)} == true</code>. * The returned map contains {@link #getLanguageParams(String) language specific parameters} * merged with {@link #getDefaultParameters()} * @param language the language * @return the parameters or <code>null</code> if none or the parsed language * is not active. */ public Map<String,String> getParameters(String parsedLang){ LangState ls = getLanguageState(parsedLang); if(ls.state){ Map<String,String> params = configuredLanguages.get(ls.lang); if(params != null){ params = new CompositeMap(params,defaultParameters,CONFIGURATION_MERGER); } else { params = defaultParameters; } return params; } else { return null; //to indicate the parsed language is not active } } /** * Getter for the language specific parameters. This does NOT include * default parameters. * @param language the language * @return the language specific parameters or <code>null</code> if no * parameters are configured. */ public Map<String,String> getLanguageParams(String parsedLang){ LangState ls = getLanguageState(parsedLang); return ls.state ? configuredLanguages.get(ls.lang) : null; } /** * Getter for the default parameters * @return the default parameters, an empty map if none. */ public Map<String,String> getDefaultParameters() { return defaultParameters; } /** * Resets the configuration to the default (as parsed in the constructor) */ public void setDefault() { try { parseConfiguration(defaultConfig); } catch (ConfigurationException e) { // can not happen else the default config is already validated // within the constructor } } /** * Returns the value of the parameter for the language (if present and the * langage is active). This merges language specific parameters with * default parameters. * @param language the language * @param paramName the name of the param * @return the param or <code>null</code> if not present OR the language * is not active. */ public String getParameter(String language, String paramName) { Map<String,String> params = getParameters(language); int countrySepPos = language == null ? -1 : language.indexOf('-'); //we need to fallback to the language specific config if // * there is a country code // * no country specific params OR // * param not present in country specific config if(countrySepPos >= 2 && (params == null || !params.containsKey(paramName))) { params = getParameters(language.substring(0,countrySepPos)); } return params == null ? null : params.get(paramName); } MapMutator CONFIGURATION_MERGER = new MapMutator() { @Override @SuppressWarnings("rawtypes") public void resolveCollision(CompositeMap composite, Map existing, Map added, Collection intersect) { //nothing to do as we want the value of the first map } @Override @SuppressWarnings({"rawtypes", "unchecked"}) public void putAll(CompositeMap map, Map[] composited, Map mapToAdd) { //add to the first composited[0].putAll(mapToAdd); } @Override @SuppressWarnings({"rawtypes", "unchecked"}) public Object put(CompositeMap map, Map[] composited, Object key, Object value) { Object prevResult = map.get(key); Object result = composited[0].put(key,value); return result == null ? prevResult : result; } }; }