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