001/*
002 * The contents of this file are subject to the terms of the Common Development and
003 * Distribution License (the License). You may not use this file except in compliance with the
004 * License.
005 *
006 * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
007 * specific language governing permission and limitations under the License.
008 *
009 * When distributing Covered Software, include this CDDL Header Notice in each file and include
010 * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
011 * Header, with the fields enclosed by brackets [] replaced by your own identifying
012 * information: "Portions Copyright [year] [name of copyright owner]".
013 *
014 * Copyright 2006-2008 Sun Microsystems, Inc.
015 * Portions Copyright 2014-2016 ForgeRock AS.
016 */
017package org.opends.server.core;
018
019import static org.opends.messages.ConfigMessages.*;
020import static org.opends.server.util.StaticUtils.*;
021
022import java.util.ArrayList;
023import java.util.Collection;
024import java.util.List;
025import java.util.concurrent.ConcurrentHashMap;
026
027import org.forgerock.i18n.LocalizableMessage;
028import org.forgerock.i18n.slf4j.LocalizedLogger;
029import org.forgerock.opendj.config.server.ConfigChangeResult;
030import org.forgerock.opendj.config.server.ConfigException;
031import org.forgerock.opendj.ldap.schema.AttributeType;
032import org.forgerock.opendj.ldap.schema.MatchingRule;
033import org.forgerock.util.Utils;
034import org.opends.server.admin.ClassPropertyDefinition;
035import org.opends.server.admin.server.ConfigurationAddListener;
036import org.opends.server.admin.server.ConfigurationChangeListener;
037import org.opends.server.admin.server.ConfigurationDeleteListener;
038import org.opends.server.admin.server.ServerManagementContext;
039import org.opends.server.admin.std.meta.MatchingRuleCfgDefn;
040import org.opends.server.admin.std.server.MatchingRuleCfg;
041import org.opends.server.admin.std.server.RootCfg;
042import org.opends.server.api.MatchingRuleFactory;
043import org.forgerock.opendj.ldap.DN;
044import org.opends.server.types.DirectoryException;
045import org.opends.server.types.InitializationException;
046import org.opends.server.types.MatchingRuleUse;
047
048/**
049 * This class defines a utility that will be used to manage the set of matching
050 * rules defined in the Directory Server.  It wil initialize the rules when the
051 * server starts, and then will manage any additions, removals, or modifications
052 * to any matching rules while the server is running.
053 */
054public class MatchingRuleConfigManager
055       implements ConfigurationChangeListener<MatchingRuleCfg>,
056                  ConfigurationAddListener<MatchingRuleCfg>,
057                  ConfigurationDeleteListener<MatchingRuleCfg>
058
059{
060
061  private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass();
062
063  /**
064   * A mapping between the DNs of the config entries and the associated matching
065   * rule Factories.
066   */
067  private ConcurrentHashMap<DN,MatchingRuleFactory> matchingRuleFactories;
068
069  private final ServerContext serverContext;
070
071  /**
072   * Creates a new instance of this matching rule config manager.
073   *
074   * @param serverContext
075   *          The server context.
076   */
077  public MatchingRuleConfigManager(ServerContext serverContext)
078  {
079    this.serverContext = serverContext;
080    matchingRuleFactories = new ConcurrentHashMap<>();
081  }
082
083
084
085  /**
086   * Initializes all matching rules after reading all the Matching Rule
087   * factories currently defined in the Directory Server configuration.
088   * This should only be called at Directory Server startup.
089   *
090   * @throws  ConfigException  If a configuration problem causes the matching
091   *                           rule initialization process to fail.
092   *
093   * @throws  InitializationException  If a problem occurs while initializing
094   *                                   the matching rules that is not related to
095   *                                   the server configuration.
096   */
097  public void initializeMatchingRules()
098         throws ConfigException, InitializationException
099  {
100    // Get the root configuration object.
101    ServerManagementContext managementContext = ServerManagementContext.getInstance();
102    RootCfg rootConfiguration =
103         managementContext.getRootConfiguration();
104
105
106    // Register as an add and delete listener with the root configuration so we
107    // can be notified if any matching rule entries are added or removed.
108    rootConfiguration.addMatchingRuleAddListener(this);
109    rootConfiguration.addMatchingRuleDeleteListener(this);
110
111    //Initialize the existing matching rules.
112    for (String name : rootConfiguration.listMatchingRules())
113    {
114      MatchingRuleCfg mrConfiguration = rootConfiguration.getMatchingRule(name);
115      mrConfiguration.addChangeListener(this);
116
117      if (mrConfiguration.isEnabled())
118      {
119        String className = mrConfiguration.getJavaClass();
120        try
121        {
122          MatchingRuleFactory<?> factory = loadMatchingRuleFactory(className, mrConfiguration, true);
123
124          try
125          {
126            for(MatchingRule matchingRule: factory.getMatchingRules())
127            {
128              DirectoryServer.registerMatchingRule(matchingRule, false);
129            }
130            matchingRuleFactories.put(mrConfiguration.dn(), factory);
131          }
132          catch (DirectoryException de)
133          {
134            logger.warn(WARN_CONFIG_SCHEMA_MR_CONFLICTING_MR, mrConfiguration.dn(), de.getMessageObject());
135            continue;
136          }
137        }
138        catch (InitializationException ie)
139        {
140          logger.error(ie.getMessageObject());
141          continue;
142        }
143      }
144    }
145  }
146
147
148
149  /** {@inheritDoc} */
150  @Override
151  public boolean isConfigurationAddAcceptable(MatchingRuleCfg configuration,
152                      List<LocalizableMessage> unacceptableReasons)
153  {
154    if (configuration.isEnabled())
155    {
156      // Get the name of the class and make sure we can instantiate it as a
157      // matching rule Factory.
158      String className = configuration.getJavaClass();
159      try
160      {
161        loadMatchingRuleFactory(className, configuration, false);
162      }
163      catch (InitializationException ie)
164      {
165        unacceptableReasons.add(ie.getMessageObject());
166        return false;
167      }
168    }
169
170    // If we've gotten here, then it's fine.
171    return true;
172  }
173
174
175
176  /** {@inheritDoc} */
177  @Override
178  public ConfigChangeResult applyConfigurationAdd(MatchingRuleCfg configuration)
179  {
180    final ConfigChangeResult ccr = new ConfigChangeResult();
181
182    configuration.addChangeListener(this);
183
184    if (! configuration.isEnabled())
185    {
186      return ccr;
187    }
188
189    MatchingRuleFactory<?> factory = null;
190
191    // Get the name of the class and make sure we can instantiate it as a
192    // matching rule Factory.
193    String className = configuration.getJavaClass();
194    try
195    {
196      factory = loadMatchingRuleFactory(className, configuration, true);
197
198      for (MatchingRule matchingRule: factory.getMatchingRules())
199      {
200        DirectoryServer.registerMatchingRule(matchingRule, false);
201      }
202      matchingRuleFactories.put(configuration.dn(),factory);
203    }
204    catch (DirectoryException de)
205    {
206      ccr.setResultCodeIfSuccess(DirectoryServer.getServerErrorResultCode());
207      ccr.addMessage(WARN_CONFIG_SCHEMA_MR_CONFLICTING_MR.get(
208          configuration.dn(), de.getMessageObject()));
209    }
210    catch (InitializationException ie)
211    {
212      ccr.setResultCodeIfSuccess(DirectoryServer.getServerErrorResultCode());
213      ccr.addMessage(ie.getMessageObject());
214    }
215
216    return ccr;
217  }
218
219
220
221  /** {@inheritDoc} */
222  @Override
223  public boolean isConfigurationDeleteAcceptable(MatchingRuleCfg configuration,
224                      List<LocalizableMessage> unacceptableReasons)
225  {
226    // If the matching rule is enabled, then check to see if there are any
227    // defined attribute types or matching rule uses that use the matching rule.
228    // If so, then don't allow it to be deleted.
229    boolean configAcceptable = true;
230    MatchingRuleFactory<?> factory = matchingRuleFactories.get(configuration.dn());
231    for(MatchingRule matchingRule: factory.getMatchingRules())
232    {
233      if (matchingRule != null)
234      {
235        for (AttributeType at : DirectoryServer.getAttributeTypes())
236        {
237          final String attr = at.getNameOrOID();
238          if (!isDeleteAcceptable(at.getApproximateMatchingRule(), matchingRule, attr, unacceptableReasons)
239              || !isDeleteAcceptable(at.getEqualityMatchingRule(), matchingRule, attr, unacceptableReasons)
240              || !isDeleteAcceptable(at.getOrderingMatchingRule(), matchingRule, attr, unacceptableReasons)
241              || !isDeleteAcceptable(at.getSubstringMatchingRule(), matchingRule, attr, unacceptableReasons))
242          {
243            configAcceptable = false;
244            continue;
245          }
246        }
247
248        final String oid = matchingRule.getOID();
249        for (MatchingRuleUse mru :
250                DirectoryServer.getMatchingRuleUses().values())
251        {
252          if (oid.equals(mru.getMatchingRule().getOID()))
253          {
254            LocalizableMessage message =
255                    WARN_CONFIG_SCHEMA_CANNOT_DELETE_MR_IN_USE_BY_MRU.get(
256                            matchingRule.getNameOrOID(), mru.getNameOrOID());
257            unacceptableReasons.add(message);
258
259            configAcceptable = false;
260            continue;
261          }
262        }
263      }
264    }
265
266    return configAcceptable;
267  }
268
269  private boolean isDeleteAcceptable(MatchingRule mr, MatchingRule matchingRule, String attr,
270      List<LocalizableMessage> unacceptableReasons)
271  {
272    if (mr != null && matchingRule.getOID().equals(mr.getOID()))
273    {
274      unacceptableReasons.add(WARN_CONFIG_SCHEMA_CANNOT_DELETE_MR_IN_USE_BY_AT.get(matchingRule.getNameOrOID(), attr));
275      return false;
276    }
277    return true;
278  }
279
280
281  /** {@inheritDoc} */
282  @Override
283  public ConfigChangeResult applyConfigurationDelete(MatchingRuleCfg configuration)
284  {
285    final ConfigChangeResult ccr = new ConfigChangeResult();
286
287    MatchingRuleFactory<?> factory = matchingRuleFactories.remove(configuration.dn());
288    if (factory != null)
289    {
290      for(MatchingRule matchingRule: factory.getMatchingRules())
291      {
292        try
293        {
294          DirectoryServer.deregisterMatchingRule(matchingRule);
295        }
296        catch (DirectoryException e)
297        {
298          ccr.addMessage(e.getMessageObject());
299          ccr.setResultCodeIfSuccess(e.getResultCode());
300        }
301      }
302      factory.finalizeMatchingRule();
303    }
304
305    return ccr;
306  }
307
308
309
310  /** {@inheritDoc} */
311  @Override
312  public boolean isConfigurationChangeAcceptable(MatchingRuleCfg configuration,
313                      List<LocalizableMessage> unacceptableReasons)
314  {
315    boolean configAcceptable = true;
316    if (configuration.isEnabled())
317    {
318      // Get the name of the class and make sure we can instantiate it as a
319      // matching rule Factory.
320      String className = configuration.getJavaClass();
321      try
322      {
323        loadMatchingRuleFactory(className, configuration, false);
324      }
325      catch (InitializationException ie)
326      {
327        unacceptableReasons.add(ie.getMessageObject());
328        configAcceptable = false;
329      }
330    }
331    else
332    {
333      // If the matching rule is currently enabled and the change would make it
334      // disabled, then only allow it if the matching rule isn't already in use.
335      MatchingRuleFactory<?> factory = matchingRuleFactories.get(configuration.dn());
336      if(factory == null)
337      {
338        //Factory was disabled again.
339        return configAcceptable;
340      }
341      for(MatchingRule matchingRule: factory.getMatchingRules())
342      {
343        if (matchingRule != null)
344        {
345          for (AttributeType at : DirectoryServer.getAttributeTypes())
346          {
347            final String attr = at.getNameOrOID();
348            if (!isDisableAcceptable(at.getApproximateMatchingRule(), matchingRule, attr, unacceptableReasons)
349                || !isDisableAcceptable(at.getEqualityMatchingRule(), matchingRule, attr, unacceptableReasons)
350                || !isDisableAcceptable(at.getOrderingMatchingRule(), matchingRule, attr, unacceptableReasons)
351                || !isDisableAcceptable(at.getSubstringMatchingRule(), matchingRule, attr, unacceptableReasons))
352            {
353              configAcceptable = false;
354              continue;
355            }
356          }
357
358          final String oid = matchingRule.getOID();
359          for (MatchingRuleUse mru :
360               DirectoryServer.getMatchingRuleUses().values())
361          {
362            if (oid.equals(mru.getMatchingRule().getOID()))
363            {
364              LocalizableMessage message =
365                      WARN_CONFIG_SCHEMA_CANNOT_DISABLE_MR_IN_USE_BY_MRU.get(
366                              matchingRule.getNameOrOID(), mru.getNameOrOID());
367              unacceptableReasons.add(message);
368
369              configAcceptable = false;
370              continue;
371            }
372          }
373        }
374      }
375    }
376    return configAcceptable;
377  }
378
379  private boolean isDisableAcceptable(MatchingRule mr, MatchingRule matchingRule,
380      String attrNameOrOID, Collection<LocalizableMessage> unacceptableReasons)
381  {
382    if (mr != null && matchingRule.getOID().equals(mr.getOID()))
383    {
384      unacceptableReasons.add(
385          WARN_CONFIG_SCHEMA_CANNOT_DISABLE_MR_IN_USE_BY_AT.get(matchingRule.getNameOrOID(), attrNameOrOID));
386      return false;
387    }
388    return true;
389  }
390
391  /** {@inheritDoc} */
392  @Override
393  public ConfigChangeResult applyConfigurationChange(
394                                 MatchingRuleCfg configuration)
395  {
396    final ConfigChangeResult ccr = new ConfigChangeResult();
397
398
399   // Get the existing matching rule factory if it's already enabled.
400    MatchingRuleFactory<?> existingFactory =
401            matchingRuleFactories.get(configuration.dn());
402
403
404    // If the new configuration has the matching rule disabled, then disable it
405    // if it is enabled, or do nothing if it's already disabled.
406    if (! configuration.isEnabled())
407    {
408     if (existingFactory != null)
409      {
410        for(MatchingRule existingRule: existingFactory.getMatchingRules())
411        {
412          try
413          {
414            DirectoryServer.deregisterMatchingRule(existingRule);
415          }
416          catch (DirectoryException e)
417          {
418            ccr.addMessage(e.getMessageObject());
419            ccr.setResultCodeIfSuccess(e.getResultCode());
420          }
421        }
422        matchingRuleFactories.remove(configuration.dn());
423        existingFactory.finalizeMatchingRule();
424      }
425      return ccr;
426    }
427
428
429    // Get the class for the matching rule.  If the matching rule is already
430    // enabled, then we shouldn't do anything with it although if the class has
431    // changed then we'll at least need to indicate that administrative action
432    // is required.  If the matching rule is disabled, then instantiate the
433    // class and initialize and register it as a matching rule.
434    String className = configuration.getJavaClass();
435    if (existingFactory != null)
436    {
437      if (! className.equals(existingFactory.getClass().getName()))
438      {
439        ccr.setAdminActionRequired(true);
440      }
441
442      return ccr;
443    }
444
445    MatchingRuleFactory<?> factory = null;
446    try
447    {
448      factory = loadMatchingRuleFactory(className, configuration, true);
449
450      for (MatchingRule matchingRule: factory.getMatchingRules())
451      {
452        DirectoryServer.registerMatchingRule(matchingRule, false);
453      }
454      matchingRuleFactories.put(configuration.dn(), factory);
455    }
456    catch (DirectoryException de)
457    {
458      ccr.addMessage(WARN_CONFIG_SCHEMA_MR_CONFLICTING_MR.get(configuration.dn(), de.getMessageObject()));
459      ccr.setResultCodeIfSuccess(DirectoryServer.getServerErrorResultCode());
460    }
461    catch (InitializationException ie)
462    {
463      ccr.setResultCodeIfSuccess(DirectoryServer.getServerErrorResultCode());
464      ccr.addMessage(ie.getMessageObject());
465    }
466
467    return ccr;
468  }
469
470
471
472  /**
473   * Loads the specified class, instantiates it as an attribute syntax, and
474   * optionally initializes that instance.
475   *
476   * @param  className      The fully-qualified name of the attribute syntax
477   *                        class to load, instantiate, and initialize.
478   * @param  configuration  The configuration to use to initialize the attribute
479   *                        syntax.  It must not be {@code null}.
480   * @param  initialize     Indicates whether the matching rule instance should
481   *                        be initialized.
482   *
483   * @return  The possibly initialized attribute syntax.
484   *
485   * @throws  InitializationException  If a problem occurred while attempting to
486   *                                   initialize the attribute syntax.
487   */
488  private MatchingRuleFactory loadMatchingRuleFactory(String className,
489                                        MatchingRuleCfg configuration,
490                                        boolean initialize)
491          throws InitializationException
492  {
493    try
494    {
495      MatchingRuleFactory factory = null;
496      MatchingRuleCfgDefn definition = MatchingRuleCfgDefn.getInstance();
497      ClassPropertyDefinition propertyDefinition = definition.getJavaClassPropertyDefinition();
498      Class<? extends MatchingRuleFactory> matchingRuleFactoryClass =
499           propertyDefinition.loadClass(className,
500                                        MatchingRuleFactory.class);
501      factory = matchingRuleFactoryClass.newInstance();
502
503      if (initialize)
504      {
505        factory.initializeMatchingRule(configuration);
506      }
507      else
508      {
509        List<LocalizableMessage> unacceptableReasons = new ArrayList<>();
510        if (!factory.isConfigurationAcceptable(configuration, unacceptableReasons))
511        {
512          String reasons = Utils.joinAsString(".  ", unacceptableReasons);
513          throw new InitializationException(
514              ERR_CONFIG_SCHEMA_MR_CONFIG_NOT_ACCEPTABLE.get(configuration.dn(), reasons));
515        }
516      }
517
518      return factory;
519    }
520    catch (Exception e)
521    {
522      LocalizableMessage message = ERR_CONFIG_SCHEMA_MR_CANNOT_INITIALIZE.
523          get(className, configuration.dn(), stackTraceToSingleLineString(e));
524      throw new InitializationException(message, e);
525    }
526  }
527}