001/*
002 * CDDL HEADER START
003 *
004 * The contents of this file are subject to the terms of the
005 * Common Development and Distribution License, Version 1.0 only
006 * (the "License").  You may not use this file except in compliance
007 * with the License.
008 *
009 * You can obtain a copy of the license at legal-notices/CDDLv1_0.txt
010 * or http://forgerock.org/license/CDDLv1.0.html.
011 * See the License for the specific language governing permissions
012 * and limitations under the License.
013 *
014 * When distributing Covered Code, include this CDDL HEADER in each
015 * file and include the License file at legal-notices/CDDLv1_0.txt.
016 * If applicable, add the following below this CDDL HEADER, with the
017 * fields enclosed by brackets "[]" replaced with your own identifying
018 * information:
019 *      Portions Copyright [yyyy] [name of copyright owner]
020 *
021 * CDDL HEADER END
022 *
023 *
024 *      Copyright 2008-2009 Sun Microsystems, Inc.
025 *      Portions Copyright 2012-2015 ForgeRock AS
026 */
027package org.opends.server.schema;
028
029import java.util.Collection;
030import java.util.Collections;
031import java.util.HashMap;
032import java.util.List;
033import java.util.Locale;
034import java.util.Map;
035import java.util.Set;
036
037import org.forgerock.i18n.LocalizableMessage;
038import org.forgerock.i18n.slf4j.LocalizedLogger;
039import org.forgerock.opendj.config.server.ConfigChangeResult;
040import org.forgerock.opendj.config.server.ConfigException;
041import org.forgerock.opendj.ldap.schema.CoreSchema;
042import org.forgerock.opendj.ldap.schema.MatchingRule;
043import org.forgerock.opendj.ldap.schema.Schema;
044import org.opends.server.admin.server.ConfigurationChangeListener;
045import org.opends.server.admin.std.server.CollationMatchingRuleCfg;
046import org.opends.server.api.MatchingRuleFactory;
047import org.opends.server.core.DirectoryServer;
048import org.opends.server.types.DirectoryException;
049import org.opends.server.types.InitializationException;
050import org.opends.server.util.CollectionUtils;
051
052import static org.opends.messages.ConfigMessages.*;
053import static org.opends.messages.SchemaMessages.*;
054
055/**
056 * This class is a factory class for Collation matching rules. It
057 * creates different matching rules based on the configuration entries.
058 */
059public final class CollationMatchingRuleFactory extends
060    MatchingRuleFactory<CollationMatchingRuleCfg> implements
061    ConfigurationChangeListener<CollationMatchingRuleCfg>
062{
063  private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass();
064
065  /** Stores the list of available locales on this JVM. */
066  private static final Set<Locale> supportedLocales = CollectionUtils.newHashSet(Locale.getAvailableLocales());
067
068  /** Current Configuration. */
069  private CollationMatchingRuleCfg currentConfig;
070  /** Map of OID and the Matching Rule. */
071  private final Map<String, MatchingRule> matchingRules = new HashMap<>();
072
073
074  /** Creates a new instance of CollationMatchingRuleFactory. */
075  public CollationMatchingRuleFactory()
076  {
077    super();
078  }
079
080  /** {@inheritDoc} */
081  @Override
082  public final Collection<org.forgerock.opendj.ldap.schema.MatchingRule> getMatchingRules()
083  {
084    return Collections.unmodifiableCollection(matchingRules.values());
085  }
086
087  /**
088   * Adds a new mapping of OID and MatchingRule.
089   *
090   * @param oid
091   *          OID of the matching rule
092   * @param matchingRule
093   *          instance of a MatchingRule.
094   */
095  private void addMatchingRule(String oid, MatchingRule matchingRule)
096  {
097    matchingRules.put(oid, matchingRule);
098  }
099
100  /**
101   * Clears the Map containing matching Rules.
102   */
103  private void resetRules()
104  {
105    matchingRules.clear();
106  }
107
108  /** {@inheritDoc} */
109  @Override
110  public void initializeMatchingRule(CollationMatchingRuleCfg configuration)
111      throws ConfigException, InitializationException
112  {
113    final Schema coreSchema = CoreSchema.getInstance();
114    for (String collation : configuration.getCollation())
115    {
116      CollationMapper mapper = new CollationMapper(collation);
117
118      String nOID = mapper.getNumericOID();
119      String languageTag = mapper.getLanguageTag();
120      if (nOID == null || languageTag == null)
121      {
122        logger.error(WARN_ATTR_INVALID_COLLATION_MATCHING_RULE_FORMAT, collation);
123        continue;
124      }
125
126      Locale locale = getLocale(languageTag);
127      if (locale != null)
128      {
129        try
130        {
131          final int[] numericSuffixes = { 1, 2, 3, 4, 5, 6 };
132          for (int suffix : numericSuffixes)
133          {
134            final String oid =  nOID + "." + suffix;
135            addMatchingRule(oid, coreSchema.getMatchingRule(oid));
136          }
137          // the default (equality) matching rule
138          addMatchingRule(nOID, coreSchema.getMatchingRule(nOID));
139        }
140        catch (Exception e)
141        {
142          logger.error(LocalizableMessage.raw("Error when adding a collation matching rule with oid %s, tag %s: %s",
143              nOID, languageTag, e.getMessage()));
144        }
145      }
146      else
147      {
148        // This locale is not supported by JVM.
149        logger.error(WARN_ATTR_INVALID_COLLATION_MATCHING_RULE_LOCALE,
150                collation, configuration.dn().toString(), languageTag);
151      }
152    }
153
154    // Save this configuration.
155    currentConfig = configuration;
156
157    // Register for change events.
158    currentConfig.addCollationChangeListener(this);
159  }
160
161  /** {@inheritDoc} */
162  @Override
163  public void finalizeMatchingRule()
164  {
165    // De-register the listener.
166    currentConfig.removeCollationChangeListener(this);
167  }
168
169  /** {@inheritDoc} */
170  @Override
171  public ConfigChangeResult applyConfigurationChange(
172      CollationMatchingRuleCfg configuration)
173  {
174    final ConfigChangeResult ccr = new ConfigChangeResult();
175
176    if (!configuration.isEnabled()
177        || currentConfig.isEnabled() != configuration.isEnabled())
178    {
179      // Don't do anything if:
180      // 1. The configuration is disabled.
181      // 2. There is a change in the enable status
182      // i.e. (disable->enable or enable->disable). In this case, the
183      // ConfigManager will have already created the new Factory object.
184      return ccr;
185    }
186
187    // Since we have come here it means that this Factory is enabled and
188    // there is a change in the CollationMatchingRuleFactory's
189    // configuration.
190    // Deregister all the Matching Rule corresponding to this factory.
191    for (MatchingRule rule : getMatchingRules())
192    {
193      DirectoryServer.deregisterMatchingRule(rule);
194    }
195
196    // Clear the associated matching rules.
197    resetRules();
198
199    final Schema coreSchema = CoreSchema.getInstance();
200    for (String collation : configuration.getCollation())
201    {
202      // validation has already been performed in isConfigurationChangeAcceptable()
203      CollationMapper mapper = new CollationMapper(collation);
204      String nOID = mapper.getNumericOID();
205      addMatchingRule(nOID, coreSchema.getMatchingRule(nOID));
206    }
207
208    try
209    {
210      for (MatchingRule matchingRule : getMatchingRules())
211      {
212        DirectoryServer.registerMatchingRule(matchingRule, false);
213      }
214    }
215    catch (DirectoryException de)
216    {
217      LocalizableMessage message =
218          WARN_CONFIG_SCHEMA_MR_CONFLICTING_MR.get(configuration.dn(), de.getMessageObject());
219      ccr.setAdminActionRequired(true);
220      ccr.addMessage(message);
221    }
222    currentConfig = configuration;
223    return ccr;
224  }
225
226  /** {@inheritDoc} */
227  @Override
228  public boolean isConfigurationChangeAcceptable(
229      CollationMatchingRuleCfg configuration,
230      List<LocalizableMessage> unacceptableReasons)
231  {
232    boolean configAcceptable = true;
233
234    // If the new configuration disables this factory, don't do
235    // anything.
236    if (!configuration.isEnabled())
237    {
238      return configAcceptable;
239    }
240
241    // If it comes here we don't need to verify MatchingRuleType; it
242    // should be okay as its syntax is verified by the admin framework.
243    // Iterate over the collations and verify if the format is okay.
244    // Also, verify if the locale is allowed by the JVM.
245    for (String collation : configuration.getCollation())
246    {
247      CollationMapper mapper = new CollationMapper(collation);
248
249      String nOID = mapper.getNumericOID();
250      String languageTag = mapper.getLanguageTag();
251      if (nOID == null || languageTag == null)
252      {
253        configAcceptable = false;
254        LocalizableMessage msg = WARN_ATTR_INVALID_COLLATION_MATCHING_RULE_FORMAT.get(collation);
255        unacceptableReasons.add(msg);
256        continue;
257      }
258
259      Locale locale = getLocale(languageTag);
260      if (locale == null)
261      {
262        LocalizableMessage msg = WARN_ATTR_INVALID_COLLATION_MATCHING_RULE_LOCALE.get(
263                collation, configuration.dn(), languageTag);
264        unacceptableReasons.add(msg);
265        configAcceptable = false;
266        continue;
267      }
268    }
269    return configAcceptable;
270  }
271
272
273  /**
274   * Verifies if the locale is supported by the JVM.
275   *
276   * @param lTag
277   *          The language tag specified in the configuration.
278   * @return Locale The locale corresponding to the languageTag.
279   */
280  private Locale getLocale(String lTag)
281  {
282    // Separates the language and the country from the locale.
283    Locale locale;
284
285    int countryIndex = lTag.indexOf("-");
286    int variantIndex = lTag.lastIndexOf("-");
287
288    if (countryIndex > 0)
289    {
290      String lang = lTag.substring(0, countryIndex);
291      String country;
292
293      if (variantIndex > countryIndex)
294      {
295        country = lTag.substring(countryIndex + 1, variantIndex);
296        String variant = lTag.substring(variantIndex + 1, lTag.length());
297        locale = new Locale(lang, country, variant);
298      }
299      else
300      {
301        country = lTag.substring(countryIndex + 1, lTag.length());
302        locale = new Locale(lang, country);
303      }
304    }
305    else
306    {
307      locale = new Locale(lTag);
308    }
309
310    if (!supportedLocales.contains(locale))
311    {
312      // This locale is not supported by this JVM.
313      locale = null;
314    }
315    return locale;
316  }
317
318  /**
319   * A utility class for extracting the OID and Language Tag from the
320   * configuration entry.
321   */
322  private final class CollationMapper
323  {
324    /** OID of the collation rule. */
325    private String oid;
326
327    /** Language Tag. */
328    private String lTag;
329
330    /**
331     * Creates a new instance of CollationMapper.
332     *
333     * @param collation
334     *          The collation text in the LOCALE:OID format.
335     */
336    private CollationMapper(String collation)
337    {
338      int index = collation.indexOf(":");
339      if (index > 0)
340      {
341        oid = collation.substring(index + 1, collation.length());
342        lTag = collation.substring(0, index);
343      }
344    }
345
346    /**
347     * Returns the OID part of the collation text.
348     *
349     * @return OID part of the collation text.
350     */
351    private String getNumericOID()
352    {
353      return oid;
354    }
355
356    /**
357     * Returns the language Tag of collation text.
358     *
359     * @return Language Tag part of the collation text.
360     */
361    private String getLanguageTag()
362    {
363      return lTag;
364    }
365  }
366}