Java tutorial
/* * 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; } }; }