/** * * Copyright (c) 2006-2017, Speedment, Inc. All Rights Reserved. * * 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 com.speedment.generator.translator; import com.speedment.common.annotation.GeneratedCode; import com.speedment.common.codegen.Generator; import static com.speedment.common.codegen.constant.DefaultJavadocTag.AUTHOR; import com.speedment.common.codegen.controller.AlignTabs; import com.speedment.common.codegen.controller.AutoImports; import static com.speedment.common.codegen.internal.util.NullUtil.requireNonNulls; import com.speedment.common.codegen.model.AnnotationUsage; import com.speedment.common.codegen.model.Class; import com.speedment.common.codegen.model.ClassOrInterface; import com.speedment.common.codegen.model.Constructor; import com.speedment.common.codegen.model.Enum; import com.speedment.common.codegen.model.Field; import com.speedment.common.codegen.model.File; import com.speedment.common.codegen.model.Interface; import com.speedment.common.codegen.model.Javadoc; import com.speedment.common.codegen.model.Value; import com.speedment.common.injector.Injector; import com.speedment.common.injector.annotation.Inject; import com.speedment.common.mapstream.MapStream; import com.speedment.generator.translator.component.TypeMapperComponent; import com.speedment.runtime.config.*; import com.speedment.runtime.config.internal.*; import com.speedment.runtime.config.trait.HasEnabled; import com.speedment.runtime.config.trait.HasId; import com.speedment.runtime.config.trait.HasMainInterface; import com.speedment.runtime.config.trait.HasName; import com.speedment.runtime.core.component.InfoComponent; import java.lang.reflect.Type; import java.util.*; import static java.util.Objects.requireNonNull; import java.util.concurrent.CopyOnWriteArrayList; import java.util.function.BiConsumer; import java.util.function.BiFunction; import java.util.function.Function; import java.util.stream.Stream; /** * * @param <DOC> document type. * @param <T> Java type (Interface, Class or Enum) to generate * * @author Per Minborg */ public abstract class AbstractJavaClassTranslator<DOC extends Document & HasId & HasName & HasEnabled & HasMainInterface, T extends ClassOrInterface<T>> implements JavaClassTranslator<DOC, T> { public static final String GETTER_METHOD_PREFIX = "get", SETTER_METHOD_PREFIX = "set", FINDER_METHOD_PREFIX = "find", JAVADOC_MESSAGE = "\n<p>\nThis file is safe to edit. It will not be overwritten by the " + "code generator."; private @Inject Generator generator; private @Inject InfoComponent infoComponent; private @Inject TypeMapperComponent typeMappers; private @Inject Injector injector; private final DOC document; private final Function<String, T> mainModelConstructor; private final List<BiConsumer<File, Builder<T>>> listeners; protected AbstractJavaClassTranslator(DOC document, Function<String, T> mainModelConstructor) { this.document = requireNonNull(document); this.mainModelConstructor = requireNonNull(mainModelConstructor); this.listeners = new CopyOnWriteArrayList<>(); } @Override public final TranslatorSupport<DOC> getSupport() { return new TranslatorSupport<>(injector, document); } @Override public final DOC getDocument() { return document; } protected AnnotationUsage generated() { final String owner = infoComponent.getTitle(); return AnnotationUsage.of(GeneratedCode.class).set(Value.ofText(owner)); } /** * Returns the name of the {@link Class}/{@link Interface}/{@link Enum} that * this translator will create. The name is only the short name. It does not * include package information. * <p> * Example: {@code HareImpl} * * @return the short filename of the file to generate */ protected abstract String getClassOrInterfaceName(); /** * Creates and configures a {@link Class}/{@link Interface}/{@link Enum} for * the specified {@code File}. This method uses a builder created using the * {@link #newBuilder(File, String)} method to make sure all the listeners * are notified when the model is built. * <p> * Observe that this method doesn't add the created model to the file. * * @param file the file to make a model for * @return the model made */ protected abstract T makeCodeGenModel(File file); /** * This method is executed after the file has been created but before it is * returned to the code generator. * * @param file the file to operate on */ protected void finializeFile(File file) { // Do nothing } @Override public File get() { final File file = File.of(getSupport().baseDirectoryName() + "/" + (isInGeneratedPackage() ? "generated/" : "") + getClassOrInterfaceName() + ".java" ); final T item = makeCodeGenModel(file); if (!item.getJavadoc().isPresent()) { item.set(getJavaDoc()); } file.add(item); finializeFile(file); file.call(new AutoImports(getCodeGenerator().getDependencyMgr())); file.call(new AlignTabs<>()); return file; } protected abstract String getJavadocRepresentText(); protected Javadoc getJavaDoc() { final String owner, message; if (isInGeneratedPackage()) { owner = infoComponent.getTitle(); message = getGeneratedJavadocMessage(); } else { owner = project().get().getCompanyName(); message = JAVADOC_MESSAGE; } return Javadoc.of(getJavadocRepresentText() + message) .add(AUTHOR.setValue(owner)); } @Override public Generator getCodeGenerator() { return generator; } @Override public boolean isInGeneratedPackage() { return false; } @Override public void onMake(BiConsumer<File, Builder<T>> action) { listeners.add(action); } @Override public Stream<BiConsumer<File, Builder<T>>> listeners() { return listeners.stream(); } protected final class BuilderImpl implements Translator.Builder<T> { private final static String PROJECTS = "projects"; private final String name; private final Map<Phase, Map<String, List<BiConsumer<T, Document>>>> map; // Special for this case private final Map<Phase, List<BiConsumer<T, ForeignKey>>> foreignKeyReferencesThisTableConsumers; public BuilderImpl(String name) { this.name = requireNonNull(name); // Phase stuff this.map = new EnumMap<>(Phase.class); this.foreignKeyReferencesThisTableConsumers = new EnumMap<>(Phase.class); for (final Phase phase : Phase.values()) { map.put(phase, new HashMap<>()); foreignKeyReferencesThisTableConsumers.put(phase, new ArrayList<>()); } } @Override public <P extends Document, D extends Document> Builder<T> forEvery(Phase phase, String key, BiFunction<P, Map<String, Object>, D> constructor, BiConsumer<T, D> consumer) { aquireListAndAdd(phase, key, wrap(consumer, constructor)); return this; } @Override public BuilderImpl forEveryProject(Phase phase, BiConsumer<T, Project> consumer) { aquireListAndAdd(phase, PROJECTS, wrap(consumer, ProjectImpl::new)); return this; } @Override public BuilderImpl forEveryDbms(Phase phase, BiConsumer<T, Dbms> consumer) { aquireListAndAdd(phase, Project.DBMSES, wrap(consumer, DbmsImpl::new)); return this; } @Override public BuilderImpl forEverySchema(Phase phase, BiConsumer<T, Schema> consumer) { aquireListAndAdd(phase, Dbms.SCHEMAS, wrap(consumer, SchemaImpl::new)); return this; } @Override public BuilderImpl forEveryTable(Phase phase, BiConsumer<T, Table> consumer) { aquireListAndAdd(phase, Schema.TABLES, wrap(consumer, TableImpl::new)); return this; } @Override public BuilderImpl forEveryColumn(Phase phase, BiConsumer<T, Column> consumer) { aquireListAndAdd(phase, Table.COLUMNS, wrap(consumer, ColumnImpl::new)); return this; } @Override public BuilderImpl forEveryIndex(Phase phase, BiConsumer<T, Index> consumer) { aquireListAndAdd(phase, Table.INDEXES, wrap(consumer, IndexImpl::new)); return this; } @Override public BuilderImpl forEveryForeignKey(Phase phase, BiConsumer<T, ForeignKey> consumer) { aquireListAndAdd(phase, Table.FOREIGN_KEYS, wrap(consumer, ForeignKeyImpl::new)); return this; } private <P extends Document, D extends Document> BiConsumer<T, Document> wrap(BiConsumer<T, D> consumer, BiFunction<P, Map<String, Object>, D> constructor) { return (t, doc) -> { @SuppressWarnings("unchecked") final P parent = (P) doc.getParent().orElse(null); consumer.accept(t, constructor.apply(parent, doc.getData())); }; } @Override public BuilderImpl forEveryForeignKeyReferencingThis(Phase phase, BiConsumer<T, ForeignKey> consumer) { foreignKeyReferencesThisTableConsumers.get(phase).add(requireNonNull(consumer)); return this; } @SuppressWarnings("unchecked") protected void aquireListAndAdd(Phase phase, String key, BiConsumer<T, Document> consumer) { aquireList(phase, key).add(requireNonNull(consumer)); } @SuppressWarnings("unchecked") protected <C extends Document> List<BiConsumer<T, C>> aquireList(Phase phase, String key) { return (List<BiConsumer<T, C>>) (List<?>) map.get(phase).computeIfAbsent(key, $ -> new CopyOnWriteArrayList<>()); } public <D extends Document & HasMainInterface> void act(Phase phase, String key, T item, D document) { aquireList(phase, key).forEach(c -> c.accept(requireNonNull(item), requireNonNull(document)) ); } @Override public T build() { final T model = mainModelConstructor.apply(name); if (isInGeneratedPackage()) { model.add(generated()); } for (Phase phase : Phase.values()) { project().ifPresent(p -> act(phase, PROJECTS, model, p)); dbms().ifPresent(d -> act(phase, Project.DBMSES, model, d)); schema().ifPresent(s -> act(phase, Dbms.SCHEMAS, model, s)); table().ifPresent(t -> act(phase, Schema.TABLES, model, t)); MapStream.of(map.get(phase)) .flatMapValue(List::stream) .forEachOrdered((key, actor) -> table() .ifPresent(table -> MapStream.of(table.getData()) .filterKey(key::equals) // Filter out elements that map to a list and flat // map the stream so that every value in the list // becomes an element of the stream. .filterValue(List.class::isInstance) .mapValue(v -> { @SuppressWarnings("unchecked") final List<Map<String, Object>> val = (List<Map<String, Object>>) v; return val; }) .flatMapValue(List::stream) // The foreignKeys-property is special in that only // keys that reference enabled and existing table // and columns are to be included. .filter((k, v) -> { if (Table.FOREIGN_KEYS.equals(k)) { return new ForeignKeyImpl(table, v) .foreignKeyColumns() .map(ForeignKeyColumn::findColumn) .allMatch(c -> c.filter(Column::isEnabled) .map(Column::getParentOrThrow) .filter(Table::isEnabled) .isPresent() ); // Indexes, PrimaryKeyColumns and Columns should // be handled as usual. } else return true; }) // We are now done with the keys. Use the values of // the stream to produce an ordinary stream of base // documents. .values() .map(data -> new BaseDocument(table, data)) // All the documents that are enabled should be // passed to the actor. .filter(HasEnabled::test) .forEachOrdered(c -> actor.accept(model, c)) ) ); if (Table.class.equals(getDocument().mainInterface())) { schema().ifPresent(schema -> schema.tables() .filter(HasEnabled::test) .flatMap(t -> t.foreignKeys()) .filter(HasEnabled::test) .filter(fk -> fk.foreignKeyColumns() .filter(fkc -> fkc.getForeignTableName().equals(getDocument().getId())) .filter(HasEnabled::test) .filter(fkc -> fkc.findForeignColumn().map(HasEnabled::test).orElse(false)) .findFirst() .isPresent() ).forEachOrdered(fk -> foreignKeyReferencesThisTableConsumers.get(phase).forEach( c -> c.accept(model, fk) ) ) ); } } return model; } } protected final Builder<T> newBuilder(File file, String className) { requireNonNulls(file, className); final Builder<T> builder = new BuilderImpl(className); listeners().forEachOrdered(action -> action.accept(file, builder)); return builder; } public Field fieldFor(Column c) { return Field.of( getSupport().variableName(c), typeMappers.get(c).getJavaType(c) ); } public Constructor emptyConstructor() { return Constructor.of().public_(); } public enum CopyConstructorMode { SETTER, FIELD } public Constructor copyConstructor(Type type, CopyConstructorMode mode) { final TranslatorSupport<DOC> support = getSupport(); final Constructor constructor = Constructor.of().protected_() .add(Field.of(support.variableName(), type)); columns().forEachOrdered(c -> { switch (mode) { case FIELD: { constructor.add( "this." + support.variableName(c) + " = " + support.variableName() + "." + GETTER_METHOD_PREFIX + support.typeName(c) + "();"); break; } case SETTER: { if (c.isNullable()) { constructor.add( support.variableName() + "." + GETTER_METHOD_PREFIX + support.typeName(c) + "().ifPresent(this::" + SETTER_METHOD_PREFIX + support.typeName(c) + ");" ); } else { constructor.add( SETTER_METHOD_PREFIX + support.typeName(c) + "(" + support.variableName() + ".get" + support.typeName(c) + "());" ); } break; } default: throw new UnsupportedOperationException( "Unknown mode '" + mode + "'." ); } }); return constructor; } protected String getGeneratedJavadocMessage() { return "\n<p>\nThis file has been automatically generated by " + infoComponent.getTitle() + ". Any changes made to it will be overwritten."; } }