001/*
002 * Copyright 2009-2024 Ping Identity Corporation
003 * All Rights Reserved.
004 */
005/*
006 * Copyright 2009-2024 Ping Identity Corporation
007 *
008 * Licensed under the Apache License, Version 2.0 (the "License");
009 * you may not use this file except in compliance with the License.
010 * You may obtain a copy of the License at
011 *
012 *    http://www.apache.org/licenses/LICENSE-2.0
013 *
014 * Unless required by applicable law or agreed to in writing, software
015 * distributed under the License is distributed on an "AS IS" BASIS,
016 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
017 * See the License for the specific language governing permissions and
018 * limitations under the License.
019 */
020/*
021 * Copyright (C) 2009-2024 Ping Identity Corporation
022 *
023 * This program is free software; you can redistribute it and/or modify
024 * it under the terms of the GNU General Public License (GPLv2 only)
025 * or the terms of the GNU Lesser General Public License (LGPLv2.1 only)
026 * as published by the Free Software Foundation.
027 *
028 * This program is distributed in the hope that it will be useful,
029 * but WITHOUT ANY WARRANTY; without even the implied warranty of
030 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
031 * GNU General Public License for more details.
032 *
033 * You should have received a copy of the GNU General Public License
034 * along with this program; if not, see <http://www.gnu.org/licenses>.
035 */
036package com.unboundid.ldap.sdk.persist;
037
038
039
040import java.io.File;
041import java.io.OutputStream;
042import java.io.Serializable;
043import java.util.LinkedHashMap;
044import java.util.List;
045
046import com.unboundid.asn1.ASN1OctetString;
047import com.unboundid.ldap.sdk.Attribute;
048import com.unboundid.ldap.sdk.Entry;
049import com.unboundid.ldap.sdk.Modification;
050import com.unboundid.ldap.sdk.ModificationType;
051import com.unboundid.ldap.sdk.ResultCode;
052import com.unboundid.ldap.sdk.Version;
053import com.unboundid.ldap.sdk.schema.AttributeTypeDefinition;
054import com.unboundid.ldap.sdk.schema.ObjectClassDefinition;
055import com.unboundid.ldif.LDIFModifyChangeRecord;
056import com.unboundid.ldif.LDIFRecord;
057import com.unboundid.ldif.LDIFWriter;
058import com.unboundid.util.CommandLineTool;
059import com.unboundid.util.Debug;
060import com.unboundid.util.Mutable;
061import com.unboundid.util.NotNull;
062import com.unboundid.util.Nullable;
063import com.unboundid.util.StaticUtils;
064import com.unboundid.util.ThreadSafety;
065import com.unboundid.util.ThreadSafetyLevel;
066import com.unboundid.util.args.ArgumentException;
067import com.unboundid.util.args.ArgumentParser;
068import com.unboundid.util.args.BooleanArgument;
069import com.unboundid.util.args.FileArgument;
070import com.unboundid.util.args.StringArgument;
071
072import static com.unboundid.ldap.sdk.persist.PersistMessages.*;
073
074
075
076/**
077 * This class provides a tool which can be used to generate LDAP attribute
078 * type and object class definitions which may be used to store objects
079 * created from a specified Java class.  The given class must be included in the
080 * classpath of the JVM used to invoke the tool, and must be marked with the
081 * {@link LDAPObject} annotation.
082 */
083@Mutable()
084@ThreadSafety(level=ThreadSafetyLevel.NOT_THREADSAFE)
085public final class GenerateSchemaFromSource
086       extends CommandLineTool
087       implements Serializable
088{
089  /**
090   * The serial version UID for this serializable class.
091   */
092  private static final long serialVersionUID = 1029934829295836935L;
093
094
095
096  // Arguments used by this tool.
097  @Nullable private BooleanArgument modifyFormatArg;
098  @Nullable private FileArgument    outputFileArg;
099  @Nullable private StringArgument  classNameArg;
100
101
102
103  /**
104   * Parse the provided command line arguments and perform the appropriate
105   * processing.
106   *
107   * @param  args  The command line arguments provided to this program.
108   */
109  public static void main(@NotNull final String[] args)
110  {
111    final ResultCode resultCode = main(args, System.out, System.err);
112    if (resultCode != ResultCode.SUCCESS)
113    {
114      System.exit(resultCode.intValue());
115    }
116  }
117
118
119
120  /**
121   * Parse the provided command line arguments and perform the appropriate
122   * processing.
123   *
124   * @param  args       The command line arguments provided to this program.
125   * @param  outStream  The output stream to which standard out should be
126   *                    written.  It may be {@code null} if output should be
127   *                    suppressed.
128   * @param  errStream  The output stream to which standard error should be
129   *                    written.  It may be {@code null} if error messages
130   *                    should be suppressed.
131   *
132   * @return  A result code indicating whether the processing was successful.
133   */
134  @NotNull()
135  public static ResultCode main(@NotNull final String[] args,
136                                @Nullable final OutputStream outStream,
137                                @Nullable final OutputStream errStream)
138  {
139    final GenerateSchemaFromSource tool =
140         new GenerateSchemaFromSource(outStream, errStream);
141    return tool.runTool(args);
142  }
143
144
145
146  /**
147   * Creates a new instance of this tool.
148   *
149   * @param  outStream  The output stream to which standard out should be
150   *                    written.  It may be {@code null} if output should be
151   *                    suppressed.
152   * @param  errStream  The output stream to which standard error should be
153   *                    written.  It may be {@code null} if error messages
154   *                    should be suppressed.
155   */
156  public GenerateSchemaFromSource(@Nullable final OutputStream outStream,
157                                  @Nullable final OutputStream errStream)
158  {
159    super(outStream, errStream);
160  }
161
162
163
164  /**
165   * {@inheritDoc}
166   */
167  @Override()
168  @NotNull()
169  public String getToolName()
170  {
171    return "generate-schema-from-source";
172  }
173
174
175
176  /**
177   * {@inheritDoc}
178   */
179  @Override()
180  @NotNull()
181  public String getToolDescription()
182  {
183    return INFO_GEN_SCHEMA_TOOL_DESCRIPTION.get();
184  }
185
186
187
188  /**
189   * Retrieves the version string for this tool.
190   *
191   * @return  The version string for this tool.
192   */
193  @Override()
194  @NotNull()
195  public String getToolVersion()
196  {
197    return Version.NUMERIC_VERSION_STRING;
198  }
199
200
201
202  /**
203   * Indicates whether this tool should provide support for an interactive mode,
204   * in which the tool offers a mode in which the arguments can be provided in
205   * a text-driven menu rather than requiring them to be given on the command
206   * line.  If interactive mode is supported, it may be invoked using the
207   * "--interactive" argument.  Alternately, if interactive mode is supported
208   * and {@link #defaultsToInteractiveMode()} returns {@code true}, then
209   * interactive mode may be invoked by simply launching the tool without any
210   * arguments.
211   *
212   * @return  {@code true} if this tool supports interactive mode, or
213   *          {@code false} if not.
214   */
215  @Override()
216  public boolean supportsInteractiveMode()
217  {
218    return true;
219  }
220
221
222
223  /**
224   * Indicates whether this tool defaults to launching in interactive mode if
225   * the tool is invoked without any command-line arguments.  This will only be
226   * used if {@link #supportsInteractiveMode()} returns {@code true}.
227   *
228   * @return  {@code true} if this tool defaults to using interactive mode if
229   *          launched without any command-line arguments, or {@code false} if
230   *          not.
231   */
232  @Override()
233  public boolean defaultsToInteractiveMode()
234  {
235    return true;
236  }
237
238
239
240  /**
241   * Indicates whether this tool supports the use of a properties file for
242   * specifying default values for arguments that aren't specified on the
243   * command line.
244   *
245   * @return  {@code true} if this tool supports the use of a properties file
246   *          for specifying default values for arguments that aren't specified
247   *          on the command line, or {@code false} if not.
248   */
249  @Override()
250  public boolean supportsPropertiesFile()
251  {
252    return true;
253  }
254
255
256
257  /**
258   * {@inheritDoc}
259   */
260  @Override()
261  public void addToolArguments(@NotNull final ArgumentParser parser)
262         throws ArgumentException
263  {
264    classNameArg = new StringArgument('c', "javaClass", true, 1,
265         INFO_GEN_SCHEMA_VALUE_PLACEHOLDER_CLASS.get(),
266         INFO_GEN_SCHEMA_ARG_DESCRIPTION_JAVA_CLASS.get());
267    classNameArg.addLongIdentifier("java-class", true);
268    parser.addArgument(classNameArg);
269
270    outputFileArg = new FileArgument('f', "outputFile", true, 1,
271         INFO_GEN_SCHEMA_VALUE_PLACEHOLDER_PATH.get(),
272         INFO_GEN_SCHEMA_ARG_DESCRIPTION_OUTPUT_FILE.get(), false, true, true,
273         false);
274    outputFileArg.addLongIdentifier("output-file", true);
275    parser.addArgument(outputFileArg);
276
277    modifyFormatArg = new BooleanArgument('m', "modifyFormat",
278         INFO_GEN_SCHEMA_ARG_DESCRIPTION_MODIFY_FORMAT.get());
279    modifyFormatArg.addLongIdentifier("modify-format", true);
280    parser.addArgument(modifyFormatArg);
281  }
282
283
284
285  /**
286   * {@inheritDoc}
287   */
288  @Override()
289  @NotNull()
290  public ResultCode doToolProcessing()
291  {
292    // Load the specified Java class.
293    final String className = classNameArg.getValue();
294    final Class<?> targetClass;
295    try
296    {
297      targetClass = Class.forName(className);
298    }
299    catch (final Exception e)
300    {
301      Debug.debugException(e);
302      err(ERR_GEN_SCHEMA_CANNOT_LOAD_CLASS.get(className));
303      return ResultCode.PARAM_ERROR;
304    }
305
306
307    // Create an LDAP persister for the class and use it to ensure that the
308    // class is valid.
309    final LDAPPersister<?> persister;
310    try
311    {
312      persister = LDAPPersister.getInstance(targetClass);
313    }
314    catch (final Exception e)
315    {
316      Debug.debugException(e);
317      err(ERR_GEN_SCHEMA_INVALID_CLASS.get(className,
318           StaticUtils.getExceptionMessage(e)));
319      return ResultCode.LOCAL_ERROR;
320    }
321
322
323    // Use the persister to generate the attribute type and object class
324    // definitions.
325    final List<AttributeTypeDefinition> attrTypes;
326    try
327    {
328      attrTypes = persister.constructAttributeTypes();
329    }
330    catch (final Exception e)
331    {
332      Debug.debugException(e);
333      err(ERR_GEN_SCHEMA_ERROR_CONSTRUCTING_ATTRS.get(className,
334           StaticUtils.getExceptionMessage(e)));
335      return ResultCode.LOCAL_ERROR;
336    }
337
338    final List<ObjectClassDefinition> objectClasses;
339    try
340    {
341      objectClasses = persister.constructObjectClasses();
342    }
343    catch (final Exception e)
344    {
345      Debug.debugException(e);
346      err(ERR_GEN_SCHEMA_ERROR_CONSTRUCTING_OCS.get(className,
347           StaticUtils.getExceptionMessage(e)));
348      return ResultCode.LOCAL_ERROR;
349    }
350
351
352    // Convert the attribute type and object class definitions into their
353    // appropriate string representations.
354    int i=0;
355    final ASN1OctetString[] attrTypeValues =
356         new ASN1OctetString[attrTypes.size()];
357    for (final AttributeTypeDefinition d : attrTypes)
358    {
359      attrTypeValues[i++] = new ASN1OctetString(d.toString());
360    }
361
362    i=0;
363    final ASN1OctetString[] ocValues =
364         new ASN1OctetString[objectClasses.size()];
365    for (final ObjectClassDefinition d : objectClasses)
366    {
367      ocValues[i++] = new ASN1OctetString(d.toString());
368    }
369
370
371    // Construct the LDIF record to be written.
372    final LDIFRecord schemaRecord;
373    if (modifyFormatArg.isPresent())
374    {
375      schemaRecord = new LDIFModifyChangeRecord("cn=schema",
376           new Modification(ModificationType.ADD, "attributeTypes",
377                attrTypeValues),
378           new Modification(ModificationType.ADD, "objectClasses", ocValues));
379    }
380    else
381    {
382      schemaRecord = new Entry("cn=schema",
383           new Attribute("objectClass", "top", "ldapSubentry", "subschema"),
384           new Attribute("cn", "schema"),
385           new Attribute("attributeTypes", attrTypeValues),
386           new Attribute("objectClasses", ocValues));
387    }
388
389
390    // Write the schema entry to the specified file.
391    final File outputFile = outputFileArg.getValue();
392    try
393    {
394      final LDIFWriter ldifWriter = new LDIFWriter(outputFile);
395      ldifWriter.writeLDIFRecord(schemaRecord);
396      ldifWriter.close();
397    }
398    catch (final Exception e)
399    {
400      Debug.debugException(e);
401      err(ERR_GEN_SCHEMA_CANNOT_WRITE_SCHEMA.get(outputFile.getAbsolutePath(),
402           StaticUtils.getExceptionMessage(e)));
403      return ResultCode.LOCAL_ERROR;
404    }
405
406
407    return ResultCode.SUCCESS;
408  }
409
410
411
412  /**
413   * {@inheritDoc}
414   */
415  @Override()
416  @NotNull()
417  public LinkedHashMap<String[],String> getExampleUsages()
418  {
419    final LinkedHashMap<String[],String> examples =
420         new LinkedHashMap<>(StaticUtils.computeMapCapacity(1));
421
422    final String[] args =
423    {
424      "--javaClass", "com.example.MyClass",
425      "--outputFile", "MyClass-schema.ldif"
426    };
427    examples.put(args, INFO_GEN_SCHEMA_EXAMPLE_1.get());
428
429    return examples;
430  }
431}