001/* 002 * Configurate 003 * Copyright (C) zml and Configurate contributors 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 ninja.leaping.configurate.objectmapping; 018 019import com.google.common.reflect.Invokable; 020import com.google.common.reflect.TypeToken; 021import ninja.leaping.configurate.ConfigurationNode; 022import ninja.leaping.configurate.commented.CommentedConfigurationNode; 023import ninja.leaping.configurate.objectmapping.serialize.TypeSerializer; 024import org.checkerframework.checker.nullness.qual.NonNull; 025import org.checkerframework.checker.nullness.qual.Nullable; 026 027import java.lang.reflect.Field; 028import java.lang.reflect.InvocationTargetException; 029import java.util.LinkedHashMap; 030import java.util.Map; 031 032import static java.util.Objects.requireNonNull; 033 034/** 035 * This is the object mapper. It handles conversion between configuration nodes and 036 * fields annotated with {@link Setting} in objects. 037 * 038 * Values in the node not used by the mapped object will be preserved. 039 * 040 * @param <T> The type to work with 041 */ 042public class ObjectMapper<T> { 043 private final TypeToken<T> type; 044 private final Class<? super T> clazz; 045 @Nullable 046 private final Invokable<T, T> constructor; 047 private final Map<String, FieldData> cachedFields = new LinkedHashMap<>(); 048 049 050 /** 051 * Create a new object mapper that can work with objects of the given class using the 052 * {@link DefaultObjectMapperFactory}. 053 * 054 * @param clazz The type of object 055 * @param <T> The type 056 * @return An appropriate object mapper instance. May be shared with other users. 057 * @throws ObjectMappingException If invalid annotated fields are presented 058 */ 059 public static <T> ObjectMapper<T> forClass(@NonNull Class<T> clazz) throws ObjectMappingException { 060 return DefaultObjectMapperFactory.getInstance().getMapper(clazz); 061 } 062 063 /** 064 * Create a new object mapper that can work with objects of the given type using the 065 * {@link DefaultObjectMapperFactory}. 066 * 067 * @param type The type of object 068 * @param <T> The type 069 * @return An appropriate object mapper instance. May be shared with other users. 070 * @throws ObjectMappingException If invalid annotated fields are presented 071 */ 072 public static <T> ObjectMapper<T> forType(@NonNull TypeToken<T> type) throws ObjectMappingException { 073 return DefaultObjectMapperFactory.getInstance().getMapper(type); 074 } 075 076 /** 077 * Creates a new object mapper bound to the given object. 078 * 079 * <strong>CAUTION</strong> Generic type information will be lost when creating a mapper. Provide a TypeToken to avoid this 080 * 081 * @param obj The object 082 * @param <T> The object type 083 * @return An appropriate object mapper instance. 084 * @throws ObjectMappingException when an object is provided that is not suitable for object mapping. 085 * Reasons may include but are not limited to: 086 * <ul> 087 * <li>Not annotated with {@link ninja.leaping.configurate.objectmapping.serialize.ConfigSerializable} annotation</li> 088 * <li>Invalid field types</li> 089 * </ul> 090 */ 091 @SuppressWarnings("unchecked") 092 public static <T> ObjectMapper<T>.BoundInstance forObject(@NonNull T obj) throws ObjectMappingException { 093 return forClass((Class<T>) requireNonNull(obj).getClass()).bind(obj); 094 } 095 096 /** 097 * Creates a new object mapper bound to the given object. 098 * 099 * @param type generic type of object 100 * @param obj The object 101 * @param <T> The object type 102 * @return An appropriate object mapper instance. 103 * @throws ObjectMappingException when an object is provided that is not suitable for object mapping. 104 * Reasons may include but are not limited to: 105 * <ul> 106 * <li>Not annotated with {@link ninja.leaping.configurate.objectmapping.serialize.ConfigSerializable} annotation</li> 107 * <li>Invalid field types</li> 108 * <li>Specified type is an interface</li> 109 * </ul> 110 */ 111 @SuppressWarnings("unchecked") 112 public static <T> ObjectMapper<T>.BoundInstance forObject(TypeToken<T> type, @NonNull T obj) throws ObjectMappingException { 113 return forType(requireNonNull(type)).bind(obj); 114 } 115 116 /** 117 * Holder for field-specific information 118 */ 119 protected static class FieldData { 120 private final Field field; 121 private final TypeToken<?> fieldType; 122 private final String comment; 123 124 public FieldData(Field field, String comment) throws ObjectMappingException { 125 this(field, comment, TypeToken.of(field.getGenericType())); 126 } 127 128 public FieldData(Field field, String comment, TypeToken<?> resolvedFieldType) { 129 this.field = field; 130 this.comment = comment; 131 this.fieldType = resolvedFieldType; 132 } 133 134 public void deserializeFrom(Object instance, ConfigurationNode node) throws ObjectMappingException { 135 TypeSerializer<?> serial = node.getOptions().getSerializers().get(this.fieldType); 136 if (serial == null) { 137 throw new ObjectMappingException("No TypeSerializer found for field " + field.getName() + " of type " 138 + this.fieldType); 139 } 140 Object newVal = node.isVirtual() ? null : serial.deserialize(this.fieldType, node); 141 try { 142 if (newVal == null) { 143 Object existingVal = field.get(instance); 144 if (existingVal != null) { 145 serializeTo(instance, node); 146 } 147 } else { 148 field.set(instance, newVal); 149 } 150 } catch (IllegalAccessException e) { 151 throw new ObjectMappingException("Unable to deserialize field " + field.getName(), e); 152 } 153 } 154 155 @SuppressWarnings({"rawtypes", "unchecked"}) 156 public void serializeTo(Object instance, ConfigurationNode node) throws ObjectMappingException { 157 try { 158 Object fieldVal = this.field.get(instance); 159 if (fieldVal == null) { 160 node.setValue(null); 161 } else { 162 TypeSerializer serial = node.getOptions().getSerializers().get(this.fieldType); 163 if (serial == null) { 164 throw new ObjectMappingException("No TypeSerializer found for field " + field.getName() + " of type " + this.fieldType); 165 } 166 serial.serialize(this.fieldType, fieldVal, node); 167 } 168 169 if (node instanceof CommentedConfigurationNode && this.comment != null && !this.comment.isEmpty()) { 170 CommentedConfigurationNode commentNode = ((CommentedConfigurationNode) node); 171 if (!commentNode.getComment().isPresent()) { 172 commentNode.setComment(this.comment); 173 } 174 } 175 } catch (IllegalAccessException e) { 176 throw new ObjectMappingException("Unable to serialize field " + field.getName(), e); 177 } 178 } 179 } 180 181 /** 182 * Represents an object mapper bound to a certain instance of the object 183 */ 184 public class BoundInstance { 185 private final T boundInstance; 186 187 protected BoundInstance(T boundInstance) { 188 this.boundInstance = boundInstance; 189 } 190 191 /** 192 * Populate the annotated fields in a pre-created object 193 * 194 * @param source The source to get data from 195 * @return The object provided, for easier chaining 196 * @throws ObjectMappingException If an error occurs while populating data 197 */ 198 public T populate(ConfigurationNode source) throws ObjectMappingException { 199 for (Map.Entry<String, FieldData> ent : cachedFields.entrySet()) { 200 ConfigurationNode node = source.getNode(ent.getKey()); 201 ent.getValue().deserializeFrom(boundInstance, node); 202 } 203 return boundInstance; 204 } 205 206 /** 207 * Serialize the data contained in annotated fields to the configuration node. 208 * 209 * @param target The target node to serialize to 210 * @throws ObjectMappingException if serialization was not possible due to some error. 211 */ 212 public void serialize(ConfigurationNode target) throws ObjectMappingException { 213 for (Map.Entry<String, FieldData> ent : cachedFields.entrySet()) { 214 ConfigurationNode node = target.getNode(ent.getKey()); 215 ent.getValue().serializeTo(boundInstance, node); 216 } 217 } 218 219 /** 220 * Return the instance this mapper is bound to. 221 * 222 * @return The active instance 223 */ 224 public T getInstance() { 225 return boundInstance; 226 } 227 } 228 229 /** 230 * Create a new object mapper of a given type. The given type must not be an interface. 231 * 232 * @param type The class this object mapper will work with 233 * @throws ObjectMappingException When errors occur discovering fields in the class 234 * @deprecated Use {@link #ObjectMapper(TypeToken)} instead to support parameterized types 235 */ 236 @Deprecated 237 protected ObjectMapper(Class<T> type) throws ObjectMappingException { 238 this(TypeToken.of(type)); 239 } 240 241 /** 242 * Method to determine if this ObjectMapper instance needs to maintain pre-3.7 behaviour. Override this if you are 243 * ready to take advantage of new 3.7 changes and also will be providing a subclass of ObjectMapper that overrides 244 * the old collectFields 245 * 246 * @return true to get legacy, less generic-aware treatment 247 * @deprecated Backwards compatibility measure, to be removed in 4.0 248 */ 249 @Deprecated 250 protected boolean isLegacy() { 251 if (this.getClass().getPackage() != ObjectMapper.class.getPackage()) { 252 try { 253 // If this is a child class that overrides the non-tokened colllectFields method, they should get legacy treatment 254 getClass().getDeclaredMethod("collectFields", Map.class, Class.class); 255 return true; 256 } catch (NoSuchMethodException ignore) { 257 } 258 } 259 return false; 260 } 261 262 /** 263 * Create a new object mapper of a given type 264 * 265 * @param type The type this object mapper will work with 266 * @throws ObjectMappingException When errors occur discovering fields in the class 267 */ 268 @SuppressWarnings("unchecked") 269 protected ObjectMapper(TypeToken<T> type) throws ObjectMappingException { 270 this.type = type; 271 this.clazz = type.getRawType(); 272 if (this.clazz.isInterface()) { 273 throw new ObjectMappingException("ObjectMapper can only work with concrete types"); 274 } 275 276 Invokable<T, T> constructor = null; 277 try { 278 constructor = type.constructor(type.getRawType().getDeclaredConstructor()); 279 constructor.setAccessible(true); 280 } catch (NoSuchMethodException ignore) { 281 } 282 this.constructor = constructor; 283 TypeToken<? super T> collectType = type; 284 Class<? super T> collectClass = type.getRawType(); 285 boolean legacy = isLegacy(); 286 while (true) { 287 if (legacy) { 288 collectFields(cachedFields, collectClass); 289 } else { 290 collectFields(cachedFields, collectType); 291 } 292 293 collectClass = collectClass.getSuperclass(); 294 if (collectClass.equals(Object.class)) { 295 break; 296 } 297 collectType = collectType.getSupertype((Class) collectClass); 298 } 299 } 300 301 /** 302 * Gather fields from a class, without having calculated types present 303 * @param cachedFields map to contribute fields to 304 * @param clazz active class to scan 305 * @throws ObjectMappingException when an error occurs 306 * @deprecated Use {@link #collectFields(Map, TypeToken)} instead 307 */ 308 @Deprecated 309 protected void collectFields(Map<String, FieldData> cachedFields, Class<? super T> clazz) throws ObjectMappingException { 310 // no-op 311 } 312 313 protected void collectFields(Map<String, FieldData> cachedFields, TypeToken<? super T> clazz) throws ObjectMappingException { 314 for (Field field : clazz.getRawType().getDeclaredFields()) { 315 if (field.isAnnotationPresent(Setting.class)) { 316 Setting setting = field.getAnnotation(Setting.class); 317 String path = setting.value(); 318 if (path.isEmpty()) { 319 path = field.getName(); 320 } 321 322 TypeToken<?> fieldType = clazz.resolveType(field.getGenericType()); 323 324 FieldData data = new FieldData(field, setting.comment(), fieldType); 325 field.setAccessible(true); 326 if (!cachedFields.containsKey(path)) { 327 cachedFields.put(path, data); 328 } 329 } 330 } 331 } 332 333 /** 334 * Create a new instance of an object of the appropriate type. This method is not 335 * responsible for any population. 336 * 337 * @return The new object instance 338 * @throws ObjectMappingException If constructing a new instance was not possible 339 */ 340 protected T constructObject() throws ObjectMappingException { 341 if (constructor == null) { 342 throw new ObjectMappingException("No zero-arg constructor is available for class " + type + " but is required to construct new instances!"); 343 } 344 try { 345 return constructor.invoke(null); 346 } catch (IllegalAccessException | InvocationTargetException e) { 347 throw new ObjectMappingException("Unable to create instance of target class " + type, e); 348 } 349 } 350 351 /** 352 * Returns whether this object mapper can create new object instances. This may be 353 * false if the provided class has no zero-argument constructors. 354 * 355 * @return Whether new object instances can be created 356 */ 357 public boolean canCreateInstances() { 358 return constructor != null; 359 } 360 361 /** 362 * Return a view on this mapper that is bound to a single object instance 363 * 364 * @param instance The instance to bind to 365 * @return A view referencing this mapper and the bound instance 366 */ 367 public BoundInstance bind(T instance) { 368 return new BoundInstance(instance); 369 } 370 371 /** 372 * Returns a view on this mapper that is bound to a newly created object instance 373 * 374 * @see #bind(Object) 375 * @return Bound mapper attached to a new object instance 376 * @throws ObjectMappingException If the object could not be constructed correctly 377 */ 378 public BoundInstance bindToNew() throws ObjectMappingException { 379 return new BoundInstance(constructObject()); 380 } 381 382 /** 383 * Get the mapped class. 384 * 385 * @return class 386 * @deprecated Use {@link #getType()} to be aware of parameterized types 387 */ 388 @Deprecated 389 @SuppressWarnings("unchecked") 390 public Class<T> getMappedType() { 391 return (Class<T>) this.clazz; 392 } 393 394 public TypeToken<T> getType() { 395 return this.type; 396 } 397}