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 2012-2016 ForgeRock AS.
016 */
017package org.opends.server.tools;
018
019import static com.forgerock.opendj.cli.ArgumentConstants.*;
020import static com.forgerock.opendj.cli.CommonArguments.*;
021import static com.forgerock.opendj.cli.Utils.*;
022
023import static org.opends.messages.ToolMessages.*;
024import static org.opends.server.config.ConfigConstants.*;
025import static org.opends.server.util.StaticUtils.*;
026
027import java.io.OutputStream;
028import java.io.PrintStream;
029import java.util.Iterator;
030import java.util.LinkedList;
031import java.util.List;
032import java.util.Map;
033import java.util.Set;
034import java.util.TreeMap;
035import java.util.TreeSet;
036
037import org.forgerock.i18n.LocalizableMessage;
038import org.forgerock.opendj.adapter.server3x.Converters;
039import org.forgerock.opendj.config.server.ConfigException;
040import org.forgerock.opendj.ldap.ByteString;
041import org.forgerock.opendj.ldap.DN;
042import org.opends.server.config.ConfigurationHandler;
043import org.opends.server.core.DirectoryServer;
044import org.opends.server.core.DirectoryServer.DirectoryServerVersionHandler;
045import org.opends.server.loggers.JDKLogging;
046import org.opends.server.types.Attribute;
047import org.opends.server.types.Entry;
048import org.opends.server.types.InitializationException;
049import org.opends.server.types.NullOutputStream;
050import org.opends.server.util.BuildVersion;
051
052import com.forgerock.opendj.cli.ArgumentException;
053import com.forgerock.opendj.cli.ArgumentParser;
054import com.forgerock.opendj.cli.BooleanArgument;
055import com.forgerock.opendj.cli.StringArgument;
056import com.forgerock.opendj.cli.TableBuilder;
057import com.forgerock.opendj.cli.TextTablePrinter;
058
059/**
060 * This program provides a utility that may be used to list the backends in the
061 * server, as well as to determine which backend holds a given entry.
062 */
063public class ListBackends
064{
065  /**
066   * Parses the provided command-line arguments and uses that information to
067   * list the backend information.
068   *
069   * @param  args  The command-line arguments provided to this program.
070   */
071  public static void main(String[] args)
072  {
073    int retCode = listBackends(args, true, System.out, System.err);
074
075    if(retCode != 0)
076    {
077      System.exit(filterExitCode(retCode));
078    }
079  }
080
081  /**
082   * Parses the provided command-line arguments and uses that information to
083   * list the backend information.
084   *
085   * @param  args              The command-line arguments provided to this
086   *                           program.
087   * @param  initializeServer  Indicates whether to initialize the server.
088   * @param  outStream         The output stream to use for standard output, or
089   *                           <CODE>null</CODE> if standard output is not
090   *                           needed.
091   * @param  errStream         The output stream to use for standard error, or
092   *                           <CODE>null</CODE> if standard error is not
093   *                           needed.
094   *
095   * @return  A return code indicating whether the processing was successful.
096   */
097  public static int listBackends(String[] args, boolean initializeServer,
098                                 OutputStream outStream, OutputStream errStream)
099  {
100    PrintStream out = NullOutputStream.wrapOrNullStream(outStream);
101    PrintStream err = NullOutputStream.wrapOrNullStream(errStream);
102    JDKLogging.disableLogging();
103
104    // Define the command-line arguments that may be used with this program.
105    BooleanArgument displayUsage = null;
106    StringArgument  backendID    = null;
107    StringArgument  baseDN       = null;
108    StringArgument  configFile   = null;
109
110    // Create the command-line argument parser for use with this program.
111    LocalizableMessage toolDescription = INFO_LISTBACKENDS_TOOL_DESCRIPTION.get();
112    ArgumentParser argParser =
113         new ArgumentParser("org.opends.server.tools.ListBackends",
114                            toolDescription, false);
115    argParser.setShortToolDescription(REF_SHORT_DESC_LIST_BACKENDS.get());
116    argParser.setVersionHandler(new DirectoryServerVersionHandler());
117
118    // Initialize all the command-line argument types and register them with the parser.
119    try
120    {
121      configFile =
122              StringArgument.builder("configFile")
123                      .shortIdentifier('f')
124                      .description(INFO_DESCRIPTION_CONFIG_FILE.get())
125                      .hidden()
126                      .required()
127                      .valuePlaceholder(INFO_CONFIGFILE_PLACEHOLDER.get())
128                      .buildAndAddToParser(argParser);
129      backendID =
130              StringArgument.builder("backendID")
131                      .shortIdentifier('n')
132                      .description(INFO_LISTBACKENDS_DESCRIPTION_BACKEND_ID.get())
133                      .multiValued()
134                      .valuePlaceholder(INFO_BACKENDNAME_PLACEHOLDER.get())
135                      .buildAndAddToParser(argParser);
136      baseDN =
137              StringArgument.builder(OPTION_LONG_BASEDN)
138                      .shortIdentifier(OPTION_SHORT_BASEDN)
139                      .description(INFO_LISTBACKENDS_DESCRIPTION_BASE_DN.get())
140                      .multiValued()
141                      .valuePlaceholder(INFO_BASEDN_PLACEHOLDER.get())
142                      .buildAndAddToParser(argParser);
143
144      displayUsage = showUsageArgument();
145      argParser.addArgument(displayUsage);
146      argParser.setUsageArgument(displayUsage, out);
147    }
148    catch (ArgumentException ae)
149    {
150      printWrappedText(err, ERR_CANNOT_INITIALIZE_ARGS.get(ae.getMessage()));
151      return 1;
152    }
153
154    // Parse the command-line arguments provided to this program.
155    try
156    {
157      argParser.parseArguments(args);
158    }
159    catch (ArgumentException ae)
160    {
161      argParser.displayMessageAndUsageReference(err, ERR_ERROR_PARSING_ARGS.get(ae.getMessage()));
162      return 1;
163    }
164
165    // If we should just display usage or version information,
166    // then it's already been done so just return.
167    if (argParser.usageOrVersionDisplayed())
168    {
169      return 0;
170    }
171
172    // Make sure that the user did not provide both the backend ID and base DN
173    // arguments.
174    if (backendID.isPresent() && baseDN.isPresent())
175    {
176      printWrappedText(err, conflictingArgsErrorMessage(backendID, baseDN));
177      return 1;
178    }
179
180    // Checks the version - if upgrade required, the tool is unusable
181    try
182    {
183      BuildVersion.checkVersionMismatch();
184    }
185    catch (InitializationException e)
186    {
187      printWrappedText(err, e.getMessage());
188      return 1;
189    }
190
191    if (initializeServer)
192    {
193      try
194      {
195        new DirectoryServer.InitializationBuilder(configFile.getValue())
196            .initialize();
197      }
198      catch (Exception e)
199      {
200        printWrappedText(err, e.getLocalizedMessage());
201        return 1;
202      }
203    }
204
205    // Retrieve a list of the backends defined in the server.
206    Map<String, Set<DN>> backends;
207    try
208    {
209      backends = getBackends();
210    }
211    catch (ConfigException ce)
212    {
213      printWrappedText(err, ERR_LISTBACKENDS_CANNOT_GET_BACKENDS.get(ce.getMessage()));
214      return 1;
215    }
216    catch (Exception e)
217    {
218      printWrappedText(err, ERR_LISTBACKENDS_CANNOT_GET_BACKENDS.get(getExceptionMessage(e)));
219      return 1;
220    }
221
222    // See what action we need to take based on the arguments provided.  If the
223    // backend ID argument was present, then list the base DNs for that backend.
224    // If the base DN argument was present, then list the backend for that base
225    // DN.  If no arguments were provided, then list all backends and base DNs.
226    boolean invalidDn = false;
227    if (baseDN.isPresent())
228    {
229      // Create a map from the base DNs of the backends to the corresponding backend ID.
230      Map<DN, String> baseToIDMap = new TreeMap<>();
231      for (String id : backends.keySet())
232      {
233        for (DN dn : backends.get(id))
234        {
235          baseToIDMap.put(dn, id);
236        }
237      }
238
239      // Iterate through the base DN values specified by the user.  Determine
240      // the backend for that entry, and whether the provided DN is a base DN
241      // for that backend.
242      for (String dnStr : baseDN.getValues())
243      {
244        DN dn;
245        try
246        {
247          dn = DN.valueOf(dnStr);
248        }
249        catch (Exception e)
250        {
251          printWrappedText(err, ERR_LISTBACKENDS_INVALID_DN.get(dnStr, getExceptionMessage(e)));
252          return 1;
253        }
254
255        String id = baseToIDMap.get(dn);
256        if (id == null)
257        {
258          err.println(INFO_LISTBACKENDS_NOT_BASE_DN.get(dn));
259
260          DN parentDN = dn.parent();
261          while (true)
262          {
263            if (parentDN == null)
264            {
265              err.println(INFO_LISTBACKENDS_NO_BACKEND_FOR_DN.get(dn));
266              invalidDn = true;
267              break;
268            }
269            else
270            {
271              id = baseToIDMap.get(parentDN);
272              if (id != null)
273              {
274                out.println(INFO_LISTBACKENDS_DN_BELOW_BASE.get(dn, parentDN, id));
275                break;
276              }
277            }
278
279            parentDN = parentDN.parent();
280          }
281        }
282        else
283        {
284          out.println(INFO_LISTBACKENDS_BASE_FOR_ID.get(dn, id));
285        }
286      }
287    }
288    else
289    {
290      List<String> backendIDs = backendID.isPresent() ? backendID.getValues() : new LinkedList<>(backends.keySet());
291
292      // Figure out the length of the longest backend ID and base DN defined in
293      // the server.  We'll use that information to try to align the output.
294      LocalizableMessage backendIDLabel = INFO_LISTBACKENDS_LABEL_BACKEND_ID.get();
295      LocalizableMessage baseDNLabel = INFO_LISTBACKENDS_LABEL_BASE_DN.get();
296      int    backendIDLength = 10;
297      int    baseDNLength    = 7;
298
299      Iterator<String> it = backendIDs.iterator();
300      while (it.hasNext())
301      {
302        String id = it.next();
303        Set<DN> baseDNs = backends.get(id);
304        if (baseDNs == null)
305        {
306          printWrappedText(err, ERR_LISTBACKENDS_NO_SUCH_BACKEND.get(id));
307          it.remove();
308        }
309        else
310        {
311          backendIDLength = Math.max(id.length(), backendIDLength);
312          for (DN dn : baseDNs)
313          {
314            baseDNLength = Math.max(dn.toString().length(), baseDNLength);
315          }
316        }
317      }
318
319      if (backendIDs.isEmpty())
320      {
321        printWrappedText(err, ERR_LISTBACKENDS_NO_VALID_BACKENDS.get());
322        return 1;
323      }
324
325      TableBuilder table = new TableBuilder();
326      LocalizableMessage[] headers = {backendIDLabel, baseDNLabel};
327      for (LocalizableMessage header : headers)
328      {
329        table.appendHeading(header);
330      }
331      for (String id : backendIDs)
332      {
333        table.startRow();
334        table.appendCell(id);
335        StringBuilder buf = new StringBuilder();
336
337        Set<DN> baseDNs = backends.get(id);
338        boolean isFirst = true;
339        for (DN dn : baseDNs)
340        {
341          if (!isFirst)
342          {
343            buf.append(",");
344          }
345          else
346          {
347            isFirst = false;
348          }
349          if (dn.size() > 1)
350          {
351            buf.append("\"").append(dn).append("\"");
352          }
353          else
354          {
355            buf.append(dn);
356          }
357        }
358        table.appendCell(buf.toString());
359      }
360      TextTablePrinter printer = new TextTablePrinter(out);
361      printer.setColumnSeparator(LIST_TABLE_SEPARATOR);
362      table.print(printer);
363    }
364
365    // If we've gotten here, then everything completed successfully.
366    return invalidDn ? 1 : 0 ;
367  }
368
369  /**
370   * Retrieves information about the backends configured in the Directory Server
371   * mapped between the backend ID to the set of base DNs for that backend.
372   *
373   * @return  Information about the backends configured in the Directory Server.
374   *
375   * @throws  ConfigException  If a problem occurs while reading the server
376   *                           configuration.
377   */
378  private static Map<String, Set<DN>> getBackends() throws ConfigException
379  {
380    // Get the base entry for all backend configuration.
381    DN backendBaseDN = null;
382    try
383    {
384      backendBaseDN = DN.valueOf(DN_BACKEND_BASE);
385    }
386    catch (Exception e)
387    {
388      LocalizableMessage message = ERR_CANNOT_DECODE_BACKEND_BASE_DN.get(
389          DN_BACKEND_BASE, getExceptionMessage(e));
390      throw new ConfigException(message, e);
391    }
392
393    // Iterate through the immediate children, attempting to parse them as backends.
394    Map<String, Set<DN>> backendMap = new TreeMap<>();
395    ConfigurationHandler configHandler = DirectoryServer.getConfigurationHandler();
396    for (DN childrenDn : configHandler.getChildren(backendBaseDN))
397    {
398      Entry configEntry = Converters.to(configHandler.getEntry(childrenDn));
399      // Get the backend ID attribute from the entry.  If there isn't one, then
400      // skip the entry.
401      String backendID = null;
402      try
403      {
404        backendID = BackendToolUtils.getStringSingleValuedAttribute(configEntry, ATTR_BACKEND_ID);
405        if (backendID == null)
406        {
407          continue;
408        }
409      }
410      catch (Exception e)
411      {
412        LocalizableMessage message = ERR_CANNOT_DETERMINE_BACKEND_ID.get(configEntry.getName(), getExceptionMessage(e));
413        throw new ConfigException(message, e);
414      }
415
416      // Get the base DN attribute from the entry.  If there isn't one, then
417      // just skip this entry.
418      Set<DN> baseDNs = new TreeSet<>();
419      try
420      {
421        List<Attribute> attributes = configEntry.getAttribute(ATTR_BACKEND_BASE_DN);
422        if (!attributes.isEmpty())
423        {
424          Attribute attribute = attributes.get(0);
425          for (ByteString byteString : attribute)
426          {
427            baseDNs.add(DN.valueOf(byteString.toString()));
428          }
429        }
430      }
431      catch (Exception e)
432      {
433        LocalizableMessage message = ERR_CANNOT_DETERMINE_BASES_FOR_BACKEND.get(
434            configEntry.getName(), getExceptionMessage(e));
435        throw new ConfigException(message, e);
436      }
437
438      backendMap.put(backendID, baseDNs);
439    }
440
441    return backendMap;
442  }
443}