| /** |
| * Copyright (c) 2008-2010 Andrey Somov |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| package org.yaml.snakeyaml.representer; |
| |
| import java.beans.IntrospectionException; |
| import java.beans.Introspector; |
| import java.beans.PropertyDescriptor; |
| import java.lang.reflect.Field; |
| import java.lang.reflect.Modifier; |
| import java.lang.reflect.Type; |
| import java.util.ArrayList; |
| import java.util.HashMap; |
| import java.util.Iterator; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.Set; |
| import java.util.TreeSet; |
| |
| import org.yaml.snakeyaml.error.YAMLException; |
| import org.yaml.snakeyaml.introspector.FieldProperty; |
| import org.yaml.snakeyaml.introspector.MethodProperty; |
| import org.yaml.snakeyaml.introspector.Property; |
| import org.yaml.snakeyaml.nodes.MappingNode; |
| import org.yaml.snakeyaml.nodes.Node; |
| import org.yaml.snakeyaml.nodes.NodeId; |
| import org.yaml.snakeyaml.nodes.NodeTuple; |
| import org.yaml.snakeyaml.nodes.ScalarNode; |
| import org.yaml.snakeyaml.nodes.SequenceNode; |
| import org.yaml.snakeyaml.nodes.Tag; |
| |
| /** |
| * Represent JavaBeans |
| */ |
| public class Representer extends SafeRepresenter { |
| private boolean allowReadOnlyProperties = true; |
| |
| public Representer() { |
| this.representers.put(null, new RepresentJavaBean()); |
| } |
| |
| protected class RepresentJavaBean implements Represent { |
| private final Map<Class<? extends Object>, Set<Property>> propertiesCache = new HashMap<Class<? extends Object>, Set<Property>>(); |
| |
| public Node representData(Object data) { |
| Set<Property> properties; |
| Class<? extends Object> clazz = data.getClass(); |
| properties = propertiesCache.get(clazz); |
| if (properties == null) { |
| try { |
| properties = getProperties(clazz); |
| propertiesCache.put(clazz, properties); |
| } catch (IntrospectionException e) { |
| throw new YAMLException(e); |
| } |
| } |
| Node node = representJavaBean(properties, data); |
| return node; |
| } |
| } |
| |
| /** |
| * Tag logic:<br/> |
| * - explicit root tag is set in serializer <br/> |
| * - if there is a predefined class tag it is used<br/> |
| * - a global tag with class name is always used as tag. The JavaBean parent |
| * of the specified JavaBean may set another tag (tag:yaml.org,2002:map) |
| * when the property class is the same as runtime class |
| * |
| * @param properties |
| * JavaBean getters |
| * @param javaBean |
| * instance for Node |
| * @return Node to get serialized |
| */ |
| protected MappingNode representJavaBean(Set<Property> properties, Object javaBean) { |
| List<NodeTuple> value = new ArrayList<NodeTuple>(properties.size()); |
| Tag tag; |
| Tag customTag = classTags.get(javaBean.getClass()); |
| tag = customTag != null ? customTag : new Tag(javaBean.getClass()); |
| // flow style will be chosen by BaseRepresenter |
| MappingNode node = new MappingNode(tag, value, null); |
| representedObjects.put(objectToRepresent, node); |
| boolean bestStyle = true; |
| for (Property property : properties) { |
| Object memberValue = property.get(javaBean); |
| NodeTuple tuple = representJavaBeanProperty(javaBean, property, memberValue, customTag); |
| if (tuple == null) { |
| continue; |
| } |
| if (((ScalarNode) tuple.getKeyNode()).getStyle() != null) { |
| bestStyle = false; |
| } |
| Node nodeValue = tuple.getValueNode(); |
| if (!((nodeValue instanceof ScalarNode && ((ScalarNode) nodeValue).getStyle() == null))) { |
| bestStyle = false; |
| } |
| value.add(tuple); |
| } |
| if (defaultFlowStyle != null) { |
| node.setFlowStyle(defaultFlowStyle); |
| } else { |
| node.setFlowStyle(bestStyle); |
| } |
| return node; |
| } |
| |
| /** |
| * Represent one JavaBean property. |
| * |
| * @param javaBean |
| * - the instance to be represented |
| * @param property |
| * - the property of the instance |
| * @param propertyValue |
| * - value to be represented |
| * @param customTag |
| * - user defined Tag |
| * @return NodeTuple to be used in a MappingNode. Return null to skip the |
| * property |
| */ |
| protected NodeTuple representJavaBeanProperty(Object javaBean, Property property, |
| Object propertyValue, Tag customTag) { |
| ScalarNode nodeKey = (ScalarNode) representData(property.getName()); |
| boolean hasAlias = false; |
| if (this.representedObjects.containsKey(propertyValue)) { |
| // the first occurrence of the node must keep the tag |
| hasAlias = true; |
| } |
| Node nodeValue = representData(propertyValue); |
| // if possible try to avoid a global tag with a class name |
| if (nodeValue instanceof MappingNode && !hasAlias) { |
| // the node is a map, set or JavaBean |
| if (!Map.class.isAssignableFrom(propertyValue.getClass())) { |
| // the node is set or JavaBean |
| if (customTag == null) { |
| // custom tag is not defined |
| if (property.getType() == propertyValue.getClass()) { |
| // we do not need global tag because the property |
| // Class is the same as the runtime class |
| nodeValue.setTag(Tag.MAP); |
| } |
| } |
| } |
| } else if (propertyValue != null && Enum.class.isAssignableFrom(propertyValue.getClass())) { |
| nodeValue.setTag(Tag.STR); |
| } |
| if (nodeValue.getNodeId() != NodeId.scalar && !hasAlias) { |
| // generic collections |
| checkGlobalTag(property, nodeValue, propertyValue); |
| } |
| return new NodeTuple(nodeKey, nodeValue); |
| } |
| |
| /** |
| * Remove redundant global tag for a type safe (generic) collection if it is |
| * the same as defined by the JavaBean property |
| * |
| * @param property |
| * - JavaBean property |
| * @param node |
| * - representation of the property |
| * @param object |
| * - instance represented by the node |
| */ |
| @SuppressWarnings("unchecked") |
| protected void checkGlobalTag(Property property, Node node, Object object) { |
| Type[] arguments = property.getActualTypeArguments(); |
| if (arguments != null) { |
| if (node.getNodeId() == NodeId.sequence) { |
| // apply map tag where class is the same |
| Class<? extends Object> t = (Class<? extends Object>) arguments[0]; |
| SequenceNode snode = (SequenceNode) node; |
| List<Object> memberList = (List<Object>) object; |
| Iterator<Object> iter = memberList.iterator(); |
| for (Node childNode : snode.getValue()) { |
| Object member = iter.next(); |
| if (member != null && t.equals(member.getClass()) |
| && childNode.getNodeId() == NodeId.mapping) { |
| childNode.setTag(Tag.MAP); |
| } |
| } |
| } else if (object instanceof Set) { |
| Class t = (Class) arguments[0]; |
| MappingNode mnode = (MappingNode) node; |
| Iterator<NodeTuple> iter = mnode.getValue().iterator(); |
| Set set = (Set) object; |
| for (Object member : set) { |
| NodeTuple tuple = iter.next(); |
| if (t.equals(member.getClass()) |
| && tuple.getKeyNode().getNodeId() == NodeId.mapping) { |
| tuple.getKeyNode().setTag(Tag.MAP); |
| } |
| } |
| } else if (node.getNodeId() == NodeId.mapping) { |
| Class keyType = (Class) arguments[0]; |
| Class valueType = (Class) arguments[1]; |
| MappingNode mnode = (MappingNode) node; |
| for (NodeTuple tuple : mnode.getValue()) { |
| resetTag(keyType, tuple.getKeyNode()); |
| resetTag(valueType, tuple.getValueNode()); |
| } |
| } |
| } |
| } |
| |
| private void resetTag(Class<? extends Object> type, Node node) { |
| Tag tag = node.getTag(); |
| if (tag.matches(type)) { |
| if (Enum.class.isAssignableFrom(type)) { |
| node.setTag(Tag.STR); |
| } else { |
| node.setTag(Tag.MAP); |
| } |
| } |
| } |
| |
| /** |
| * Get JavaBean properties to be serialised. The order is respected. This |
| * method may be overridden to provide custom property selection or order. |
| * |
| * @param type |
| * - JavaBean to inspect the properties |
| * @return properties to serialise |
| */ |
| protected Set<Property> getProperties(Class<? extends Object> type) |
| throws IntrospectionException { |
| Set<Property> properties = new TreeSet<Property>(); |
| // add JavaBean getters |
| for (PropertyDescriptor property : Introspector.getBeanInfo(type).getPropertyDescriptors()) |
| if (property.getReadMethod() != null |
| && (allowReadOnlyProperties || property.getWriteMethod() != null) |
| && !property.getReadMethod().getName().equals("getClass")) { |
| properties.add(new MethodProperty(property)); |
| } |
| // add public fields |
| for (Field field : type.getFields()) { |
| int modifiers = field.getModifiers(); |
| if (Modifier.isStatic(modifiers) || Modifier.isTransient(modifiers)) |
| continue; |
| properties.add(new FieldProperty(field)); |
| } |
| if (properties.isEmpty()) { |
| throw new YAMLException("No JavaBean properties found in " + type.getName()); |
| } |
| return properties; |
| } |
| |
| public void setAllowReadOnlyProperties(boolean allowReadOnlyProperties) { |
| this.allowReadOnlyProperties = allowReadOnlyProperties; |
| } |
| } |