001/**
002 * Copyright (C) 2006-2024 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.lang.String.format;
019import static java.lang.String.join;
020import static java.util.Arrays.asList;
021
022import java.lang.reflect.Method;
023import java.lang.reflect.Parameter;
024import java.util.ArrayList;
025import java.util.Arrays;
026import java.util.HashSet;
027import java.util.List;
028import java.util.Set;
029import java.util.stream.Collectors;
030import java.util.stream.Stream;
031
032import org.apache.xbean.finder.AnnotationFinder;
033import org.talend.sdk.component.api.record.Record;
034
035public class RecordValidator implements Validator {
036
037    @Override
038    public Stream<String> validate(final AnnotationFinder finder, final List<Class<?>> components) {
039        final List<String> messages = new ArrayList<>();
040        components.stream()
041                .forEach(component -> {
042                    messages.addAll(this.validateMethods(component.getDeclaredMethods()));
043                });
044        return messages.stream();
045    }
046
047    private List<String> validateMethods(final Method[] methods) {
048        final List<String> messages = new ArrayList<>();
049
050        Arrays.stream(methods)
051                .forEach(method -> {
052                    if (isProducing(method, Record.Builder.class) && !isSafeEntryBuilderProvider(method)) {
053                        final String errorMessage = join("\n", asList(
054                                format("Method %s calls unsafe Builder creator. This either means:",
055                                        getFullName(method)),
056                                "  * That the TCK method is safe and should belong to WHITE_LIST_TCK_RECORD_BUILDER_PROVIDER"));
057                        messages.add(errorMessage);
058                    }
059                });
060        return messages;
061    }
062
063    private static boolean isSafeEntryBuilderProvider(final Method method) {
064        return WHITE_LIST_TCK_RECORD_BUILDER_PROVIDER.contains(getFullName(method));
065    }
066
067    protected static boolean isProducing(final Method method, final Class<?> clazz) {
068        return areTheSameClass(method.getReturnType(), clazz);
069    }
070
071    private static boolean areTheSameClass(final Class javaClass, final Class<?> clazz) {
072        return javaClass.getPackage() != null && javaClass.getPackage().getName().equals(clazz.getPackage().getName())
073                && javaClass.isAssignableFrom(clazz);
074    }
075
076    protected static String getFullName(final Method method) {
077        return formatMethod(method.getDeclaringClass().getName(), method.getName(), namesOf(method.getParameters()));
078    }
079
080    private static String formatMethod(final String ownerName, final String methodName, final String parameters) {
081        return ownerName + "." + methodName + "(" + parameters + ")";
082    }
083
084    private static String namesOf(final Parameter[] parameters) {
085        return Arrays.stream(parameters).map(type -> type.getType().getName()).collect(Collectors.joining(", "));
086    }
087
088    /**
089     * TCK Java methods that create a {@link Record.Builder} from another builder/record.
090     * These are safe, they preserve the props from the other builder/record.
091     */
092    private static final Set<String> WHITE_LIST_TCK_RECORD_BUILDER_PROVIDER = new HashSet<>(asList(
093            "org.talend.sdk.component.runtime.record.RecordImpl.withNewSchema(org.talend.sdk.component.api.record.Schema)",
094
095            "org.talend.sdk.component.runtime.record.RecordImpl.BuilderImpl.before(java.lang.String)",
096            "org.talend.sdk.component.runtime.record.RecordImpl.BuilderImpl.after(java.lang.String)",
097            "org.talend.sdk.component.runtime.record.RecordImpl.BuilderImpl.withRecord(java.lang.String, org.talend.sdk.component.api.record.Record)",
098            "org.talend.sdk.component.runtime.record.RecordImpl.BuilderImpl.removeEntry(org.talend.sdk.component.api.record.Schema$Entry)",
099            "org.talend.sdk.component.runtime.record.RecordImpl.BuilderImpl.updateEntryByName(java.lang.String, org.talend.sdk.component.api.record.Schema$Entry)",
100            "org.talend.sdk.component.api.record.Record.withNewSchema(org.talend.sdk.component.api.record.Schema)",
101            "org.talend.sdk.component.api.record.Record$Builder.before(java.lang.String)",
102            "org.talend.sdk.component.api.record.Record$Builder.after(java.lang.String)",
103            "org.talend.sdk.component.api.record.Record$Builder.removeEntry(org.talend.sdk.component.api.record.Schema$Entry)",
104            "org.talend.sdk.component.api.record.Record$Builder.withInstant(org.talend.sdk.component.api.record.Schema$Entry, java.time.Instant)",
105            "org.talend.sdk.component.api.record.Record$Builder.updateEntryByName(java.lang.String, org.talend.sdk.component.api.record.Schema$Entry)",
106            "org.talend.sdk.component.api.record.Record$Builder.updateEntryByName(java.lang.String, org.talend.sdk.component.api.record.Schema$Entry, java.util.function.Function)",
107            "org.talend.sdk.component.api.record.Record$Builder.with(org.talend.sdk.component.api.record.Schema$Entry, java.lang.Object)",
108            "org.talend.sdk.component.api.record.Record$Builder.withString(java.lang.String, java.lang.String)",
109            "org.talend.sdk.component.api.record.Record$Builder.withString(org.talend.sdk.component.api.record.Schema$Entry, java.lang.String)",
110            "org.talend.sdk.component.api.record.Record$Builder.withBytes(java.lang.String, [B)",
111            "org.talend.sdk.component.api.record.Record$Builder.withBytes(org.talend.sdk.component.api.record.Schema$Entry, [B)",
112            "org.talend.sdk.component.api.record.Record$Builder.withDateTime(java.lang.String, java.util.Date)",
113            "org.talend.sdk.component.api.record.Record$Builder.withDateTime(org.talend.sdk.component.api.record.Schema$Entry, java.util.Date)",
114            "org.talend.sdk.component.api.record.Record$Builder.withDateTime(java.lang.String, java.time.ZonedDateTime)",
115            "org.talend.sdk.component.api.record.Record$Builder.withDateTime(org.talend.sdk.component.api.record.Schema$Entry, java.time.ZonedDateTime)",
116            "org.talend.sdk.component.api.record.Record$Builder.withDecimal(java.lang.String, java.math.BigDecimal)",
117            "org.talend.sdk.component.api.record.Record$Builder.withDecimal(org.talend.sdk.component.api.record.Schema$Entry, java.math.BigDecimal)",
118            "org.talend.sdk.component.api.record.Record$Builder.withTimestamp(java.lang.String, long)",
119            "org.talend.sdk.component.api.record.Record$Builder.withTimestamp(org.talend.sdk.component.api.record.Schema$Entry, long)",
120            "org.talend.sdk.component.api.record.Record$Builder.withInstant(java.lang.String, java.time.Instant)",
121            "org.talend.sdk.component.api.record.Record$Builder.withInstant(org.talend.sdk.component.api.record.Schema$Entry,java.time.Instant)",
122            "org.talend.sdk.component.api.record.Record$Builder.withInt(java.lang.String, int)",
123            "org.talend.sdk.component.api.record.Record$Builder.withInt(org.talend.sdk.component.api.record.Schema$Entry, int)",
124            "org.talend.sdk.component.api.record.Record$Builder.withLong(java.lang.String, long)",
125            "org.talend.sdk.component.api.record.Record$Builder.withLong(org.talend.sdk.component.api.record.Schema$Entry, long)",
126            "org.talend.sdk.component.api.record.Record$Builder.withFloat(java.lang.String, float)",
127            "org.talend.sdk.component.api.record.Record$Builder.withFloat(org.talend.sdk.component.api.record.Schema$Entry, float)",
128            "org.talend.sdk.component.api.record.Record$Builder.withDouble(java.lang.String, double)",
129            "org.talend.sdk.component.api.record.Record$Builder.withDouble(org.talend.sdk.component.api.record.Schema$Entry, double)",
130            "org.talend.sdk.component.api.record.Record$Builder.withBoolean(java.lang.String, boolean)",
131            "org.talend.sdk.component.api.record.Record$Builder.withBoolean(org.talend.sdk.component.api.record.Schema$Entry, boolean)",
132            "org.talend.sdk.component.api.record.Record$Builder.withRecord(java.lang.String, org.talend.sdk.component.api.record.Record)",
133            "org.talend.sdk.component.api.record.Record$Builder.withRecord(org.talend.sdk.component.api.record.Schema$Entry, org.talend.sdk.component.api.record.Record)",
134            "org.talend.sdk.component.api.record.Record$Builder.withArray(org.talend.sdk.component.api.record.Schema$Entry, java.util.Collection)",
135
136            "org.talend.sdk.component.api.record.RecordImpl.withNewSchema(org.talend.sdk.component.api.record.Schema)",
137            "org.talend.sdk.component.api.record.RecordImpl.BuilderImpl.with(org.talend.sdk.component.api.record.Schema$Entry, java.lang.Object)",
138            "org.talend.sdk.component.api.record.RecordImpl.BuilderImpl.withString(java.lang.String, java.lang.String)",
139            "org.talend.sdk.component.api.record.RecordImpl.BuilderImpl.withString(org.talend.sdk.component.api.record.Schema$Entry, java.lang.String)",
140            "org.talend.sdk.component.api.record.RecordImpl.BuilderImpl.withBytes(java.lang.String, [B)",
141            "org.talend.sdk.component.api.record.RecordImpl.BuilderImpl.withBytes(org.talend.sdk.component.api.record.Schema$Entry, [B)",
142            "org.talend.sdk.component.api.record.RecordImpl.BuilderImpl.withDateTime(java.lang.String, java.util.Date)",
143            "org.talend.sdk.component.api.record.RecordImpl.BuilderImpl.withDateTime(org.talend.sdk.component.api.record.Schema$Entry, java.util.Date)",
144            "org.talend.sdk.component.api.record.RecordImpl.BuilderImpl.withDateTime(java.lang.String, java.time.ZonedDateTime)",
145            "org.talend.sdk.component.api.record.RecordImpl.BuilderImpl.withDateTime(org.talend.sdk.component.api.record.Schema$Entry, java.time.ZonedDateTime)",
146            "org.talend.sdk.component.api.record.RecordImpl.BuilderImpl.withDecimal(java.lang.String, java.math.BigDecimal)",
147            "org.talend.sdk.component.api.record.RecordImpl.BuilderImpl.withDecimal(org.talend.sdk.component.api.record.Schema$Entry, java.math.BigDecimal)",
148            "org.talend.sdk.component.api.record.RecordImpl.BuilderImpl.withTimestamp(java.lang.String, long)",
149            "org.talend.sdk.component.api.record.RecordImpl.BuilderImpl.withTimestamp(org.talend.sdk.component.api.record.Schema$Entry, long)",
150            "org.talend.sdk.component.api.record.RecordImpl.BuilderImpl.withInstant(java.lang.String, java.time.Instant)",
151            "org.talend.sdk.component.api.record.RecordImpl.BuilderImpl.withInstant(org.talend.sdk.component.api.record.Schema$Entry,java.time.Instant)",
152            "org.talend.sdk.component.api.record.RecordImpl.BuilderImpl.withInt(java.lang.String, int)",
153            "org.talend.sdk.component.api.record.RecordImpl.BuilderImpl.withInt(org.talend.sdk.component.api.record.Schema$Entry, int)",
154            "org.talend.sdk.component.api.record.RecordImpl.BuilderImpl.withLong(java.lang.String, long)",
155            "org.talend.sdk.component.api.record.RecordImpl.BuilderImpl.withLong(org.talend.sdk.component.api.record.Schema$Entry, long)",
156            "org.talend.sdk.component.api.record.RecordImpl.BuilderImpl.withFloat(java.lang.String, float)",
157            "org.talend.sdk.component.api.record.RecordImpl.BuilderImpl.withFloat(org.talend.sdk.component.api.record.Schema$Entry, float)",
158            "org.talend.sdk.component.api.record.RecordImpl.BuilderImpl.withDouble(java.lang.String, double)",
159            "org.talend.sdk.component.api.record.RecordImpl.BuilderImpl.withDouble(org.talend.sdk.component.api.record.Schema$Entry, double)",
160            "org.talend.sdk.component.api.record.RecordImpl.BuilderImpl.withBoolean(java.lang.String, boolean)",
161            "org.talend.sdk.component.api.record.RecordImpl.BuilderImpl.withBoolean(org.talend.sdk.component.api.record.Schema$Entry, boolean)",
162            "org.talend.sdk.component.api.record.RecordImpl.BuilderImpl.withRecord(java.lang.String, org.talend.sdk.component.api.record.Record)",
163            "org.talend.sdk.component.api.record.RecordImpl.BuilderImpl.withRecord(org.talend.sdk.component.api.record.Schema$Entry, org.talend.sdk.component.api.record.Record)",
164            "org.talend.sdk.component.api.record.RecordImpl.BuilderImpl.withArray(org.talend.sdk.component.api.record.Schema$Entry, java.util.Collection)"));
165
166}