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.runtime.manager.reflect.parameterenricher;
017
018import static java.util.Collections.emptyMap;
019import static java.util.Collections.singletonList;
020import static java.util.Collections.singletonMap;
021import static java.util.Locale.ENGLISH;
022import static java.util.function.Function.identity;
023import static java.util.stream.Collectors.joining;
024import static java.util.stream.Collectors.toList;
025import static java.util.stream.Collectors.toMap;
026
027import java.lang.annotation.Annotation;
028import java.lang.reflect.Array;
029import java.lang.reflect.InvocationTargetException;
030import java.lang.reflect.Method;
031import java.lang.reflect.Type;
032import java.time.LocalDate;
033import java.time.LocalDateTime;
034import java.time.LocalTime;
035import java.time.ZonedDateTime;
036import java.util.Collection;
037import java.util.HashMap;
038import java.util.Map;
039import java.util.function.Function;
040import java.util.stream.Stream;
041
042import org.talend.sdk.component.api.configuration.ui.layout.GridLayout;
043import org.talend.sdk.component.api.configuration.ui.layout.GridLayouts;
044import org.talend.sdk.component.api.configuration.ui.meta.Ui;
045import org.talend.sdk.component.api.configuration.ui.widget.DateTime;
046
047import lombok.Data;
048
049public class UiParameterEnricher extends BaseParameterEnricher {
050
051    public static final String META_PREFIX = "tcomp::ui::";
052
053    @Override
054    public Map<Type, Collection<Annotation>> getImplicitAnnotationForTypes() {
055        final Collection<Annotation> annotations = singletonList(new DateTimeAnnotation());
056        return Stream
057                .<Type> of(ZonedDateTime.class, LocalDateTime.class, LocalDate.class, LocalTime.class)
058                .collect(toMap(identity(), it -> annotations));
059    }
060
061    @Override
062    public Map<String, String> onParameterAnnotation(final String parameterName, final Type parameterType,
063            final Annotation annotation) {
064        final Ui condition = annotation.annotationType().getAnnotation(Ui.class);
065        if (condition != null) {
066            final String prefix = META_PREFIX + annotation.annotationType().getSimpleName().toLowerCase(ENGLISH) + "::";
067            if (GridLayouts.class == annotation.annotationType()) {
068                return Stream
069                        .of(GridLayouts.class.cast(annotation).value())
070                        .flatMap(a -> toConfig(a, prefix.substring(0, prefix.length() - 3) + "::").entrySet().stream())
071                        .collect(toMap(Map.Entry::getKey, Map.Entry::getValue));
072            }
073            if (DateTime.class == annotation.annotationType()) {
074                final String key = META_PREFIX + "datetime";
075                final DateTime dateTime = DateTime.class.cast(annotation);
076                final Map<String, String> dtmap = new HashMap<>();
077                if (parameterType == LocalTime.class) {
078                    dtmap.put(key, "time");
079                    dtmap.put(key + "::useSeconds", String.valueOf(dateTime.useSeconds()));
080                    return dtmap;
081                }
082                if (parameterType == LocalDate.class) {
083                    dtmap.put(key, "date");
084                    dtmap.put(key + "::dateFormat", String.valueOf(dateTime.dateFormat()));
085                    return dtmap;
086                }
087                if (parameterType == LocalDateTime.class) {
088                    dtmap.put(key, "datetime");
089                    dtmap.put(key + "::dateFormat", String.valueOf(dateTime.dateFormat()));
090                    dtmap.put(key + "::useSeconds", String.valueOf(dateTime.useSeconds()));
091                    dtmap.put(key + "::useUTC", String.valueOf(dateTime.useUTC()));
092                    return dtmap;
093                }
094                if (parameterType == ZonedDateTime.class || parameterType == Object.class /* unsafe */) {
095                    dtmap.put(key, "zoneddatetime");
096                    dtmap.put(key + "::dateFormat", String.valueOf(dateTime.dateFormat()));
097                    dtmap.put(key + "::useSeconds", String.valueOf(dateTime.useSeconds()));
098                    dtmap.put(key + "::useUTC", String.valueOf(dateTime.useUTC()));
099                    return dtmap;
100                }
101                throw new IllegalArgumentException(
102                        "Unsupported type for @DateTime option: " + parameterType + " on " + parameterName);
103            }
104            return toConfig(annotation, prefix);
105        }
106        return emptyMap();
107    }
108
109    private Map<String, String> toConfig(final Annotation annotation, final String prefix) {
110        if (GridLayout.class == annotation.annotationType()) {
111            final GridLayout layout = GridLayout.class.cast(annotation);
112            return Stream
113                    .of(layout.names())
114                    .flatMap(name -> Stream
115                            .of(annotation.annotationType().getMethods())
116                            .filter(m -> m.getDeclaringClass() == annotation.annotationType()
117                                    && !"names".equals(m.getName()))
118                            .collect(toMap(m -> prefix + name + "::" + m.getName(),
119                                    m -> toString(annotation, m, invoke -> {
120                                        if (invoke.getClass().isArray()) {
121                                            final Class<?> component = invoke.getClass().getComponentType();
122                                            if (!Annotation.class.isAssignableFrom(component)) {
123                                                return null;
124                                            }
125                                            final int length = Array.getLength(invoke);
126                                            if (length == 0) {
127                                                return "";
128                                            }
129                                            final Collection<Method> mtds = Stream
130                                                    .of(component.getMethods())
131                                                    .filter(mtd -> mtd.getDeclaringClass() == component
132                                                            && "value".equals(mtd.getName()))
133                                                    .collect(toList());
134                                            final StringBuilder builder = new StringBuilder("");
135                                            for (int i = 0; i < length; i++) {
136                                                final Object annot = Array.get(invoke, i);
137                                                mtds
138                                                        .forEach(p -> builder
139                                                                .append(toString(Annotation.class.cast(annot), p,
140                                                                        o -> null)));
141                                                if (i + 1 < length) {
142                                                    builder.append('|');
143                                                }
144                                            }
145                                            return builder.toString();
146                                        }
147                                        return null;
148                                    })))
149                            .entrySet()
150                            .stream())
151                    .collect(toMap(Map.Entry::getKey, Map.Entry::getValue));
152        }
153
154        final Map<String, String> config = Stream
155                .of(annotation.annotationType().getMethods())
156                .filter(m -> m.getDeclaringClass() == annotation.annotationType())
157                .collect(toMap(m -> prefix + m.getName(), m -> toString(annotation, m, invoke -> null)));
158        return config.isEmpty() ? singletonMap(prefix.substring(0, prefix.length() - "::".length()), "true") : config;
159    }
160
161    private String toString(final Annotation annotation, final Method m,
162            final Function<Object, String> customConversions) {
163        try {
164            final Object invoke = m.invoke(annotation);
165            final String custom = customConversions.apply(invoke);
166            if (custom != null) {
167                return custom;
168            }
169            if (String.class.isInstance(invoke)) {
170                final String string = String.valueOf(invoke);
171                if (string.startsWith("local_configuration:")) {
172                    return getContext()
173                            .map(context -> context
174                                    .getConfiguration()
175                                    .get(string.substring("local_configuration:".length())))
176                            .orElse(string);
177                }
178                return string;
179            }
180            if (Class.class.isInstance(invoke)) {
181                return Class.class.cast(invoke).getSimpleName().toLowerCase(ENGLISH);
182            }
183            if (String[].class.isInstance(invoke)) {
184                return Stream.of(String[].class.cast(invoke)).collect(joining(","));
185            }
186            return String.valueOf(invoke);
187        } catch (final InvocationTargetException | IllegalAccessException e) {
188            throw new IllegalStateException(e);
189        }
190    }
191
192    @Data
193    private static class DateTimeAnnotation implements DateTime {
194
195        public String dateFormat = "YYYY/MM/DD";
196
197        boolean useSeconds = true;
198
199        boolean useUTC = true;
200
201        @Override
202        public Class<? extends Annotation> annotationType() {
203            return DateTime.class;
204        }
205
206        @Override
207        public String dateFormat() {
208            return dateFormat;
209        }
210
211        @Override
212        public boolean useSeconds() {
213            return useSeconds;
214        }
215
216        @Override
217        public boolean useUTC() {
218            return useUTC;
219        }
220    }
221}