org.ldaptive.beans.generate.BeanGenerator Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of ldaptive-beans Show documentation
Show all versions of ldaptive-beans Show documentation
Provides a mapping, persistence, and code generation API for reading and writing POJOs to an LDAP directory
/* See LICENSE for licensing and NOTICE for copyright. */
package org.ldaptive.beans.generate;
import java.io.File;
import java.io.IOException;
import java.security.cert.Certificate;
import java.time.ZonedDateTime;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import java.util.UUID;
import java.util.stream.Collectors;
import com.sun.codemodel.JAnnotationArrayMember;
import com.sun.codemodel.JAnnotationUse;
import com.sun.codemodel.JClass;
import com.sun.codemodel.JClassAlreadyExistsException;
import com.sun.codemodel.JCodeModel;
import com.sun.codemodel.JConditional;
import com.sun.codemodel.JDefinedClass;
import com.sun.codemodel.JDocComment;
import com.sun.codemodel.JExpr;
import com.sun.codemodel.JExpression;
import com.sun.codemodel.JFieldVar;
import com.sun.codemodel.JInvocation;
import com.sun.codemodel.JMethod;
import com.sun.codemodel.JMod;
import com.sun.codemodel.JVar;
import org.ldaptive.beans.generate.props.BeanGeneratorPropertySource;
import org.ldaptive.schema.AttributeType;
import org.ldaptive.schema.AttributeUsage;
import org.ldaptive.schema.ObjectClass;
import org.ldaptive.schema.Schema;
import org.ldaptive.schema.Syntax;
/**
* Utility class for creating Java POJOs from an LDAP schema. Sample usage:
*
*
Schema schema = new Schema(new DefaultConnectionFactory(
"ldap://directory.ldaptive.org"));
BeanGenerator generator = new BeanGenerator(
schema,
"com.my.package",
new String[] {"inetOrgPerson"});
generator.generate();
generator.write();
*
*
* @author Middleware Services
*/
public class BeanGenerator
{
/** Code model for java class creation. */
private final JCodeModel codeModel = new JCodeModel();
/** Schema to generate beans from. */
private Schema schema;
/** Package to create beans in. */
private String packageName;
/** Object classes to build beans for. */
private String[] objectClasses;
/** Whether to include optional attributes. */
private boolean useOptionalAttributes = true;
/** Whether to include operational attributes. */
private boolean useOperationalAttributes;
/** Whether to include superior classes for each object class. */
private boolean includeSuperiorClasses;
/** Mapping to determine attribute value type. */
private Map> typeMappings = getDefaultTypeMappings();
/** Name mappings. */
private Map nameMappings = new HashMap<>();
/** Excluded names. */
private String[] excludedNames = new String[0];
/** Default constructor. */
public BeanGenerator() {}
/**
* Creates a new bean generator. A bean will be generated for each supplied object class.
*
* @param s schema containing directory data for generation
* @param name package name to place the generated classes in
* @param oc object classes to generate beans for
*/
public BeanGenerator(final Schema s, final String name, final String[] oc)
{
schema = s;
packageName = name;
objectClasses = oc;
}
/**
* Returns the schema.
*
* @return schema
*/
public Schema getSchema()
{
return schema;
}
/**
* Sets the schema.
*
* @param s schema
*/
public void setSchema(final Schema s)
{
schema = s;
}
/**
* Returns the package name where beans will be generated.
*
* @return package name
*/
public String getPackageName()
{
return packageName;
}
/**
* Sets the package name where beans will be generated.
*
* @param name package name
*/
public void setPackageName(final String name)
{
packageName = name;
}
/**
* Returns the object classes. A class is generated for each object class.
*
* @return object classes
*/
public String[] getObjectClasses()
{
return objectClasses;
}
/**
* Sets the object classes. A class is generated for each object class.
*
* @param oc object classes
*/
public void setObjectClasses(final String... oc)
{
objectClasses = oc;
}
/**
* Returns whether to include optional attributes in bean generation.
*
* @return whether to include optional attributes
*/
public boolean isUseOptionalAttributes()
{
return useOptionalAttributes;
}
/**
* Sets whether to include optional attributes in bean generation.
*
* @param b whether to include optional attributes
*/
public void setUseOptionalAttributes(final boolean b)
{
useOptionalAttributes = b;
}
/**
* Returns whether to include operational attributes in bean generation.
*
* @return whether to include operational attributes
*/
public boolean isUseOperationalAttributes()
{
return useOperationalAttributes;
}
/**
* Sets whether to include operational attributes in bean generation.
*
* @param b whether to include operational attributes
*/
public void setUseOperationalAttributes(final boolean b)
{
useOperationalAttributes = b;
}
/**
* Returns whether to include superior classes in bean generation.
*
* @return whether to include superior classes attributes
*/
public boolean isIncludeSuperiorClasses()
{
return includeSuperiorClasses;
}
/**
* Sets whether to include superior classes in bean generation.
*
* @param b whether to include superior classes
*/
public void setIncludeSuperiorClasses(final boolean b)
{
includeSuperiorClasses = b;
}
/**
* Returns the type mappings. Type mappings is syntax OID to class type and is used to determine field type in the
* generated POJOs.
*
* @return type mappings
*/
public Map> getTypeMappings()
{
return typeMappings;
}
/**
* Sets the type mappings. Type mappings is syntax OID to class type and is used to determine field type in the
* generated POJOs.
*
* @param m type mappings
*/
public void setTypeMappings(final Map> m)
{
typeMappings = m;
}
/**
* Returns the mapping of directory attribute name to bean property. This property is used to override the default
* schema name. For instance, you may prefer using 'countryName' to 'c', which would be set as 'c'=>'countryName'.
*
* @return attribute name to bean property mapping
*/
public Map getNameMappings()
{
return nameMappings;
}
/**
* Sets the mapping of directory attribute name to bean property.
*
* @param m name mappings
*
* @throws NullPointerException if m is null
*/
public void setNameMappings(final Map m)
{
if (m == null) {
throw new NullPointerException("Name mappings cannot be null");
}
nameMappings = m;
}
/**
* Returns the attribute names to exclude from bean generation. Excludes an attribute from the generated POJO. For
* instance, you may not want 'userPassword' included in your bean.
*
* @return attribute names to exclude
*/
public String[] getExcludedNames()
{
return excludedNames;
}
/**
* Sets the attribute names to exclude from bean generation.
*
* @param names to exclude
*
* @throws NullPointerException if names is null
*/
public void setExcludedNames(final String... names)
{
if (names == null) {
throw new NullPointerException("Excluded names cannot be null");
}
excludedNames = names;
}
/**
* Returns the default syntax types used to determine attribute property type.
*
* @return map of syntax OID to class type
*/
protected static Map> getDefaultTypeMappings()
{
final Map> m = new HashMap<>();
m.put("1.3.6.1.4.1.1466.115.121.1.7", Boolean.class);
m.put("1.3.6.1.4.1.1466.115.121.1.5", byte[].class);
m.put("1.3.6.1.4.1.1466.115.121.1.8", Certificate.class);
m.put("1.3.6.1.4.1.1466.115.121.1.24", ZonedDateTime.class);
m.put("1.3.6.1.4.1.1466.115.121.1.36", Integer.class);
m.put("1.3.6.1.1.16.1", UUID.class);
return m;
}
/**
* Returns the class for the supplied attribute type and syntax. If the attribute type syntax OID is found in the
* default type mapping it is used. Otherwise if the syntax is "X-NOT-HUMAN-READABLE", a byte array is used.
*
* @param type attribute type
* @param syntax associated with the attribute type
*
* @return syntax type
*/
protected Class> getSyntaxType(final AttributeType type, final Syntax syntax)
{
Class> t = null;
for (Map.Entry> entry : typeMappings.entrySet()) {
if (entry.getKey().equals(type.getSyntaxOID(false))) {
t = entry.getValue();
}
}
if (t == null) {
if (Syntax.containsBooleanExtension(syntax, "X-NOT-HUMAN-READABLE")) {
t = byte[].class;
} else {
t = String.class;
}
}
return t;
}
/**
* Generates a class for each configured object class. See {@link #objectClasses}. {@link #write(String)} must be
* invoked to write the classes to disk.
*/
public void generate()
{
for (String objectClass : objectClasses) {
final JDefinedClass definedClass = createClass(packageName, objectClass);
final JDocComment jDocComment = definedClass.javadoc();
jDocComment.add(String.format("Ldaptive generated bean for objectClass '%s'", objectClass));
final ObjectClass oc = schema.getObjectClass(objectClass);
final Set attributeNames = getAttributeNames(oc);
if (useOperationalAttributes) {
attributeNames.addAll(
schema.getAttributeTypes().stream().filter(
type -> AttributeUsage.DIRECTORY_OPERATION.equals(type.getUsage())).map(
AttributeType::getName).collect(Collectors.toList()));
}
final Map mutators = new TreeMap<>();
for (String name : attributeNames) {
final AttributeType type = schema.getAttributeType(name);
if (!isNameExcluded(type)) {
if (nameMappings.containsKey(type.getName())) {
mutators.put(nameMappings.get(type.getName()), type);
} else {
mutators.put(formatAttributeName(type.getName()), type);
}
}
}
// add entry annotation
final JAnnotationUse entryAnnotation = definedClass.annotate(codeModel.ref(org.ldaptive.beans.Entry.class));
entryAnnotation.param("dn", "dn");
final JAnnotationArrayMember attrArray = entryAnnotation.paramArray("attributes");
// add mutator for the DN
createMutators(definedClass, "dn", String.class, false);
// add mutators for each attribute
for (Map.Entry mutator : mutators.entrySet()) {
final Class> syntaxType = getSyntaxType(
mutator.getValue(),
schema.getSyntax(mutator.getValue().getSyntaxOID(false)));
createMutators(definedClass, mutator.getKey(), syntaxType, !mutator.getValue().isSingleValued());
// add attribute annotation
final JAnnotationUse attrAnnotation = attrArray.annotate(org.ldaptive.beans.Attribute.class);
attrAnnotation.param("name", mutator.getValue().getName());
if (!mutator.getKey().equals(mutator.getValue().getName())) {
attrAnnotation.param("property", mutator.getKey());
}
if (byte[].class.equals(syntaxType)) {
attrAnnotation.param("binary", true);
}
}
// create additional methods
createHashCode(definedClass);
createEquals(definedClass);
createToString(definedClass);
}
}
/**
* Returns the attribute names to use for the supplied object class. See {@link #getAttributeNames(ObjectClass, Set)}.
*
* @param objectClass to retrieve names from
*
* @return set of all attribute names used for bean generation
*/
private Set getAttributeNames(final ObjectClass objectClass)
{
return getAttributeNames(objectClass, new HashSet<>());
}
/**
* Returns the attribute names to use for the supplied object class. This method is invoked recursively if superior
* classes are included.
*
* @param objectClass to retrieve names from
* @param processed object classes that have already been processed
*
* @return set of all attribute names used for bean generation
*/
private Set getAttributeNames(final ObjectClass objectClass, final Set processed)
{
final Set attributeNames = new HashSet<>();
if (objectClass != null) {
if (objectClass.getRequiredAttributes() != null) {
attributeNames.addAll(Arrays.asList(objectClass.getRequiredAttributes()));
}
if (useOptionalAttributes && objectClass.getOptionalAttributes() != null) {
attributeNames.addAll(Arrays.asList(objectClass.getOptionalAttributes()));
}
processed.add(objectClass);
if (includeSuperiorClasses && objectClass.getSuperiorClasses() != null) {
for (String oc : objectClass.getSuperiorClasses()) {
final ObjectClass superiorOc = schema.getObjectClass(oc);
if (!processed.contains(superiorOc)) {
attributeNames.addAll(getAttributeNames(superiorOc, processed));
}
}
}
}
return attributeNames;
}
/**
* Formats the supplied name for use as a Java property.
*
* @param name to format
*
* @return formatted name
*/
private String formatAttributeName(final String name)
{
final String formatted;
if (name.contains("-")) {
formatted = name.replace("-", "");
} else {
formatted = name;
}
return formatted;
}
/**
* Returns whether the supplied attribute type has a matching OID or name in the excluded names list.
*
* @param type to compare
*
* @return whether attribute type should be excluded from bean generation
*/
private boolean isNameExcluded(final AttributeType type)
{
if (excludedNames != null && excludedNames.length > 0) {
for (String excluded : excludedNames) {
if (type.getOID().equals(excluded) || type.hasName(excluded)) {
return true;
}
}
}
return false;
}
/**
* Creates a class in the supplied package.
*
* @param classPackage to place the class in
* @param className to create
*
* @return class
*
* @throws IllegalArgumentException if the class already exists
*/
protected JDefinedClass createClass(final String classPackage, final String className)
{
final String fqClassName;
if (!Character.isUpperCase(className.charAt(0))) {
fqClassName = String.format(
"%s.%s",
classPackage,
className.substring(0, 1).toUpperCase() + className.substring(1));
} else {
fqClassName = String.format("%s.%s", classPackage, className);
}
try {
return codeModel._class(fqClassName);
} catch (JClassAlreadyExistsException e) {
throw new IllegalArgumentException("Class already exists: " + fqClassName, e);
}
}
/**
* Creates the getter and setter methods on the supplied class for the supplied name.
*
* @param clazz to put getter and setter methods on
* @param name of the property
* @param syntaxType of the property
* @param multivalue whether this property is a collection
*/
protected void createMutators(
final JDefinedClass clazz,
final String name,
final Class> syntaxType,
final boolean multivalue)
{
final String upperName = name.substring(0, 1).toUpperCase() + name.substring(1);
if (multivalue) {
final JClass detailClass = codeModel.ref(syntaxType);
final JClass collectionClass = codeModel.ref(Collection.class);
final JClass genericClass = collectionClass.narrow(detailClass);
final JFieldVar field = clazz.field(JMod.PRIVATE, genericClass, name);
final JMethod getterMethod = clazz.method(JMod.PUBLIC, genericClass, "get" + upperName);
getterMethod.body()._return(field);
final JMethod setterMethod = clazz.method(JMod.PUBLIC, Void.TYPE, "set" + upperName);
setterMethod.param(genericClass, "c");
setterMethod.body().assign(JExpr._this().ref(name), JExpr.ref("c"));
} else {
final JFieldVar field = clazz.field(JMod.PRIVATE, syntaxType, name);
final JMethod getterMethod = clazz.method(JMod.PUBLIC, syntaxType, "get" + upperName);
getterMethod.body()._return(field);
final JMethod setterMethod = clazz.method(JMod.PUBLIC, Void.TYPE, "set" + upperName);
setterMethod.param(syntaxType, "s");
setterMethod.body().assign(JExpr._this().ref(name), JExpr.ref("s"));
}
}
/**
* Creates the hashCode method on the supplied class. Leverages {@link org.ldaptive.LdapUtils#computeHashCode(int,
* Object...)}.
*
* @param clazz to put hashCode method on
*/
private void createHashCode(final JDefinedClass clazz)
{
final JClass ldapUtilsClass = codeModel.ref(org.ldaptive.LdapUtils.class);
final JInvocation computeHashCode = ldapUtilsClass.staticInvoke("computeHashCode");
final JMethod hashCode = clazz.method(JMod.PUBLIC, int.class, "hashCode");
hashCode.annotate(java.lang.Override.class);
// CheckStyle:MagicNumber OFF
computeHashCode.arg(JExpr.lit(7919));
// CheckStyle:MagicNumber ON
for (Map.Entry entry : clazz.fields().entrySet()) {
computeHashCode.arg(JExpr._this().ref(entry.getValue()));
}
hashCode.body()._return(computeHashCode);
}
/**
* Creates the equals method on the supplied class. Leverages {@link org.ldaptive.LdapUtils#areEqual(Object, Object)}.
*
* @param clazz to put equals method on
*/
private void createEquals(final JDefinedClass clazz)
{
final JMethod equals = clazz.method(JMod.PUBLIC, boolean.class, "equals");
equals.annotate(java.lang.Override.class);
final JVar o = equals.param(Object.class, "o");
final JConditional ifSame = equals.body()._if(o.eq(JExpr._this()));
ifSame._then()._return(JExpr.TRUE);
final JConditional ifInstance = equals.body()._if(o._instanceof(clazz));
final JVar v = ifInstance._then().decl(clazz, "v", JExpr.cast(clazz, o));
JExpression propertyComparison = null;
for (Map.Entry entry : clazz.fields().entrySet()) {
final JClass ldapUtilsClass = codeModel.ref(org.ldaptive.LdapUtils.class);
final JInvocation areEqual = ldapUtilsClass.staticInvoke("areEqual");
areEqual.arg(entry.getValue());
areEqual.arg(v.ref(entry.getValue()));
if (propertyComparison == null) {
propertyComparison = areEqual;
} else {
propertyComparison = propertyComparison.cand(areEqual);
}
}
ifInstance._then()._return(propertyComparison);
equals.body()._return(JExpr.FALSE);
}
/**
* Creates the toString method on the supplied class. Creates a string that contains every property on the generated
* bean.
*
* @param clazz to put toString method on
*/
private void createToString(final JDefinedClass clazz)
{
final JClass stringClass = codeModel.ref(java.lang.String.class);
final JInvocation format = stringClass.staticInvoke("format");
final JMethod toString = clazz.method(JMod.PUBLIC, String.class, "toString");
toString.annotate(java.lang.Override.class);
final StringBuilder sb = new StringBuilder("[%s@%d::");
for (Map.Entry entry : clazz.fields().entrySet()) {
sb.append(entry.getKey()).append("=%s, ");
}
sb.setLength(sb.length() - 2);
sb.append("]");
format.arg(sb.toString());
format.arg(JExpr._this().invoke("getClass").invoke("getName"));
format.arg(JExpr._this().invoke("hashCode"));
for (Map.Entry entry : clazz.fields().entrySet()) {
format.arg(JExpr._this().ref(entry.getValue()));
}
toString.body()._return(format);
}
/**
* Writes the generated classes to disk. Invokes {@link #write(String)} with ".".
*
* @throws IOException if the write fails
*/
public void write()
throws IOException
{
write(".");
}
/**
* Writes the generated classes to disk at the supplied path.
*
* @param path to write the classes to
*
* @throws IOException if the write fails
*/
public void write(final String path)
throws IOException
{
final File f = new File(path);
if (!f.exists()) {
f.mkdirs();
}
codeModel.build(f);
}
/**
* Provides command line access to a {@link BeanGenerator}. Expects two arguments:
*
*
* - path to a configuration property file
* - target directory to write files to
*
*
* A sample configuration property file looks like:
*
*
org.ldaptive.packageName=my.package.ldap.beans
org.ldaptive.objectClasses=eduPerson
org.ldaptive.nameMappings=c=countryName,l=localityName
org.ldaptive.excludedNames=userPassword
org.ldaptive.ldapUrl=ldap://directory.ldaptive.org
*
*
* @param args command line arguments
*
* @throws Exception if any error occurs
*/
public static void main(final String[] args)
throws Exception
{
final String propsPath = args[0];
final String targetPath = args[1];
final BeanGenerator generator = new BeanGenerator();
final BeanGeneratorPropertySource source = new BeanGeneratorPropertySource(generator, propsPath);
source.initialize();
generator.generate();
generator.write(targetPath);
}
/**
* Creates a builder for this class.
*
* @return new builder
*/
public static Builder builder()
{
return new Builder();
}
// CheckStyle:OFF
public static class Builder
{
private final BeanGenerator object = new BeanGenerator();
protected Builder() {}
public Builder schema(final Schema schema)
{
object.setSchema(schema);
return this;
}
public Builder packageName(final String name)
{
object.setPackageName(name);
return this;
}
public Builder objectClasses(final String... classes)
{
object.setObjectClasses(classes);
return this;
}
public Builder useOptionalAttributes(final boolean b)
{
object.setUseOptionalAttributes(b);
return this;
}
public Builder useOperationalAttributes(final boolean b)
{
object.setUseOperationalAttributes(b);
return this;
}
public Builder includeSuperiorClasses(final boolean b)
{
object.setIncludeSuperiorClasses(b);
return this;
}
public Builder typeMappings(final Map> mappings)
{
object.setTypeMappings(mappings);
return this;
}
public Builder nameMappings(final Map mappings)
{
object.setNameMappings(mappings);
return this;
}
public Builder excludedNames(final String... classes)
{
object.setExcludedNames(classes);
return this;
}
public BeanGenerator build()
{
return object;
}
}
// CheckStyle:ON
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy