blob: d9db488a4a89ab9bfb8373997dfeb366fdfaaead [file] [log] [blame]
/*
* 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);
}
}