| /* |
| * Copyright (c) 2010-2021 BSI Business Systems Integration AG. |
| * All rights reserved. This program and the accompanying materials |
| * are made available under the terms of the Eclipse Public License v1.0 |
| * which accompanies this distribution, and is available at |
| * https://www.eclipse.org/legal/epl-v10.html |
| * |
| * Contributors: |
| * BSI Business Systems Integration AG - initial API and implementation |
| */ |
| package org.eclipse.scout.sdk.core.s.dto; |
| |
| import static java.util.Comparator.comparing; |
| import static java.util.stream.Collectors.toSet; |
| import static org.eclipse.scout.sdk.core.model.api.Flags.isAbstract; |
| |
| import java.beans.Introspector; |
| import java.util.Arrays; |
| import java.util.Optional; |
| import java.util.function.Predicate; |
| import java.util.stream.Stream; |
| |
| import org.eclipse.scout.sdk.core.builder.java.IJavaSourceBuilder; |
| import org.eclipse.scout.sdk.core.generator.annotation.AnnotationGenerator; |
| import org.eclipse.scout.sdk.core.generator.annotation.IAnnotationGenerator; |
| import org.eclipse.scout.sdk.core.generator.field.FieldGenerator; |
| import org.eclipse.scout.sdk.core.generator.method.MethodGenerator; |
| import org.eclipse.scout.sdk.core.generator.methodparam.MethodParameterGenerator; |
| import org.eclipse.scout.sdk.core.generator.type.ITypeGenerator; |
| import org.eclipse.scout.sdk.core.generator.type.TypeGenerator; |
| import org.eclipse.scout.sdk.core.model.api.IAnnotatable; |
| import org.eclipse.scout.sdk.core.model.api.IAnnotation; |
| import org.eclipse.scout.sdk.core.model.api.IJavaEnvironment; |
| import org.eclipse.scout.sdk.core.model.api.IMethod; |
| import org.eclipse.scout.sdk.core.model.api.IType; |
| import org.eclipse.scout.sdk.core.model.api.PropertyBean; |
| import org.eclipse.scout.sdk.core.s.ISdkConstants; |
| import org.eclipse.scout.sdk.core.s.annotation.DataAnnotationDescriptor; |
| import org.eclipse.scout.sdk.core.s.annotation.ExtendsAnnotation; |
| import org.eclipse.scout.sdk.core.s.annotation.FormDataAnnotationDescriptor; |
| import org.eclipse.scout.sdk.core.s.annotation.ReplaceAnnotation; |
| import org.eclipse.scout.sdk.core.s.apidef.IScoutAnnotationApi; |
| import org.eclipse.scout.sdk.core.s.apidef.IScoutApi; |
| import org.eclipse.scout.sdk.core.s.generator.annotation.ScoutAnnotationGenerator; |
| import org.eclipse.scout.sdk.core.s.generator.method.ScoutMethodGenerator; |
| import org.eclipse.scout.sdk.core.util.Ensure; |
| import org.eclipse.scout.sdk.core.util.JavaTypes; |
| import org.eclipse.scout.sdk.core.util.SdkException; |
| import org.eclipse.scout.sdk.core.util.Strings; |
| |
| /** |
| * <h3>{@link AbstractDtoGenerator}</h3> |
| * |
| * @since 3.10.0 2013-08-27 |
| */ |
| public abstract class AbstractDtoGenerator<TYPE extends AbstractDtoGenerator<TYPE>> extends TypeGenerator<TYPE> { |
| |
| public static final String FORMDATA_CLASSID_SUFFIX = "-formdata"; |
| private final IType m_modelType; |
| private final IJavaEnvironment m_targetEnvironment; |
| private final IScoutApi m_scoutApi; |
| private boolean m_setupExecuted; |
| |
| protected AbstractDtoGenerator(IType modelType, IJavaEnvironment targetEnvironment) { |
| m_modelType = Ensure.notNull(modelType); |
| m_targetEnvironment = Ensure.notNull(targetEnvironment); |
| m_scoutApi = modelType.javaEnvironment().requireApi(IScoutApi.class); |
| } |
| |
| @Override |
| protected void build(IJavaSourceBuilder<?> builder) { |
| if (!m_setupExecuted) { |
| setupBuilder(); |
| m_setupExecuted = true; |
| } |
| super.build(builder); |
| } |
| |
| public IScoutApi scoutApi() { |
| return m_scoutApi; |
| } |
| |
| protected void copyAnnotations() { |
| copyAnnotations(modelType(), this, targetEnvironment()); |
| } |
| |
| private static void copyAnnotations(IAnnotatable annotationOwner, ITypeGenerator<?> target, IJavaEnvironment targetEnv) { |
| var scoutApi = annotationOwner.javaEnvironment().requireApi(IScoutApi.class); |
| annotationOwner.annotations().stream() |
| .filter(a -> isAnnotationDtoRelevant(a.type(), scoutApi)) |
| .filter(a -> targetEnv.exists(a.type())) |
| .map(AbstractDtoGenerator::toDtoAnnotationGenerator) |
| .forEach(target::withAnnotation); |
| } |
| |
| /** |
| * Override original ClassId value |
| */ |
| private static IAnnotationGenerator<?> toDtoAnnotationGenerator(IAnnotation a) { |
| var result = a.toWorkingCopy(); |
| var classIdApi = a.javaEnvironment().requireApi(IScoutApi.class).ClassId(); |
| if (classIdApi.fqn().equals(a.type().name())) { |
| var valueElementName = classIdApi.valueElementName(); |
| var id = a.element(valueElementName).get().value().as(String.class); |
| result.withElement(valueElementName, b -> b.stringLiteral(id + FORMDATA_CLASSID_SUFFIX)); |
| } |
| return result; |
| } |
| |
| private static boolean isAnnotationDtoRelevant(IType annotationType, IScoutAnnotationApi scoutApi) { |
| if (annotationType == null) { |
| return false; |
| } |
| var elementName = annotationType.name(); |
| var isDtoAnnotation = elementName.equals(scoutApi.FormData().fqn()) |
| || elementName.equals(scoutApi.PageData().fqn()) |
| || elementName.equals(scoutApi.Data().fqn()); |
| return !isDtoAnnotation |
| && !elementName.equals(scoutApi.Order().fqn()) |
| && annotationType.annotations().withName(scoutApi.DtoRelevant().fqn()).existsAny(); |
| } |
| |
| /** |
| * Adds all interfaces as specified in the given {@link FormDataAnnotationDescriptor}.<br> |
| * For all methods that also exist in the interfaces added and {@link Override} annotation is added as well. |
| * |
| * @param formDataAnnotation |
| * The {@link FormDataAnnotationDescriptor} holding the interfaces. |
| */ |
| protected TYPE withAdditionalInterfaces(FormDataAnnotationDescriptor formDataAnnotation) { |
| var interfaces = formDataAnnotation.getInterfaces(); |
| if (interfaces.isEmpty()) { |
| return thisInstance(); |
| } |
| |
| var javaEnvironment = targetEnvironment(); |
| var allSuperInterfaceMethods = interfaces.stream() |
| .filter(javaEnvironment::exists) |
| .peek(ifcType -> withInterface(ifcType.reference())) |
| .flatMap(ifcType -> ifcType.methods().withSuperTypes(true).stream()) |
| .map(IMethod::identifier) |
| .collect(toSet()); |
| |
| methods() |
| .filter(msb -> allSuperInterfaceMethods.contains(msb.identifier(javaEnvironment))) |
| .forEach(msb -> msb.withAnnotation(AnnotationGenerator.createOverride())); |
| return thisInstance(); |
| } |
| |
| protected TYPE withExtendsAnnotationIfNecessary(IType element) { |
| var extendedTypeOpt = getExtendedType(element); |
| if (extendedTypeOpt.isEmpty()) { |
| return thisInstance(); |
| } |
| var extendedType = extendedTypeOpt.get(); |
| var primaryType = extendedType.primary(); |
| |
| Optional<IType> extendedDto = Optional.empty(); |
| var api = scoutApi(); |
| if (primaryType.isInstanceOf(api.IForm()) || primaryType.isInstanceOf(api.IFormField())) { |
| var declaring = extendedType.declaringType(); |
| if (extendedType.isInstanceOf(api.ITable()) && declaring.isPresent()) { |
| var tableFieldDto = getFormDataType(declaring.get()); |
| extendedDto = tableFieldDto.flatMap(dto -> dto.innerTypes().withInstanceOf(api.AbstractTableRowData()).first()); |
| } |
| else { |
| extendedDto = findDtoForForm(primaryType); |
| } |
| } |
| else if (primaryType.isInstanceOf(api.IExtension())) { |
| extendedDto = findDtoForPage(primaryType); |
| } |
| else if (primaryType.isInstanceOf(api.IPageWithTable())) { |
| var pageDto = findDtoForPage(primaryType); |
| extendedDto = pageDto.flatMap(dto -> dto.innerTypes().withInstanceOf(api.AbstractTableRowData()).first()); |
| } |
| |
| return extendedDto |
| .map(t -> withAnnotation(ScoutAnnotationGenerator.createExtends(t.reference()))) |
| .orElseGet(this::thisInstance); |
| } |
| |
| /** |
| * Gets the form data type that is referenced in the form data annotation of the given form.<br> |
| * If the annotation does not exist or points to an inexistent form data type, null is returned. |
| * |
| * @param form |
| * the form for which the form data should be returned. |
| * @return the form data type or null if it could not be found. |
| */ |
| private static Optional<IType> findDtoForForm(IType form) { |
| if (form == null) { |
| return Optional.empty(); |
| } |
| var a = FormDataAnnotationDescriptor.of(form); |
| return Optional.ofNullable(a.getFormDataType()); |
| } |
| |
| /** |
| * Gets the page data type that is referenced in the page data annotation of the given page type.<br> |
| * If the annotation does not exist or points to an inexistent page data type, null is returned. |
| * |
| * @param page |
| * the page for which the page data should be returned. |
| * @return the page data class or null. |
| */ |
| private static Optional<IType> findDtoForPage(IType page) { |
| if (page == null) { |
| return Optional.empty(); |
| } |
| return DataAnnotationDescriptor.of(page) |
| .map(DataAnnotationDescriptor::getDataType); |
| } |
| |
| protected static String getRowDataName(String base) { |
| var rowDataSuffix = "RowData"; |
| if (Strings.isBlank(base)) { |
| return rowDataSuffix; |
| } |
| |
| var result = new StringBuilder(base.length() + rowDataSuffix.length()); |
| String[] suffixes = {"PageData", "FieldData", ISdkConstants.SUFFIX_DTO}; |
| Arrays.stream(suffixes) |
| .filter(base::endsWith) |
| .findFirst() |
| .ifPresent(suffix -> result.append(base, 0, base.length() - suffix.length())); |
| if (result.length() < 1) { |
| // has none of the suffixes |
| result.append(base); |
| } |
| return result.append(rowDataSuffix).toString(); |
| } |
| |
| protected static String removeFieldSuffix(String fieldName) { |
| if (fieldName.endsWith(ISdkConstants.SUFFIX_FORM_FIELD)) { |
| return fieldName.substring(0, fieldName.length() - ISdkConstants.SUFFIX_FORM_FIELD.length()); |
| } |
| if (fieldName.endsWith(ISdkConstants.SUFFIX_BUTTON)) { |
| return fieldName.substring(0, fieldName.length() - ISdkConstants.SUFFIX_BUTTON.length()); |
| } |
| if (fieldName.endsWith(ISdkConstants.SUFFIX_COLUMN)) { |
| return fieldName.substring(0, fieldName.length() - ISdkConstants.SUFFIX_COLUMN.length()); |
| } |
| if (fieldName.endsWith(ISdkConstants.SUFFIX_OUTLINE_PAGE)) { |
| return fieldName.substring(0, fieldName.length() - ISdkConstants.SUFFIX_OUTLINE_PAGE.length()); |
| } |
| return fieldName; |
| } |
| |
| /** |
| * @return Returns an {@link Optional} with the form field data/form data for the given form field/form. |
| * @since 3.8.2 |
| */ |
| private static Optional<IType> getFormDataType(IType modelType) { |
| var primaryType = getFormFieldDataPrimaryTypeRec(modelType); |
| if (primaryType == null) { |
| return Optional.empty(); |
| } |
| |
| var isPrimaryType = modelType.declaringType().isEmpty(); |
| if (isPrimaryType) { |
| // model type is a primary type (form, template) and we have a corresponding DTO type. |
| return Optional.of(primaryType); |
| } |
| |
| // check if the primary type itself is the correct type |
| var formDataName = removeFieldSuffix(modelType.elementName()); |
| if (primaryType.elementName().equals(formDataName)) { |
| return Optional.of(primaryType); |
| } |
| |
| // search field data within form data |
| return primaryType.innerTypes().withRecursiveInnerTypes(true).withSimpleName(formDataName).first(); |
| } |
| |
| private static Optional<IType> computeDtoGenericType(IType contextType, IType annotationOwnerType, int genericOrdinal) { |
| if (contextType == null || Object.class.getName().equals(contextType.name()) || annotationOwnerType == null) { |
| return Optional.empty(); |
| } |
| |
| if (annotationOwnerType.typeArguments().count() <= genericOrdinal) { |
| // cannot be found in arguments. check parameters |
| var param = annotationOwnerType.typeParameters() |
| .skip(genericOrdinal) |
| .findAny() |
| .orElseThrow(() -> new SdkException("Invalid genericOrdinal value on class '{}': {}.", annotationOwnerType.name(), genericOrdinal)); |
| return param.bounds().findAny(); |
| } |
| |
| return annotationOwnerType.resolveTypeParamValue(genericOrdinal) |
| .flatMap(Stream::findFirst); |
| } |
| |
| /** |
| * @return Returns the form field data/form data for the given form field/form or {@code null} if it does not have |
| * one. The method walks recursively through the list of declaring classes until it has reached a primary |
| * type. |
| * @since 3.8.2 |
| */ |
| private static IType getFormFieldDataPrimaryTypeRec(IType recursiveDeclaringType) { |
| while (true) { |
| if (recursiveDeclaringType == null) { |
| return null; |
| } |
| |
| var formDataAnnotation = FormDataAnnotationDescriptor.of(recursiveDeclaringType); |
| if (FormDataAnnotationDescriptor.isIgnore(formDataAnnotation)) { |
| return null; |
| } |
| |
| var declaringType = recursiveDeclaringType.declaringType(); |
| if (declaringType.isEmpty()) { |
| // primary type |
| if (FormDataAnnotationDescriptor.isCreate(formDataAnnotation) || FormDataAnnotationDescriptor.isSdkCommandUse(formDataAnnotation)) { |
| return formDataAnnotation.getFormDataType(); |
| } |
| return null; |
| } |
| |
| recursiveDeclaringType = declaringType.get(); |
| } |
| } |
| |
| protected static String computeSuperTypeForFormData(IType modelType, FormDataAnnotationDescriptor formDataAnnotation) { |
| // handle replace |
| if (modelType.annotations().withManagedWrapper(ReplaceAnnotation.class).existsAny()) { |
| var replaced = modelType.superClass() |
| .flatMap(AbstractDtoGenerator::getFormDataType) |
| .map(IType::reference); |
| if (replaced.isPresent()) { |
| return replaced.get(); |
| } |
| } |
| return computeSuperTypeForFormDataIgnoringReplace(modelType, formDataAnnotation); |
| } |
| |
| private static String computeSuperTypeForFormDataIgnoringReplace(IType formField, FormDataAnnotationDescriptor formDataAnnotation) { |
| var superType = formDataAnnotation.getSuperType(); |
| if (superType == null) { |
| return null; |
| } |
| |
| if (formDataAnnotation.getGenericOrdinal() >= 0) { |
| var genericOrdinalDefinitionType = formDataAnnotation.getGenericOrdinalDefinitionType(); |
| if (genericOrdinalDefinitionType != null && superType.hasTypeParameters()) { |
| var genericType = computeDtoGenericType(formField, genericOrdinalDefinitionType, formDataAnnotation.getGenericOrdinal()); |
| if (genericType.isPresent()) { |
| return genericType |
| .map(IType::reference) |
| .map(fqn -> superType.name() + JavaTypes.C_GENERIC_START + fqn + JavaTypes.C_GENERIC_END) |
| .get(); |
| } |
| } |
| } |
| return superType.reference(); |
| } |
| |
| private static Optional<IType> findExtendsAnnotationValue(IType element) { |
| return element.superTypes().withSuperInterfaces(false).stream() |
| .flatMap(curType -> curType.annotations().withManagedWrapper(ExtendsAnnotation.class).first().stream()) |
| .map(ExtendsAnnotation::value) |
| .findAny(); |
| } |
| |
| /** |
| * Gets the {@link IType} the given model type extends or {@code null} if none.<br> |
| * The given modelType must be an IExtension or must have an @Extends annotation. |
| * |
| * @param modelType |
| * The extension whose owner should be returned. |
| * @return The owner of the given extension or null. |
| */ |
| private Optional<IType> getExtendedType(IType modelType) { |
| if (modelType == null) { |
| return Optional.empty(); |
| } |
| |
| // 1. try to read from @Extends annotation |
| var extendsValue = findExtendsAnnotationValue(modelType); |
| if (extendsValue.isPresent()) { |
| return extendsValue; |
| } |
| |
| // 2. try to read from generic |
| var iExtension = scoutApi().IExtension(); |
| if (modelType.isInstanceOf(iExtension)) { |
| var owner = modelType.resolveTypeParamValue(iExtension.ownerTypeParamIndex(), iExtension.fqn()); |
| if (owner.isPresent()) { |
| return owner.get().findFirst(); |
| } |
| } |
| |
| // 3. try in declaring type |
| return modelType |
| .declaringType() |
| .flatMap(this::getExtendedType); |
| } |
| |
| protected void setupBuilder() { |
| // flags |
| asPublic() |
| .withSuperClass(computeSuperType()) |
| .withField(FieldGenerator.createSerialVersionUid()); |
| if (declaringGenerator().orElse(null) instanceof ITypeGenerator) { |
| asStatic(); |
| } |
| if (isAbstract(modelType().flags())) { |
| asAbstract(); |
| } |
| |
| // copy annotations over to the DTO |
| copyAnnotations(); |
| } |
| |
| protected TYPE withReplaceIfNecessary() { |
| // add replace annotation to DTO if replace annotation is present on the model |
| if (modelType().annotations().withManagedWrapper(ReplaceAnnotation.class).existsAny()) { |
| withAnnotation(ScoutAnnotationGenerator.createReplace()); |
| } |
| return thisInstance(); |
| } |
| |
| protected abstract String computeSuperType(); |
| |
| public IType modelType() { |
| return m_modelType; |
| } |
| |
| protected TYPE withPropertyDtos() { |
| var scoutApi = scoutApi(); |
| var formDataFqn = scoutApi.FormData().fqn(); |
| var data = scoutApi.Data().fqn(); |
| Predicate<IMethod> hasDtoAnnotation = method -> method.annotations().withName(formDataFqn).existsAny() || method.annotations().withName(data).existsAny(); |
| |
| PropertyBean.of(modelType()) |
| .filter(bean -> bean.readMethod().isPresent() && bean.writeMethod().isPresent()) |
| .filter(bean -> hasDtoAnnotation.test(bean.readMethod().get()) || hasDtoAnnotation.test(bean.writeMethod().get())) |
| .sorted(comparing(PropertyBean::name).thenComparing(PropertyBean::toString)) |
| .forEach(this::addPropertyDto); |
| return thisInstance(); |
| } |
| |
| @SuppressWarnings("squid:UnusedPrivateMethod") // used as method-reference |
| private void addPropertyDto(PropertyBean desc) { |
| var lowerCaseBeanName = Introspector.decapitalize(desc.name()); |
| var upperCaseBeanName = Strings.ensureStartWithUpperCase(desc.name()).toString(); |
| var propName = upperCaseBeanName + ISdkConstants.SUFFIX_DTO_PROPERTY; |
| var propDataType = desc.type().reference(); |
| var propDataTypeBoxed = JavaTypes.boxPrimitive(propDataType); |
| |
| // property class |
| var abstractPropertyDataApi = scoutApi().AbstractPropertyData(); |
| var propertyTypeBuilder = TypeGenerator.create() |
| .asPublic() |
| .asStatic() |
| .withElementName(propName) |
| .withSuperClass(abstractPropertyDataApi.fqn() + JavaTypes.C_GENERIC_START + propDataTypeBoxed + JavaTypes.C_GENERIC_END) |
| .withField(FieldGenerator.createSerialVersionUid()); |
| copyAnnotations(desc.readMethod().get(), propertyTypeBuilder, targetEnvironment()); |
| |
| var getterName = PropertyBean.GETTER_PREFIX + propName; |
| this |
| .withType(propertyTypeBuilder, DtoMemberSortObjectFactory.forTypeFormDataProperty(propName)) |
| .withMethod(ScoutMethodGenerator.create() // getter |
| .asPublic() |
| .withElementName(getterName) |
| .withReturnType(propName) |
| .withBody(b -> b.returnClause().appendGetPropertyByClass(propName).semicolon()), |
| DtoMemberSortObjectFactory.forMethodFormDataProperty(upperCaseBeanName)) |
| .withMethod(MethodGenerator.create() // legacy getter |
| .asPublic() |
| .withElementName(PropertyBean.getterPrefixFor(propDataType) + upperCaseBeanName) |
| .withComment(b -> b.appendJavaDocComment("access method for property " + upperCaseBeanName + JavaTypes.C_DOT)) |
| .withReturnType(propDataType) |
| .withBody(b -> { |
| var suffix = "()." + abstractPropertyDataApi.getValueMethodName() + "()"; |
| b.returnClause().append(getterName).append(suffix); |
| if (JavaTypes.isPrimitive(propDataType)) { |
| b.append(" == ").nullLiteral().append(" ? ").appendDefaultValueOf(propDataTypeBoxed).append(" : ").append(getterName).append(suffix); |
| } |
| b.semicolon(); |
| }), |
| DtoMemberSortObjectFactory.forMethodFormDataPropertyLegacy(upperCaseBeanName)) |
| .withMethod(MethodGenerator.create() // legacy setter |
| .asPublic() |
| .withElementName(PropertyBean.SETTER_PREFIX + upperCaseBeanName) |
| .withComment(b -> b.appendJavaDocComment("access method for property " + upperCaseBeanName + JavaTypes.C_DOT)) |
| .withReturnType(JavaTypes._void) |
| .withParameter(MethodParameterGenerator.create() |
| .withElementName(lowerCaseBeanName) |
| .withDataType(propDataType)) |
| .withBody(b -> b.append(getterName).parenthesisOpen().parenthesisClose() |
| .dot().append(abstractPropertyDataApi.setValueMethodName()).parenthesisOpen().append(lowerCaseBeanName).parenthesisClose().semicolon()), |
| DtoMemberSortObjectFactory.forMethodFormDataPropertyLegacy(upperCaseBeanName)); |
| } |
| |
| public IJavaEnvironment targetEnvironment() { |
| return m_targetEnvironment; |
| } |
| } |