001/** 002 * Copyright (C) 2006-2025 Talend Inc. - www.talend.com 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * http://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016package org.talend.sdk.component.tools.validator; 017 018import static java.util.stream.Collectors.toMap; 019import static java.util.stream.Stream.concat; 020import static java.util.stream.Stream.empty; 021import static java.util.stream.Stream.of; 022import static org.talend.sdk.component.runtime.manager.ParameterMeta.Type.ARRAY; 023import static org.talend.sdk.component.runtime.manager.ParameterMeta.Type.ENUM; 024import static org.talend.sdk.component.runtime.manager.ParameterMeta.Type.OBJECT; 025 026import java.lang.reflect.ParameterizedType; 027import java.lang.reflect.Type; 028import java.util.AbstractMap; 029import java.util.List; 030import java.util.Map; 031import java.util.Objects; 032import java.util.Set; 033import java.util.stream.Collectors; 034import java.util.stream.Stream; 035 036import org.apache.xbean.finder.AnnotationFinder; 037import org.talend.sdk.component.runtime.manager.ParameterMeta; 038import org.talend.sdk.component.tools.validator.Validators.ValidatorHelper; 039 040import lombok.extern.slf4j.Slf4j; 041 042@Slf4j 043public class LayoutValidator implements Validator { 044 045 private final Validators.ValidatorHelper helper; 046 047 public LayoutValidator(final ValidatorHelper helper) { 048 this.helper = helper; 049 } 050 051 @Override 052 public Stream<String> validate(final AnnotationFinder finder, final List<Class<?>> components) { 053 return components 054 .stream() 055 .map(helper::buildOrGetParameters) 056 .flatMap(this::toFlatNonPrimitiveConfig) 057 .collect(toMap(AbstractMap.SimpleEntry::getKey, AbstractMap.SimpleEntry::getValue, (p1, p2) -> p1)) 058 .entrySet() 059 .stream() 060 .flatMap(this::visitLayout); 061 } 062 063 private Stream<String> visitLayout(final Map.Entry<String, ParameterMeta> config) { 064 065 final Set<String> fieldsInGridLayout = config 066 .getValue() 067 .getMetadata() 068 .entrySet() 069 .stream() 070 .filter(meta -> meta.getKey().startsWith("tcomp::ui::gridlayout")) 071 .flatMap(meta -> of(meta.getValue().split("\\|"))) 072 .flatMap(s -> of(s.split(","))) 073 .filter(s -> !s.isEmpty()) 074 .collect(Collectors.toSet()); 075 076 final Set<String> fieldsInOptionOrder = config 077 .getValue() 078 .getMetadata() 079 .entrySet() 080 .stream() 081 .filter(meta -> meta.getKey().startsWith("tcomp::ui::optionsorder")) 082 .flatMap(meta -> of(meta.getValue().split(","))) 083 .collect(Collectors.toSet()); 084 085 if (fieldsInGridLayout.isEmpty() && fieldsInOptionOrder.isEmpty()) { 086 return Stream.empty(); 087 } 088 089 if (!fieldsInGridLayout.isEmpty() && !fieldsInOptionOrder.isEmpty()) { 090 this.log.error("Concurrent layout found for '" + config.getKey() + "', the @OptionsOrder will be ignored."); 091 } 092 093 final Stream<String> errorsLib; 094 if (!fieldsInGridLayout.isEmpty()) { 095 errorsLib = fieldsInGridLayout 096 .stream() 097 .filter(fieldInLayout -> config 098 .getValue() 099 .getNestedParameters() 100 .stream() 101 .map(ParameterMeta::getName) 102 .noneMatch(field -> field.equals(fieldInLayout))) 103 .map(fieldInLayout -> "Option '" + fieldInLayout 104 + "' in @GridLayout doesn't exist in declaring class '" + config.getKey() + "'") 105 .sorted(); 106 107 config 108 .getValue() 109 .getNestedParameters() 110 .stream() 111 .filter(field -> !fieldsInGridLayout.contains(field.getName())) 112 .map(field -> "Field '" + field.getName() + "' in " + config.getKey() 113 + " is not declared in any layout.") 114 .forEach(this.log::error); 115 } else { 116 errorsLib = fieldsInOptionOrder 117 .stream() 118 .filter(fieldInLayout -> config 119 .getValue() 120 .getNestedParameters() 121 .stream() 122 .map(ParameterMeta::getName) 123 .noneMatch(field -> field.equals(fieldInLayout))) 124 .map(fieldInLayout -> "Option '" + fieldInLayout 125 + "' in @OptionOrder doesn't exist in declaring class '" + config.getKey() + "'") 126 .sorted(); 127 128 config 129 .getValue() 130 .getNestedParameters() 131 .stream() 132 .filter(field -> !fieldsInOptionOrder.contains(field.getName())) 133 .map(field -> "Field '" + field.getName() + "' in " + config.getKey() 134 + " is not declared in any layout.") 135 .forEach(this.log::error); 136 } 137 return errorsLib; 138 } 139 140 private Stream<AbstractMap.SimpleEntry<String, ParameterMeta>> 141 toFlatNonPrimitiveConfig(final List<ParameterMeta> config) { 142 if (config == null || config.isEmpty()) { 143 return empty(); 144 } 145 return config 146 .stream() 147 .filter(Objects::nonNull) 148 .filter(p -> OBJECT.equals(p.getType()) || isArrayOfObject(p)) 149 .filter(p -> p.getNestedParameters() != null) 150 .flatMap(p -> concat(of(new AbstractMap.SimpleEntry<>(toJavaType(p).getName(), p)), 151 toFlatNonPrimitiveConfig(p.getNestedParameters()))); 152 } 153 154 private boolean isArrayOfObject(final ParameterMeta param) { 155 156 return ARRAY.equals(param.getType()) && param.getNestedParameters() != null 157 && param 158 .getNestedParameters() 159 .stream() 160 .anyMatch(p -> OBJECT.equals(p.getType()) || ENUM.equals(p.getType()) || isArrayOfObject(p)); 161 162 } 163 164 private Class<?> toJavaType(final ParameterMeta p) { 165 if (p.getType().equals(OBJECT) || p.getType().equals(ENUM)) { 166 if (Class.class.isInstance(p.getJavaType())) { 167 return Class.class.cast(p.getJavaType()); 168 } 169 throw new IllegalArgumentException("Unsupported type for parameter " + p.getPath() + " (from " 170 + p.getSource().declaringClass() + "), ensure it is a Class<?>"); 171 } 172 173 if (p.getType().equals(ARRAY) && ParameterizedType.class.isInstance(p.getJavaType())) { 174 final ParameterizedType parameterizedType = ParameterizedType.class.cast(p.getJavaType()); 175 final Type[] arguments = parameterizedType.getActualTypeArguments(); 176 if (arguments.length == 1 && Class.class.isInstance(arguments[0])) { 177 return Class.class.cast(arguments[0]); 178 } 179 throw new IllegalArgumentException("Unsupported type for parameter " + p.getPath() + " (from " 180 + p.getSource().declaringClass() + "), " + "ensure it is a ParameterizedType with one argument"); 181 } 182 183 throw new IllegalStateException("Parameter '" + p.getName() + "' is not an object."); 184 } 185}