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