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.runtime.manager.util;
017
018import static java.util.Collections.singleton;
019import static java.util.Collections.singletonList;
020import static java.util.Optional.ofNullable;
021import static java.util.stream.Collectors.joining;
022import static lombok.AccessLevel.PRIVATE;
023
024import java.lang.reflect.Field;
025import java.lang.reflect.InvocationTargetException;
026import java.lang.reflect.ParameterizedType;
027import java.lang.reflect.Type;
028import java.util.Collection;
029import java.util.List;
030import java.util.Set;
031import java.util.SortedSet;
032import java.util.TreeSet;
033
034import org.talend.sdk.component.runtime.manager.ParameterMeta;
035
036import lombok.AllArgsConstructor;
037import lombok.Data;
038
039public class DefaultValueInspector {
040
041    // for now we use instantiation to find the defaults assuming it will be cached
042    // but we can move it later in design module to directly read it from the bytecode
043    public Instance createDemoInstance(final Object rootInstance, final ParameterMeta param) {
044        if (rootInstance != null) {
045            final Object field = findField(rootInstance, param);
046            if (field != null) {
047                return new Instance(field, false);
048            }
049        }
050
051        final Type javaType = param.getJavaType();
052        if (Class.class.isInstance(javaType)) {
053            return new Instance(tryCreatingObjectInstance(javaType), true);
054        } else if (ParameterizedType.class.isInstance(javaType)) {
055            final ParameterizedType pt = ParameterizedType.class.cast(javaType);
056            final Type rawType = pt.getRawType();
057            if (Class.class.isInstance(rawType) && Collection.class.isAssignableFrom(Class.class.cast(rawType))
058                    && pt.getActualTypeArguments().length == 1
059                    && Class.class.isInstance(pt.getActualTypeArguments()[0])) {
060                final Object instance = tryCreatingObjectInstance(pt.getActualTypeArguments()[0]);
061                final Class<?> collectionType = Class.class.cast(rawType);
062                if (Set.class == collectionType) {
063                    return new Instance(singleton(instance), true);
064                }
065                if (SortedSet.class == collectionType) {
066                    return new Instance(new TreeSet<>(singletonList(instance)), true);
067                }
068                if (List.class == collectionType || Collection.class == collectionType) {
069                    return new Instance(singletonList(instance), true);
070                }
071                // todo?
072                return null;
073            }
074        }
075        return null;
076    }
077
078    public String findDefault(final Object instance, final ParameterMeta param) {
079        if (instance == null) {
080            return null;
081        }
082        final ParameterMeta.Type type = param.getType();
083        switch (type) {
084        case OBJECT:
085            return null;
086        case ENUM:
087            return Enum.class.cast(instance).name();
088        case STRING:
089        case NUMBER:
090        case BOOLEAN:
091            return String.valueOf(instance);
092        case ARRAY: // can be enhanced
093            if (!param.getNestedParameters().isEmpty()) {
094                return null;
095            } else if (Collection.class.isInstance(instance)) {
096                return ((Collection<Object>) instance).stream().map(String::valueOf).collect(joining(","));
097            } else { // primitives
098                return String.valueOf(instance);
099            }
100        default:
101            throw new IllegalArgumentException("Unsupported type: " + param.getType());
102        }
103    }
104
105    private Object findField(final Object rootInstance, final ParameterMeta param) {
106        if (param.getPath().startsWith("$") || param.getName().startsWith("$")) { // virtual param
107            return null;
108        }
109        if (Collection.class.isInstance(rootInstance)) {
110            return findCollectionField(rootInstance, param);
111        }
112        Class<?> current = rootInstance.getClass();
113        while (current != null) {
114            try {
115                final Field declaredField = current.getDeclaredField(findName(param));
116                if (!declaredField.isAccessible()) {
117                    declaredField.setAccessible(true);
118                }
119                return declaredField.get(rootInstance);
120            } catch (final IllegalAccessException | NoSuchFieldException e) {
121                // next
122            }
123            current = current.getSuperclass();
124        }
125        throw new IllegalArgumentException("Didn't find field '" + param.getName() + "' in " + rootInstance);
126    }
127
128    private Object findCollectionField(final Object rootInstance, final ParameterMeta param) {
129        final Collection<?> collection = Collection.class.cast(rootInstance);
130        if (!collection.isEmpty()) {
131            final Object next = collection.iterator().next();
132            if (param.getPath().endsWith("[${index}]")) {
133                return next;
134            }
135            return findField(next, param);
136        }
137        return null;
138    }
139
140    private String findName(final ParameterMeta meta) {
141        return ofNullable(meta.getSource()).map(ParameterMeta.Source::name).orElse(meta.getName());
142    }
143
144    private Object tryCreatingObjectInstance(final Type javaType) {
145        final Class<?> type = Class.class.cast(javaType);
146        if (type.isPrimitive()) {
147            if (int.class == type) {
148                return 0;
149            }
150            if (long.class == type) {
151                return 0L;
152            }
153            if (double.class == type) {
154                return 0.;
155            }
156            if (float.class == type) {
157                return 0f;
158            }
159            if (short.class == type) {
160                return (short) 0;
161            }
162            if (byte.class == type) {
163                return (byte) 0;
164            }
165            if (boolean.class == type) {
166                return false;
167            }
168            throw new IllegalArgumentException("Not a primitive: " + type);
169        }
170        if (type.getName().startsWith("java.") || type.getName().startsWith("javax.")) {
171            return null;
172        }
173        try {
174            return type.getConstructor().newInstance();
175        } catch (final NoSuchMethodException | InstantiationException | IllegalAccessException
176                | InvocationTargetException e) {
177            // ignore, we'll skip the defaults
178        }
179        return null;
180    }
181
182    @Data
183    @AllArgsConstructor(access = PRIVATE)
184    public static class Instance {
185
186        private final Object value;
187
188        private final boolean created;
189    }
190}