06af2a195f2f62e23035d01bff10a3ae93520674
[mdsal.git] / binding / mdsal-binding-spec-util / src / main / java / org / opendaylight / mdsal / binding / spec / reflect / StringValueObjectFactory.java
1 /*
2  * Copyright (c) 2016 Pantheon Technologies s.r.o. and others.  All rights reserved.
3  *
4  * This program and the accompanying materials are made available under the
5  * terms of the Eclipse Public License v1.0 which accompanies this distribution,
6  * and is available at http://www.eclipse.org/legal/epl-v10.html
7  */
8 package org.opendaylight.mdsal.binding.spec.reflect;
9
10 import static java.util.Objects.requireNonNull;
11
12 import com.google.common.annotations.Beta;
13 import com.google.common.base.Throwables;
14 import java.lang.invoke.MethodHandle;
15 import java.lang.invoke.MethodHandles;
16 import java.lang.invoke.MethodHandles.Lookup;
17 import java.lang.invoke.MethodType;
18 import java.lang.reflect.Constructor;
19 import java.lang.reflect.Field;
20 import java.lang.reflect.InvocationTargetException;
21 import java.security.AccessController;
22 import java.security.PrivilegedAction;
23 import org.slf4j.Logger;
24 import org.slf4j.LoggerFactory;
25
26 /**
27  * Utility class for instantiating value-type generated objects with String being the base type. Unlike the normal
28  * constructor, instances of this class bypass string validation.
29  *
30  * <p>
31  * THE USE OF THIS CLASS IS DANGEROUS AND SHOULD ONLY BE USED TO IMPLEMENT WELL-AUDITED AND CORRECT UTILITY METHODS
32  * SHIPPED WITH MODELS TO PROVIDE INSTANTIATION FROM TYPES DIFFERENT THAN STRING.
33  *
34  * <p>
35  * APPLICATION CODE <em>MUST NOT</em> USE THIS CLASS DIRECTLY. VIOLATING THIS CONSTRAINT HAS SECURITY AND CORRECTNESS
36  * IMPLICATIONS ON EVERY USER INTERACTING WITH THE RESULTING OBJECTS.
37  *
38  * @param <T> Resulting object type
39  */
40 @Beta
41 public final class StringValueObjectFactory<T> {
42     private static final MethodType CONSTRUCTOR_METHOD_TYPE = MethodType.methodType(Object.class, Object.class);
43     private static final MethodType SETTER_METHOD_TYPE = MethodType.methodType(void.class, Object.class, String.class);
44     private static final Logger LOG = LoggerFactory.getLogger(StringValueObjectFactory.class);
45     private static final Lookup LOOKUP = MethodHandles.lookup();
46
47     private final MethodHandle constructor;
48     private final MethodHandle setter;
49     private final T template;
50
51     private StringValueObjectFactory(final T template, final MethodHandle constructor, final MethodHandle setter) {
52         this.template = requireNonNull(template);
53         this.constructor = constructor.bindTo(template);
54         this.setter = requireNonNull(setter);
55     }
56
57     public static <T> StringValueObjectFactory<T> create(final Class<T> clazz, final String templateString) {
58         final Constructor<T> stringConstructor;
59         try {
60             stringConstructor = clazz.getConstructor(String.class);
61         } catch (NoSuchMethodException e) {
62             throw new IllegalArgumentException(String.format("%s does not have a String constructor", clazz), e);
63         }
64
65         final T template;
66         try {
67             template = stringConstructor.newInstance(templateString);
68         } catch (InstantiationException | InvocationTargetException | IllegalAccessException e) {
69             throw new IllegalArgumentException(String.format("Failed to instantiate template %s for '%s'", clazz,
70                 templateString), e);
71         }
72
73         final Constructor<T> copyConstructor;
74         try {
75             copyConstructor = clazz.getConstructor(clazz);
76         } catch (NoSuchMethodException e) {
77             throw new IllegalArgumentException(String.format("%s does not have a copy constructor", clazz), e);
78         }
79
80         final Field f = findValueField(clazz);
81         final StringValueObjectFactory<T> ret;
82         try {
83             ret = new StringValueObjectFactory<>(template,
84                     LOOKUP.unreflectConstructor(copyConstructor).asType(CONSTRUCTOR_METHOD_TYPE),
85                     LOOKUP.unreflectSetter(f).asType(SETTER_METHOD_TYPE));
86         } catch (IllegalAccessException e) {
87             throw new IllegalStateException("Failed to instantiate method handles", e);
88         }
89
90         // Let us be very defensive and scream loudly if the invocation does not come from the same package. This
91         // is far from perfect, but better than nothing.
92         final Throwable t = new Throwable("Invocation stack");
93         t.fillInStackTrace();
94         if (matchesPackage(clazz.getPackage().getName(), t.getStackTrace())) {
95             LOG.info("Instantiated factory for {}", clazz);
96         } else {
97             LOG.warn("Instantiated factory for {} outside its package", clazz, t);
98         }
99
100         return ret;
101     }
102
103     private static boolean matchesPackage(final String pkg, final StackTraceElement[] stackTrace) {
104         for (StackTraceElement e : stackTrace) {
105             final String sp = e.getClassName();
106             if (sp.startsWith(pkg) && sp.lastIndexOf('.') == pkg.length()) {
107                 return true;
108             }
109         }
110
111         return false;
112     }
113
114     private static Field findValueField(final Class<?> orig) {
115         NoSuchFieldException cause = null;
116         Class<?> clazz = orig;
117         while (clazz != null) {
118             final Field f;
119             try {
120                 f = clazz.getDeclaredField("_value");
121             } catch (NoSuchFieldException e) {
122                 if (cause != null) {
123                     e.addSuppressed(cause);
124                 }
125                 cause = e;
126                 clazz = clazz.getSuperclass();
127                 continue;
128             }
129
130             return AccessController.doPrivileged((PrivilegedAction<Field>) () -> {
131                 f.setAccessible(true);
132                 return f;
133             });
134         }
135
136         throw new IllegalArgumentException(orig + " nor its superclasses define required internal field _value", cause);
137     }
138
139     @SuppressWarnings("checkstyle:illegalCatch")
140     public T newInstance(final String string) {
141         requireNonNull(string, "Argument may not be null");
142
143         try {
144             final T ret = (T) constructor.invokeExact();
145             setter.invokeExact(ret, string);
146             LOG.trace("Instantiated new object {} value {}", ret.getClass(), string);
147             return ret;
148         } catch (Throwable e) {
149             Throwables.throwIfUnchecked(e);
150             throw new RuntimeException(e);
151         }
152     }
153
154     public T getTemplate() {
155         return template;
156     }
157 }