/* * Copyright 2017 MongoDB, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.bson.codecs.pojo; import java.lang.annotation.Annotation; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import static java.lang.String.format; import static java.util.Collections.emptyList; import static java.util.Collections.emptyMap; import static java.util.Collections.unmodifiableList; import static java.util.Collections.unmodifiableMap; import static org.bson.assertions.Assertions.notNull; import static org.bson.codecs.pojo.Conventions.DEFAULT_CONVENTIONS; import static org.bson.codecs.pojo.PojoBuilderHelper.configureClassModelBuilder; import static org.bson.codecs.pojo.PojoBuilderHelper.stateNotNull; /** * A builder for programmatically creating {@code ClassModels}. * * @param <T> The type of the class the ClassModel represents * @since 3.5 * @see ClassModel */ public class ClassModelBuilder<T> { private static final String ID_FIELD_NAME = "_id"; private final List<FieldModelBuilder<?>> fields = new ArrayList<FieldModelBuilder<?>>(); private InstanceCreatorFactory<T> instanceCreatorFactory; private Class<T> type; private Map<String, TypeParameterMap> fieldNameToTypeParameterMap = emptyMap(); private List<Convention> conventions = DEFAULT_CONVENTIONS; private List<Annotation> annotations = emptyList(); private boolean discriminatorEnabled; private String discriminator; private String discriminatorKey; private String idField; ClassModelBuilder(final Class<T> type) { configureClassModelBuilder(this, notNull("type", type)); } /** * Sets the InstanceCreatorFactory for the ClassModel * * @param instanceCreatorFactory the InstanceCreatorFactory * @return this */ public ClassModelBuilder<T> instanceCreatorFactory(final InstanceCreatorFactory<T> instanceCreatorFactory) { this.instanceCreatorFactory = notNull("instanceCreatorFactory", instanceCreatorFactory); return this; } /** * @return the InstanceCreatorFactory for the ClassModel */ public InstanceCreatorFactory<T> getInstanceCreatorFactory() { return instanceCreatorFactory; } /** * Sets the type of the model * * @param type the type of the class * @return the builder to configure the class being modeled */ public ClassModelBuilder<T> type(final Class<T> type) { this.type = notNull("type", type); return this; } /** * @return the type if set or null */ public Class<T> getType() { return type; } /** * Sets the conventions to apply to the model * * @param conventions a list of conventions * @return this */ public ClassModelBuilder<T> conventions(final List<Convention> conventions) { this.conventions = notNull("conventions", conventions); return this; } /** * @return the conventions o apply to the model */ public List<Convention> getConventions() { return conventions; } /** * Sets the annotations for the model * * @param annotations a list of annotations * @return this */ public ClassModelBuilder<T> annotations(final List<Annotation> annotations) { this.annotations = notNull("annotations", annotations); return this; } /** * @return the annotations on the modeled type if set or null */ public List<Annotation> getAnnotations() { return annotations; } /** * Sets the discriminator to be used when storing instances of the modeled type * * @param discriminator the discriminator value * @return this */ public ClassModelBuilder<T> discriminator(final String discriminator) { this.discriminator = discriminator; return this; } /** * @return the discriminator to be used when storing instances of the modeled type or null if not set */ public String getDiscriminator() { return discriminator; } /** * Sets the discriminator key to be used when storing instances of the modeled type * * @param discriminatorKey the discriminator key value * @return this */ public ClassModelBuilder<T> discriminatorKey(final String discriminatorKey) { this.discriminatorKey = discriminatorKey; return this; } /** * @return the discriminator key to be used when storing instances of the modeled type or null if not set */ public String getDiscriminatorKey() { return discriminatorKey; } /** * Enables or disables the use of a discriminator when serializing * * @param discriminatorEnabled true to enable the use of a discriminator * @return this */ public ClassModelBuilder<T> enableDiscriminator(final boolean discriminatorEnabled) { this.discriminatorEnabled = discriminatorEnabled; return this; } /** * @return true if a discriminator should be used when serializing, otherwise false */ public Boolean useDiscriminator() { return discriminatorEnabled; } /** * Designates a field as the {@code _id} field for this type. If another field is currently marked as the {@code _id} field, * that setting is cleared in favor of the named field. * * @param idField the FieldModel field name to use for the {@code _id} field * @return this */ public ClassModelBuilder<T> idField(final String idField) { this.idField = notNull("idField", idField); return this; } /** * @return the designated {@code _id} field for this type or null if not set */ public String getIdField() { return idField; } /** * Remove a field from the builder * * @param name the actual field name in the POJO and not the {@code documentFieldName}. * @return returns true if the field matched and was removed */ public boolean removeField(final String name) { return fields.remove(getField(notNull("name", name))); } /** * Gets a field by the given name. * * <p> * Note: Searches against the actual field name in the POJO and not the {@code documentFieldName}. * </p> * * @param name the name of the field to find. * @return the field or null if the field is not found */ public FieldModelBuilder<?> getField(final String name) { notNull("name", name); for (FieldModelBuilder<?> fieldModelBuilder : fields) { if (fieldModelBuilder.getFieldName().equals(name)) { return fieldModelBuilder; } } return null; } /** * @return the fields on the modeled type */ public List<FieldModelBuilder<?>> getFields() { return Collections.unmodifiableList(fields); } /** * Creates a new ClassModel instance based on the mapping data provided. * * @return the new instance */ public ClassModel<T> build() { List<FieldModel<?>> fieldModels = new ArrayList<FieldModel<?>>(); FieldModel<?> idFieldModel = null; stateNotNull("type", type); for (Convention convention : conventions) { convention.apply(this); } stateNotNull("instanceCreatorFactory", instanceCreatorFactory); if (discriminatorEnabled) { stateNotNull("discriminatorKey", discriminatorKey); stateNotNull("discriminator", discriminator); } for (FieldModelBuilder<?> fieldModelBuilder : fields) { boolean isIdField = fieldModelBuilder.getFieldName().equals(idField); if (isIdField) { fieldModelBuilder.documentFieldName(ID_FIELD_NAME); } FieldModel<?> model = fieldModelBuilder.build(); fieldModels.add(model); if (isIdField) { idFieldModel = model; } } validateFieldModels(fieldModels); return new ClassModel<T>(type, fieldNameToTypeParameterMap, instanceCreatorFactory, discriminatorEnabled, discriminatorKey, discriminator, idFieldModel, unmodifiableList(fieldModels)); } @Override public String toString() { return format("ClassModelBuilder{type=%s}", type); } Map<String, TypeParameterMap> getFieldNameToTypeParameterMap() { return fieldNameToTypeParameterMap; } ClassModelBuilder<T> fieldNameToTypeParameterMap(final Map<String, TypeParameterMap> fieldNameToTypeParameterMap) { this.fieldNameToTypeParameterMap = unmodifiableMap(new HashMap<String, TypeParameterMap>(fieldNameToTypeParameterMap)); return this; } ClassModelBuilder<T> addField(final FieldModelBuilder<?> fieldModelBuilder) { fields.add(notNull("fieldModelBuilder", fieldModelBuilder)); return this; } private void validateFieldModels(final List<FieldModel<?>> fieldModels) { Map<String, Integer> fieldNameMap = new HashMap<String, Integer>(); Map<String, Integer> fieldDocumentNameMap = new HashMap<String, Integer>(); String duplicateFieldName = null; String duplicateDocumentFieldName = null; for (FieldModel<?> fieldModel : fieldModels) { String fieldName = fieldModel.getFieldName(); if (fieldNameMap.containsKey(fieldName)) { duplicateFieldName = fieldName; break; } fieldNameMap.put(fieldName, 1); String documentFieldName = fieldModel.getDocumentFieldName(); if (fieldDocumentNameMap.containsKey(documentFieldName)) { duplicateDocumentFieldName = documentFieldName; break; } fieldDocumentNameMap.put(documentFieldName, 1); } if (idField != null && !fieldNameMap.containsKey(idField)) { throw new IllegalStateException(format("Invalid id field, field named field '%s' can not be found.", idField)); } else if (duplicateFieldName != null) { throw new IllegalStateException(format("Duplicate field named '%s' found.", duplicateFieldName)); } else if (duplicateDocumentFieldName != null) { throw new IllegalStateException(format("Duplicate document field named '%s' found.", duplicateDocumentFieldName)); } } }