| /* |
| * Copyright 2000-2014 JetBrains s.r.o. |
| * |
| * 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 com.intellij.util.xmlb; |
| |
| import com.intellij.openapi.diagnostic.Logger; |
| import com.intellij.openapi.util.Couple; |
| import com.intellij.openapi.util.JDOMUtil; |
| import com.intellij.openapi.util.Pair; |
| import com.intellij.openapi.util.text.StringUtil; |
| import com.intellij.util.ArrayUtil; |
| import com.intellij.util.containers.ConcurrentSoftValueHashMap; |
| import com.intellij.util.containers.ContainerUtil; |
| import com.intellij.util.containers.MultiMap; |
| import com.intellij.util.xmlb.annotations.*; |
| import org.jdom.Element; |
| import org.jetbrains.annotations.NonNls; |
| import org.jetbrains.annotations.NotNull; |
| import org.jetbrains.annotations.Nullable; |
| |
| import java.awt.*; |
| import java.beans.Introspector; |
| import java.lang.reflect.Field; |
| import java.lang.reflect.Method; |
| import java.lang.reflect.Modifier; |
| import java.util.*; |
| import java.util.List; |
| |
| class BeanBinding implements Binding { |
| private static final Logger LOG = Logger.getInstance("#com.intellij.util.xmlb.BeanBinding"); |
| |
| private static final Map<Class, List<Accessor>> ourAccessorCache = new ConcurrentSoftValueHashMap<Class, List<Accessor>>(); |
| |
| private final String myTagName; |
| private final Map<Binding, Accessor> myPropertyBindings = new HashMap<Binding, Accessor>(); |
| private final List<Binding> myPropertyBindingsList = new ArrayList<Binding>(); |
| private final Class<?> myBeanClass; |
| @NonNls private static final String CLASS_PROPERTY = "class"; |
| private final Accessor myAccessor; |
| |
| public BeanBinding(Class<?> beanClass, @Nullable Accessor accessor) { |
| myAccessor = accessor; |
| assert !beanClass.isArray() : "Bean is an array: " + beanClass; |
| assert !beanClass.isPrimitive() : "Bean is primitive type: " + beanClass; |
| myBeanClass = beanClass; |
| myTagName = getTagName(beanClass); |
| assert !StringUtil.isEmptyOrSpaces(myTagName) : "Bean name is empty: " + beanClass; |
| } |
| |
| @Override |
| public void init() { |
| initPropertyBindings(myBeanClass); |
| } |
| |
| private synchronized void initPropertyBindings(Class<?> beanClass) { |
| for (Accessor accessor : getAccessors(beanClass)) { |
| final Binding binding = createBindingByAccessor(accessor); |
| myPropertyBindingsList.add(binding); |
| myPropertyBindings.put(binding, accessor); |
| } |
| } |
| |
| @Override |
| public Object serialize(@NotNull Object o, Object context, SerializationFilter filter) { |
| Element element = new Element(myTagName); |
| |
| serializeInto(o, element, filter); |
| |
| return element; |
| } |
| |
| public void serializeInto(@NotNull final Object o, final Element element, SerializationFilter filter) { |
| for (Binding binding : myPropertyBindingsList) { |
| Accessor accessor = myPropertyBindings.get(binding); |
| if (!filter.accepts(accessor, o)) continue; |
| |
| //todo: optimize. Cache it. |
| final Property property = XmlSerializerImpl.findAnnotation(accessor.getAnnotations(), Property.class); |
| if (property != null) { |
| try { |
| if (!property.filter().newInstance().accepts(accessor, o)) continue; |
| } |
| catch (InstantiationException e) { |
| throw new XmlSerializationException(e); |
| } |
| catch (IllegalAccessException e) { |
| throw new XmlSerializationException(e); |
| } |
| } |
| |
| Object node = binding.serialize(o, element, filter); |
| if (node != element) { |
| if (node instanceof org.jdom.Attribute) { |
| org.jdom.Attribute attr = (org.jdom.Attribute)node; |
| element.setAttribute(attr.getName(), attr.getValue()); |
| } |
| else { |
| JDOMUtil.addContent(element, node); |
| } |
| } |
| } |
| } |
| |
| public void deserializeInto(final Object bean, @NotNull Element element) { |
| _deserializeInto(bean, element); |
| } |
| |
| @Override |
| public Object deserialize(Object o, @NotNull Object... nodes) { |
| return _deserializeInto(instantiateBean(), nodes); |
| } |
| |
| private Object _deserializeInto(final Object result, @NotNull Object... aNodes) { |
| List<Object> nodes = new ArrayList<Object>(); |
| for (Object aNode : aNodes) { |
| if (XmlSerializerImpl.isIgnoredNode(aNode)) continue; |
| nodes.add(aNode); |
| } |
| |
| if (nodes.size() != 1) { |
| throw new XmlSerializationException("Wrong set of nodes: " + nodes + " for bean" + myBeanClass + " in " + myAccessor); |
| } |
| assert nodes.get(0) instanceof Element : "Wrong node: " + nodes; |
| Element e = (Element)nodes.get(0); |
| |
| ArrayList<Binding> bindings = new ArrayList<Binding>(myPropertyBindings.keySet()); |
| |
| MultiMap<Binding, Object> data = new MultiMap<Binding, Object>(); |
| |
| final Object[] children = JDOMUtil.getChildNodesWithAttrs(e); |
| nextNode: |
| for (Object child : children) { |
| if (XmlSerializerImpl.isIgnoredNode(child)) continue; |
| |
| for (Binding binding : bindings) { |
| if (binding.isBoundTo(child)) { |
| data.putValue(binding, child); |
| continue nextNode; |
| } |
| } |
| |
| { |
| final String message = "Format error: no binding for " + child + " inside " + this; |
| LOG.debug(message); |
| Logger.getInstance(myBeanClass.getName()).debug(message); |
| Logger.getInstance("#" + myBeanClass.getName()).debug(message); |
| } |
| } |
| |
| for (Object o1 : data.keySet()) { |
| Binding binding = (Binding)o1; |
| Collection<Object> nn = data.get(binding); |
| binding.deserialize(result, ArrayUtil.toObjectArray(nn)); |
| } |
| |
| return result; |
| } |
| |
| private Object instantiateBean() { |
| return XmlSerializerImpl.newInstance(myBeanClass); |
| } |
| |
| @Override |
| public boolean isBoundTo(Object node) { |
| return node instanceof Element && ((Element)node).getName().equals(myTagName); |
| } |
| |
| @Override |
| public Class getBoundNodeType() { |
| return Element.class; |
| } |
| |
| private static String getTagName(Class<?> aClass) { |
| for (Class<?> c = aClass; c != null; c = c.getSuperclass()) { |
| String name = getTagNameFromAnnotation(c); |
| if (name != null) { |
| return name; |
| } |
| } |
| return aClass.getSimpleName(); |
| } |
| |
| private static String getTagNameFromAnnotation(Class<?> aClass) { |
| Tag tag = aClass.getAnnotation(Tag.class); |
| if (tag != null && !tag.value().isEmpty()) return tag.value(); |
| return null; |
| } |
| |
| @NotNull |
| static List<Accessor> getAccessors(Class<?> aClass) { |
| List<Accessor> accessors = ourAccessorCache.get(aClass); |
| if (accessors != null) { |
| return accessors; |
| } |
| |
| accessors = ContainerUtil.newArrayList(); |
| |
| if (aClass != Rectangle.class) { // special case for Rectangle.class to avoid infinite recursion during serialization due to bounds() method |
| collectPropertyAccessors(aClass, accessors); |
| } |
| collectFieldAccessors(aClass, accessors); |
| |
| ourAccessorCache.put(aClass, accessors); |
| |
| return accessors; |
| } |
| |
| private static void collectPropertyAccessors(Class<?> aClass, List<Accessor> accessors) { |
| final Map<String, Couple<Method>> candidates = ContainerUtil.newTreeMap(); // (name,(getter,setter)) |
| for (Method method : aClass.getMethods()) { |
| if (!Modifier.isPublic(method.getModifiers())) continue; |
| final Pair<String, Boolean> propertyData = getPropertyData(method.getName()); // (name,isSetter) |
| if (propertyData == null || propertyData.first.equals(CLASS_PROPERTY)) continue; |
| if (method.getParameterTypes().length != (propertyData.second ? 1 : 0)) continue; |
| |
| Couple<Method> candidate = candidates.get(propertyData.first); |
| if (candidate == null) candidate = Couple.getEmpty(); |
| if ((propertyData.second ? candidate.second : candidate.first) != null) continue; |
| candidate = Couple.of(propertyData.second ? candidate.first : method, propertyData.second ? method : candidate.second); |
| candidates.put(propertyData.first, candidate); |
| } |
| for (Map.Entry<String, Couple<Method>> candidate: candidates.entrySet()) { |
| final Couple<Method> methods = candidate.getValue(); // (getter,setter) |
| if (methods.first != null && methods.second != null && |
| methods.first.getReturnType().equals(methods.second.getParameterTypes()[0]) && |
| XmlSerializerImpl.findAnnotation(methods.first.getAnnotations(), Transient.class) == null && |
| XmlSerializerImpl.findAnnotation(methods.second.getAnnotations(), Transient.class) == null) { |
| accessors.add(new PropertyAccessor(candidate.getKey(), methods.first.getReturnType(), methods.first, methods.second)); |
| } |
| } |
| } |
| |
| private static void collectFieldAccessors(Class<?> aClass, List<Accessor> accessors) { |
| for (Field field : aClass.getFields()) { |
| final int modifiers = field.getModifiers(); |
| if (Modifier.isPublic(modifiers) && !Modifier.isStatic(modifiers) && |
| !Modifier.isFinal(modifiers) && !Modifier.isTransient(modifiers) && |
| XmlSerializerImpl.findAnnotation(field.getAnnotations(), Transient.class) == null) { |
| accessors.add(new FieldAccessor(field)); |
| } |
| } |
| } |
| |
| @Nullable |
| private static Pair<String, Boolean> getPropertyData(final String methodName) { |
| String part = ""; |
| boolean isSetter = false; |
| if (methodName.startsWith("get")) { |
| part = methodName.substring(3, methodName.length()); |
| } |
| else if (methodName.startsWith("is")) { |
| part = methodName.substring(2, methodName.length()); |
| } |
| else if (methodName.startsWith("set")) { |
| part = methodName.substring(3, methodName.length()); |
| isSetter = true; |
| } |
| return !part.isEmpty() ? Pair.create(Introspector.decapitalize(part), isSetter) : null; |
| } |
| |
| public String toString() { |
| return "BeanBinding[" + myBeanClass.getName() + ", tagName=" + myTagName + "]"; |
| } |
| |
| private static Binding createBindingByAccessor(final Accessor accessor) { |
| final Binding binding = _createBinding(accessor); |
| binding.init(); |
| return binding; |
| } |
| |
| private static Binding _createBinding(final Accessor accessor) { |
| Property property = XmlSerializerImpl.findAnnotation(accessor.getAnnotations(), Property.class); |
| Tag tag = XmlSerializerImpl.findAnnotation(accessor.getAnnotations(), Tag.class); |
| Attribute attribute = XmlSerializerImpl.findAnnotation(accessor.getAnnotations(), Attribute.class); |
| Text text = XmlSerializerImpl.findAnnotation(accessor.getAnnotations(), Text.class); |
| |
| final Binding binding = XmlSerializerImpl.getTypeBinding(accessor.getGenericType(), accessor); |
| |
| if (binding instanceof JDOMElementBinding) return binding; |
| |
| if (text != null) return new TextBinding(accessor); |
| |
| if (attribute != null) { |
| return new AttributeBinding(accessor, attribute); |
| } |
| |
| if (tag != null) { |
| if (!tag.value().isEmpty()) return new TagBinding(accessor, tag); |
| } |
| |
| boolean surroundWithTag = true; |
| |
| if (property != null) { |
| surroundWithTag = property.surroundWithTag(); |
| } |
| |
| if (!surroundWithTag) { |
| if (!Element.class.isAssignableFrom(binding.getBoundNodeType())) { |
| throw new XmlSerializationException("Text-serializable properties can't be serialized without surrounding tags: " + accessor); |
| } |
| return new AccessorBindingWrapper(accessor, binding); |
| } |
| |
| OptionTag optionTag = XmlSerializerImpl.findAnnotation(accessor.getAnnotations(), OptionTag.class); |
| return new OptionTagBinding(accessor, optionTag); |
| } |
| } |