xref: /OpenGrok/opengrok-indexer/src/main/java/org/opengrok/indexer/util/ClassUtil.java (revision fc53bae70c239c88aca30a43e63bed2f06bdbfe8)
1 /*
2  * CDDL HEADER START
3  *
4  * The contents of this file are subject to the terms of the
5  * Common Development and Distribution License (the "License").
6  * You may not use this file except in compliance with the License.
7  *
8  * See LICENSE.txt included in this distribution for the specific
9  * language governing permissions and limitations under the License.
10  *
11  * When distributing Covered Code, include this CDDL HEADER in each
12  * file and include the License file at LICENSE.txt.
13  * If applicable, add the following below this CDDL HEADER, with the
14  * fields enclosed by brackets "[]" replaced with your own identifying
15  * information: Portions Copyright [yyyy] [name of copyright owner]
16  *
17  * CDDL HEADER END
18  */
19 
20 /*
21  * Copyright (c) 2017, 2021, Oracle and/or its affiliates. All rights reserved.
22  */
23 package org.opengrok.indexer.util;
24 
25 import java.beans.BeanInfo;
26 import java.beans.IntrospectionException;
27 import java.beans.Introspector;
28 import java.beans.PropertyDescriptor;
29 import java.io.IOException;
30 import java.lang.reflect.Field;
31 import java.lang.reflect.InvocationTargetException;
32 import java.lang.reflect.Method;
33 import java.lang.reflect.Modifier;
34 import java.util.logging.Level;
35 import java.util.logging.Logger;
36 
37 import com.fasterxml.jackson.databind.ObjectMapper;
38 import org.apache.commons.lang3.BooleanUtils;
39 import org.opengrok.indexer.logger.LoggerFactory;
40 
41 /**
42  *
43  * @author Krystof Tulinger
44  */
45 public class ClassUtil {
46 
47     private static final Logger LOGGER = LoggerFactory.getLogger(ClassUtil.class);
48 
ClassUtil()49     private ClassUtil() {
50     }
51 
52     /**
53      * Mark all transient fields in {@code targetClass} as @Transient for the
54      * XML serialization.
55      *
56      * Fields marked with java transient keyword do not work because the
57      * XMLEncoder does not take these into account. This helper marks the fields
58      * marked with transient keyword as transient also for the XMLDecoder.
59      *
60      * @param targetClass the class
61      */
remarkTransientFields(Class<?> targetClass)62     public static void remarkTransientFields(Class<?> targetClass) {
63         try {
64             BeanInfo info = Introspector.getBeanInfo(targetClass);
65             PropertyDescriptor[] propertyDescriptors = info.getPropertyDescriptors();
66             for (Field f : targetClass.getDeclaredFields()) {
67                 if (Modifier.isTransient(f.getModifiers())) {
68                     for (PropertyDescriptor propertyDescriptor : propertyDescriptors) {
69                         if (propertyDescriptor.getName().equals(f.getName())) {
70                             propertyDescriptor.setValue("transient", Boolean.TRUE);
71                         }
72                     }
73                 }
74             }
75         } catch (IntrospectionException ex) {
76             LOGGER.log(Level.WARNING, "An exception occurred during remarking transient fields:", ex);
77         }
78     }
79 
stringToObject(String fieldName, Class<?> c, String value)80     private static Object stringToObject(String fieldName, Class<?> c, String value) throws IOException {
81         Object v;
82         String paramClass = c.getName();
83 
84         try {
85             /*
86              * Java primitive types as per
87              * <a href="https://docs.oracle.com/javase/tutorial/java/nutsandbolts/datatypes.html">java
88              * datatypes</a>.
89              */
90             if (paramClass.equals("boolean") || paramClass.equals(Boolean.class.getName())) {
91                 Boolean parsedValue = BooleanUtils.toBooleanObject(value);
92                 if (parsedValue == null) {
93                     throw new IOException(String.format("Unsupported type conversion from String to a boolean for name \"%s\" -"
94                                     + " got \"%s\" - allowed values are [false, off, 0, true, on, 1].",
95                             paramClass, value));
96                 }
97                 v = parsedValue;
98             } else if (paramClass.equals("short") || paramClass.equals(Short.class.getName())) {
99                 v = Short.valueOf(value);
100             } else if (paramClass.equals("int") || paramClass.equals(Integer.class.getName())) {
101                 v = Integer.valueOf(value);
102             } else if (paramClass.equals("long") || paramClass.equals(Long.class.getName())) {
103                 v = Long.valueOf(value);
104             } else if (paramClass.equals("float") || paramClass.equals(Float.class.getName())) {
105                 v = Float.valueOf(value);
106             } else if (paramClass.equals("double") || paramClass.equals(Double.class.getName())) {
107                 v = Double.valueOf(value);
108             } else if (paramClass.equals("byte") || paramClass.equals(Byte.class.getName())) {
109                 v = Byte.valueOf(value);
110             } else if (paramClass.equals("char") || paramClass.equals(Character.class.getName())) {
111                 v = value.charAt(0);
112             } else if (paramClass.equals(String.class.getName())) {
113                 v = value;
114             } else {
115                 ObjectMapper mapper = new ObjectMapper();
116                 v = mapper.readValue(value, c);
117             }
118         }  catch (NumberFormatException ex) {
119             throw new IOException(
120                     String.format("Unsupported type conversion from String to a number for name \"%s\" - %s.",
121                             fieldName, ex.getLocalizedMessage()), ex);
122         } catch (IndexOutOfBoundsException ex) {
123             throw new IOException(
124                     String.format("The string is not long enough to extract 1 character for name \"%s\" - %s.",
125                             fieldName, ex.getLocalizedMessage()), ex);
126         }
127 
128         return v;
129     }
130 
getSetter(Object obj, String fieldName)131     private static Method getSetter(Object obj, String fieldName) throws IOException {
132         PropertyDescriptor desc;
133         try {
134             desc = new PropertyDescriptor(fieldName, obj.getClass());
135         } catch (IntrospectionException e) {
136             throw new IOException(e);
137         }
138         Method setter = desc.getWriteMethod();
139 
140         if (setter == null) {
141             throw new IOException(
142                     String.format("No setter for the name \"%s\".", fieldName));
143         }
144 
145         if (setter.getParameterCount() != 1) {
146             // not a setter
147             /*
148              * Actually should not happen as it is not considered as a
149              * writer method so an exception would be thrown earlier.
150              */
151             throw new IOException(
152                     String.format("The setter \"%s\" for the name \"%s\" does not take exactly 1 parameter.",
153                             setter.getName(), fieldName));
154         }
155 
156         return setter;
157     }
158 
159     /**
160      * Invokes a setter on an object and passes a value to that setter.
161      *
162      * The value is passed as string and the function will automatically try to
163      * convert it to the parameter type in the setter. These conversion are
164      * available only for
165      * <a href="https://docs.oracle.com/javase/tutorial/java/nutsandbolts/datatypes.html">java
166      * primitive datatypes</a>:
167      * <ul>
168      * <li>Boolean or boolean</li>
169      * <li>Short or short</li>
170      * <li>Integer or integer</li>
171      * <li>Long or long</li>
172      * <li>Float or float</li>
173      * <li>Double or double</li>
174      * <li>Byte or byte</li>
175      * <li>Character or char</li>
176      * <li>String</li>
177      * </ul>
178      * Any other parameter type will cause an exception. The size/value itself is checked elsewhere.
179      *
180      * @param obj the object
181      * @param fieldName name of the field which will be changed
182      * @param value desired value represented as string
183      *
184      * @throws IOException if any error occurs (no suitable method, bad conversion, ...)
185      */
setFieldValue(Object obj, String fieldName, String value)186     public static void setFieldValue(Object obj, String fieldName, String value) throws IOException {
187         Method setter = getSetter(obj, fieldName);
188         Class<?> c = setter.getParameterTypes()[0];
189         Object objValue = stringToObject(fieldName, c, value);
190         invokeSetter(setter, obj, fieldName, objValue);
191     }
192 
193     /**
194      * Invokes a setter on an object and passes a value to that setter.
195      *
196      * @param obj the object
197      * @param fieldName name of the field which will be changed
198      * @param value desired value
199      * @throws IOException all exceptions from the reflection
200      */
setFieldValue(Object obj, String fieldName, Object value)201     public static void setFieldValue(Object obj, String fieldName, Object value) throws IOException {
202         Method setter = getSetter(obj, fieldName);
203         invokeSetter(setter, obj, fieldName, value);
204     }
205 
invokeSetter(Method setter, Object obj, String fieldName, Object value)206     private static void invokeSetter(Method setter, Object obj, String fieldName, Object value) throws IOException {
207         try {
208             setter.invoke(obj, value);
209         } catch (IllegalAccessException
210                 | IllegalArgumentException
211                 /*
212                  * This the case when the invocation failed because the invoked
213                  * method failed with an exception. All exceptions are
214                  * propagated through this exception.
215                  */
216                 | InvocationTargetException ex) {
217             throw new IOException(
218                     String.format("Unsupported operation with object of class %s for name \"%s\" - %s.",
219                             obj.getClass().toString(),
220                             fieldName,
221                             ex.getCause() == null
222                                     ? ex.getLocalizedMessage()
223                                     : ex.getCause().getLocalizedMessage()), ex);
224         }
225     }
226 
227     /**
228      * @param obj object
229      * @param fieldName field name
230      * @return true if field is present in the object (not recursively) or false
231      */
hasField(Object obj, String fieldName)232     public static boolean hasField(Object obj, String fieldName) {
233         try {
234             PropertyDescriptor desc = new PropertyDescriptor(fieldName, obj.getClass());
235         } catch (IntrospectionException e) {
236             return false;
237         }
238         return true;
239     }
240 
241     /**
242      * Invokes a getter of a property on an object.
243      *
244      * @param obj the object
245      * @param field string with field name
246      * @return string representation of the field value
247      * @throws java.io.IOException exception
248      */
getFieldValue(Object obj, String field)249     public static Object getFieldValue(Object obj, String field) throws IOException {
250 
251         try {
252             PropertyDescriptor desc = new PropertyDescriptor(field, obj.getClass());
253             Method getter = desc.getReadMethod();
254 
255             if (getter == null) {
256                 throw new IOException(
257                         String.format("No getter for the name \"%s\".", field));
258             }
259 
260             if (getter.getParameterCount() != 0) {
261                 /*
262                  * Actually should not happen as it is not considered as a
263                  * read method so an exception would be thrown earlier.
264                  */
265                 throw new IOException(
266                         String.format("The getter \"%s\" for the name \"%s\" takes a parameter.",
267                                 getter.getName(), field));
268             }
269 
270             return getter.invoke(obj);
271         } catch (IntrospectionException
272                 | IllegalAccessException
273                 | InvocationTargetException
274                 | IllegalArgumentException ex) {
275             throw new IOException(
276                     String.format("Unsupported operation with object of class %s for name \"%s\" - %s.",
277                             obj.getClass().toString(),
278                             field,
279                             ex.getCause() == null
280                             ? ex.getLocalizedMessage()
281                             : ex.getCause().getLocalizedMessage()),
282                     ex);
283         }
284     }
285 }
286