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  protected boolean supportsDebugLogging()
262  {
263    return true;
264  }
265
266
267
268  /**
269   * {@inheritDoc}
270   */
271  @Override()
272  public void addToolArguments(@NotNull final ArgumentParser parser)
273         throws ArgumentException
274  {
275    classNameArg = new StringArgument('c', "javaClass", true, 1,
276         INFO_GEN_SCHEMA_VALUE_PLACEHOLDER_CLASS.get(),
277         INFO_GEN_SCHEMA_ARG_DESCRIPTION_JAVA_CLASS.get());
278    classNameArg.addLongIdentifier("java-class", true);
279    parser.addArgument(classNameArg);
280
281    outputFileArg = new FileArgument('f', "outputFile", true, 1,
282         INFO_GEN_SCHEMA_VALUE_PLACEHOLDER_PATH.get(),
283         INFO_GEN_SCHEMA_ARG_DESCRIPTION_OUTPUT_FILE.get(), false, true, true,
284         false);
285    outputFileArg.addLongIdentifier("output-file", true);
286    parser.addArgument(outputFileArg);
287
288    modifyFormatArg = new BooleanArgument('m', "modifyFormat",
289         INFO_GEN_SCHEMA_ARG_DESCRIPTION_MODIFY_FORMAT.get());
290    modifyFormatArg.addLongIdentifier("modify-format", true);
291    parser.addArgument(modifyFormatArg);
292  }
293
294
295
296  /**
297   * {@inheritDoc}
298   */
299  @Override()
300  @NotNull()
301  public ResultCode doToolProcessing()
302  {
303    // Load the specified Java class.
304    final String className = classNameArg.getValue();
305    final Class<?> targetClass;
306    try
307    {
308      targetClass = Class.forName(className);
309    }
310    catch (final Exception e)
311    {
312      Debug.debugException(e);
313      err(ERR_GEN_SCHEMA_CANNOT_LOAD_CLASS.get(className));
314      return ResultCode.PARAM_ERROR;
315    }
316
317
318    // Create an LDAP persister for the class and use it to ensure that the
319    // class is valid.
320    final LDAPPersister<?> persister;
321    try
322    {
323      persister = LDAPPersister.getInstance(targetClass);
324    }
325    catch (final Exception e)
326    {
327      Debug.debugException(e);
328      err(ERR_GEN_SCHEMA_INVALID_CLASS.get(className,
329           StaticUtils.getExceptionMessage(e)));
330      return ResultCode.LOCAL_ERROR;
331    }
332
333
334    // Use the persister to generate the attribute type and object class
335    // definitions.
336    final List<AttributeTypeDefinition> attrTypes;
337    try
338    {
339      attrTypes = persister.constructAttributeTypes();
340    }
341    catch (final Exception e)
342    {
343      Debug.debugException(e);
344      err(ERR_GEN_SCHEMA_ERROR_CONSTRUCTING_ATTRS.get(className,
345           StaticUtils.getExceptionMessage(e)));
346      return ResultCode.LOCAL_ERROR;
347    }
348
349    final List<ObjectClassDefinition> objectClasses;
350    try
351    {
352      objectClasses = persister.constructObjectClasses();
353    }
354    catch (final Exception e)
355    {
356      Debug.debugException(e);
357      err(ERR_GEN_SCHEMA_ERROR_CONSTRUCTING_OCS.get(className,
358           StaticUtils.getExceptionMessage(e)));
359      return ResultCode.LOCAL_ERROR;
360    }
361
362
363    // Convert the attribute type and object class definitions into their
364    // appropriate string representations.
365    int i=0;
366    final ASN1OctetString[] attrTypeValues =
367         new ASN1OctetString[attrTypes.size()];
368    for (final AttributeTypeDefinition d : attrTypes)
369    {
370      attrTypeValues[i++] = new ASN1OctetString(d.toString());
371    }
372
373    i=0;
374    final ASN1OctetString[] ocValues =
375         new ASN1OctetString[objectClasses.size()];
376    for (final ObjectClassDefinition d : objectClasses)
377    {
378      ocValues[i++] = new ASN1OctetString(d.toString());
379    }
380
381
382    // Construct the LDIF record to be written.
383    final LDIFRecord schemaRecord;
384    if (modifyFormatArg.isPresent())
385    {
386      schemaRecord = new LDIFModifyChangeRecord("cn=schema",
387           new Modification(ModificationType.ADD, "attributeTypes",
388                attrTypeValues),
389           new Modification(ModificationType.ADD, "objectClasses", ocValues));
390    }
391    else
392    {
393      schemaRecord = new Entry("cn=schema",
394           new Attribute("objectClass", "top", "ldapSubentry", "subschema"),
395           new Attribute("cn", "schema"),
396           new Attribute("attributeTypes", attrTypeValues),
397           new Attribute("objectClasses", ocValues));
398    }
399
400
401    // Write the schema entry to the specified file.
402    final File outputFile = outputFileArg.getValue();
403    try
404    {
405      final LDIFWriter ldifWriter = new LDIFWriter(outputFile);
406      ldifWriter.writeLDIFRecord(schemaRecord);
407      ldifWriter.close();
408    }
409    catch (final Exception e)
410    {
411      Debug.debugException(e);
412      err(ERR_GEN_SCHEMA_CANNOT_WRITE_SCHEMA.get(outputFile.getAbsolutePath(),
413           StaticUtils.getExceptionMessage(e)));
414      return ResultCode.LOCAL_ERROR;
415    }
416
417
418    return ResultCode.SUCCESS;
419  }
420
421
422
423  /**
424   * {@inheritDoc}
425   */
426  @Override()
427  @NotNull()
428  public LinkedHashMap<String[],String> getExampleUsages()
429  {
430    final LinkedHashMap<String[],String> examples =
431         new LinkedHashMap<>(StaticUtils.computeMapCapacity(1));
432
433    final String[] args =
434    {
435      "--javaClass", "com.example.MyClass",
436      "--outputFile", "MyClass-schema.ldif"
437    };
438    examples.put(args, INFO_GEN_SCHEMA_EXAMPLE_1.get());
439
440    return examples;
441  }
442}