001/*
002 * Trident - A Multithreaded Server Alternative
003 * Copyright 2014 The TridentSDK Team
004 *
005 * Licensed under the Apache License, Version 2.0 (the "License");
006 * you may not use this file except in compliance with the License.
007 * You may obtain a copy of the License at
008 *
009 *    http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017package net.tridentsdk.reflect;
018
019import com.google.common.collect.Iterators;
020import com.google.common.collect.Lists;
021import com.google.common.collect.Maps;
022import com.google.common.collect.PeekingIterator;
023import net.tridentsdk.util.TridentLogger;
024
025import javax.annotation.concurrent.ThreadSafe;
026import java.lang.reflect.Constructor;
027import java.lang.reflect.Field;
028import java.lang.reflect.InvocationTargetException;
029import java.lang.reflect.Modifier;
030import java.util.Arrays;
031import java.util.Iterator;
032import java.util.List;
033import java.util.Map;
034
035/**
036 * Injects the target class, or creates a new instance of an injected class
037 *
038 * @author The TridentSDK Team
039 * @since 0.3-alpha-DP
040 * @param <T> the type to inject for
041 */
042@ThreadSafe
043public final class Injector<T> {
044    private static final Map<Class<?>, Producer<?>> injectors = Maps.newConcurrentMap();
045
046    private final Class<T> clazz;
047
048    private Injector(Class<T> clazz) {
049        this.clazz = clazz;
050    }
051
052    /**
053     * Binds a class
054     *
055     * <p>This returns an injector which you MUST use to bind the producer</p>
056     *
057     * @param clazz the class to bind
058     * @param <T> the type of the class
059     * @return the injector which provides binding members
060     */
061    public static <T> Injector<T> inject(Class<T> clazz) {
062        return new Injector<>(clazz);
063    }
064
065    /**
066     * Binds the provided class to the producer
067     *
068     * <p>The producer creates objects for injectable fields with that particular type</p>
069     *
070     * @param producer the producing class
071     */
072    public void with(Producer<T> producer) {
073        injectors.put(this.clazz, producer);
074    }
075
076    /**
077     * Binds the provided class to the instance given
078     *
079     * <p>This provides singleton-like functionality for the injector</p>
080     *
081     * @param instance the instance to return for any injection
082     */
083    public void with(final T instance) {
084        with(new Producer<T>() {
085            @Override
086            public T produce() {
087                return instance;
088            }
089
090            @Override
091            public T produce(Class<?> metadata) {
092                return instance;
093            }
094        });
095    }
096
097    /**
098     * Injects all static fields in the class
099     *
100     * <p>This does not inject any constructors, only static fields of the provided class to inject</p>
101     *
102     * @param clazz the class with static fields to inject
103     */
104    public static void staticInject(Class<?> clazz) {
105        for (Field field : findFields(clazz)) {
106            if (!Modifier.isStatic(field.getModifiers())) continue;
107            setField(null, field);
108        }
109    }
110
111    /**
112     * Creates a new object which has injectable fields, using the injectable constructor
113     *
114     * @param clazz the class to instantiate
115     * @param args the parameters, not including the injectable classes, in order of declaration
116     * @param <T> the type to return
117     * @return the new object
118     */
119    public static <T> T newObject(Class<T> clazz, Object... args) {
120        for (Map.Entry<Constructor, Class<?>[]> entry : findConstructors(clazz).entrySet()) {
121            List<Object> arguments = Lists.newArrayList();
122            Constructor constructor = entry.getKey();
123            Class<?>[] constructorParameters = entry.getValue();
124            PeekingIterator<Object> iterator = Iterators.peekingIterator(Iterators.forArray(args));
125
126            if (!checkArray(args, constructorParameters))
127                continue;
128
129            for (Class<?> c : constructorParameters) {
130                Producer<?> producer = injectors.get(c);
131                Inject inject = (Inject) constructor.getAnnotation(Inject.class);
132
133                if (iterator.hasNext()) {
134                    if (iterator.peek().getClass() == c) {
135                        arguments.add(iterator.next());
136                        continue;
137                    }
138                }
139
140                if (producer == null) {
141                    if (iterator.hasNext()) {
142                        if (iterator.peek().getClass() == c) {
143                            arguments.add(iterator.next());
144                            continue;
145                        }
146                    }
147
148                    TridentLogger.get().error(new IllegalArgumentException(
149                            "Constructor " + clazz.getName() + "(" +
150                                    Arrays.toString(constructorParameters)
151                                            .replaceAll("class ", "")
152                                            .replaceAll("\\[", "")
153                                            .replaceAll("\\]", "") + ") " +
154                                    "does not provide or registered parameter " + c.getName()));
155                    return null;
156                } else {
157                    if (inject.meta() == Class.class) {
158                        arguments.add(producer.produce());
159                    } else {
160                        arguments.add(producer.produce(inject.meta()));
161                    }
162                }
163            }
164
165            try {
166                T t = (T) constructor.newInstance(arguments.toArray());
167                for (Field field : findFields(clazz)) {
168                    setField(t, field);
169                }
170                return t;
171            } catch (InstantiationException | IllegalAccessException | InvocationTargetException e) {
172                TridentLogger.get().error(e);
173                return null;
174            }
175        }
176
177        try {
178            T t = clazz.newInstance();
179            for (Field field : findFields(clazz)) {
180                setField(t, field);
181            }
182            return t;
183        } catch (InstantiationException | IllegalAccessException e) {
184            TridentLogger.get().error(e);
185            return null;
186        }
187    }
188
189    /**
190     * Injects all fields of an existing object
191     *
192     * @param instance the instance of the object to inject
193     */
194    public static void injectAll(Object instance) {
195        for (Field field : findFields(instance.getClass())) {
196            setField(instance, field);
197        }
198    }
199
200    private static void setField(Object instance, Field field) {
201        field.setAccessible(true);
202
203        Class<?> type = field.getType();
204        Producer<?> producer = injectors.get(type);
205        if (producer == null) {
206            TridentLogger.get().error(new IllegalArgumentException("Class " +
207                    instance.getClass().getName() + " does not have bound injector for type " + type.getName()));
208            return;
209        }
210
211        Inject inject = field.getAnnotation(Inject.class);
212        try {
213            if (inject.meta() == Class.class) {
214                field.set(instance, producer.produce());
215            } else {
216                field.set(instance, producer.produce(inject.meta()));
217            }
218        } catch (IllegalAccessException e) {
219            TridentLogger.get().error(e);
220        }
221    }
222
223    private static List<Field> findFields(Class<?> c) {
224        List<Field> fields = Lists.newArrayList();
225        for (Field field : c.getDeclaredFields()) {
226            if (field.getAnnotation(Inject.class) != null)
227                fields.add(field);
228        }
229        return fields;
230    }
231
232    private static Map<Constructor, Class<?>[]> findConstructors(Class<?> c) {
233        Map<Constructor, Class<?>[]> map = Maps.newHashMap();
234        for (Constructor constructor : c.getDeclaredConstructors()) {
235            if (constructor.getAnnotation(Inject.class) != null)
236                map.put(constructor, constructor.getParameterTypes());
237        }
238
239        for (Constructor constructor : c.getConstructors()) {
240            if (map.containsKey(constructor)) continue;
241            if (constructor.getAnnotation(Inject.class) != null)
242                map.put(constructor, constructor.getParameterTypes());
243        }
244
245        return map;
246    }
247
248    private static boolean checkArray(Object[] args, Class<?>[] params) {
249        Iterator<Class<?>> iterator = Iterators.forArray(params);
250        for (Object o : args) {
251            if (!Iterators.contains(iterator, o.getClass()))
252                return false;
253        }
254
255        return true;
256    }
257}