/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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.apache.brooklyn.util.core.xstream;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.thoughtworks.xstream.core.Caching;
import com.thoughtworks.xstream.mapper.Mapper;
import com.thoughtworks.xstream.mapper.MapperWrapper;
/**
* <p>Compiler independent outer class field mapper.</p>
* <p>Different compilers generate different indexes for the names of outer class reference
* field (this$N) leading to deserialization errors.</p>
* <ul>
* <li> eclipse-[groovy-]compiler counts all outer static classes
* <li> OpenJDK/Oracle/IBM compiler starts at 0, regardless of the nesting level
* </ul>
* <p>The mapper will be able to update field names for instances with a single this$N
* field only (including those from parent classes).</p>
* <p>For difference between generated field names compare
* {@code src/test/java/brooklyn/util/xstream/compiler_compatibility_eclipse.xml} and
* {@code src/test/java/brooklyn/util/xstream/compiler_compatibility_oracle.xml},
* generated from {@code org.apache.brooklyn.core.util.xstream.CompilerCompatibilityTest}</p>
* <p>JLS 1.1 relevant section, copied verbatim for a lack of reliable URL:</p>
* <blockquote>
* <p>Java 1.1 compilers are strongly encouraged, though not required, to use the
* following naming conventions when implementing inner classes. Compilers may
* not use synthetic names of the forms defined here for any other purposes.</p>
* <p>A synthetic field pointing to the outermost enclosing instance is named this$0.
* The next-outermost enclosing instance is this$1, and so forth. (At most one such
* field is necessary in any given inner class.) A synthetic field containing a copy
* of a constant v is named val$v. These fields are final.</p>
* </blockquote>
* <p>Currently available at
* http://web.archive.org/web/20000830111107/http://java.sun.com/products/jdk/1.1/docs/guide/innerclasses/spec/innerclasses.doc10.html</p>
*/
public class CompilerIndependentOuterClassFieldMapper extends MapperWrapper implements Caching {
public static final Logger LOG = LoggerFactory.getLogger(CompilerIndependentOuterClassFieldMapper.class);
private static final String OUTER_CLASS_FIELD_PREFIX = "this$";
private final Map<String, Collection<String>> classOuterFields = new ConcurrentHashMap<String, Collection<String>>();
public CompilerIndependentOuterClassFieldMapper(Mapper wrapped) {
super(wrapped);
classOuterFields.put(Object.class.getName(), Collections.<String>emptyList());
}
@Override
public String realMember(@SuppressWarnings("rawtypes") Class type, String serialized) {
// Let com.thoughtworks.xstream.mapper.OuterClassMapper also run on the input.
String serializedFieldName = super.realMember(type, serialized);
if (serializedFieldName.startsWith(OUTER_CLASS_FIELD_PREFIX)) {
Collection<String> compiledFieldNames = findOuterClassFieldNames(type);
if (compiledFieldNames.size() == 0) {
throw new IllegalStateException("Unable to find any outer class fields in " + type + ", searching specifically for " + serializedFieldName);
}
Set<String> uniqueFieldNames = new HashSet<String>(compiledFieldNames);
String deserializeFieldName;
if (!compiledFieldNames.contains(serializedFieldName)) {
String msg =
"Unable to find outer class field " + serializedFieldName + " in class " + type + ". " +
"This could be caused by " +
"1) changing the class (or one of its parents) to a static or " +
"2) moving the class to a different lexical level (enclosing classes) or " +
"3) using a different compiler (i.e eclipse vs oracle) at the time the object was serialized. ";
if (uniqueFieldNames.size() == 1) {
// Try to fix the field naming only for the case with a single field or
// multiple fields with the same name, in which case XStream puts defined-in
// for the field declared in super.
//
// We don't have access to the XML elements from here to check for same name
// so we check the target class instead. This should work most of the time, but
// if code is recompiled in such a way that the new instance has fields with
// different names, where only the field of the extending class is renamed and
// the super field is not, then the instance will be deserialized incorrectly -
// the super field will be assigned both times. If the field type is incompatible
// then a casting exception will be thrown, if it's the same then only the warning
// below will indicate of a possible problem down the line - most probably NPE on
// the this$N field.
deserializeFieldName = compiledFieldNames.iterator().next();
LOG.warn(msg + "Will use the field " + deserializeFieldName + " instead.");
} else {
// Multiple fields with differing names case - don't try to fix it.
// Better fail with an explicit error, and have someone fix it manually,
// than try to fix it here non-reliably and have it fail down the line
// with some unrelated error.
// XStream will fail later with a field not found exception.
LOG.error(msg + "Will fail with a field not found exception. " +
"Edit the persistence state manually and update the field names. "+
"Existing field names are " + uniqueFieldNames);
deserializeFieldName = serializedFieldName;
}
} else {
if (uniqueFieldNames.size() > 1) {
// Log at debug level as the actual problem would occur in very specific cases. Only
// useful when the compiler is changed, otherwise leads to false positives.
LOG.debug("Deserializing the non-static class " + type + " with multiple outer class fields " + uniqueFieldNames + ". " +
"When changing compilers it's possible that the instance won't be able to be deserialized due to changed outer class field names. " +
"In those cases deserialization could fail with field not found exception or class cast exception following this log line.");
}
deserializeFieldName = serializedFieldName;
}
return deserializeFieldName;
} else {
return serializedFieldName;
}
}
private Collection<String> findOuterClassFieldNames(Class<?> type) {
Collection<String> fields = classOuterFields.get(type.getName());
if (fields == null) {
fields = new ArrayList<String>();
addOuterClassFields(type, fields);
classOuterFields.put(type.getName(), fields);
}
return fields;
}
private void addOuterClassFields(Class<?> type, Collection<String> fields) {
for (Field field : type.getDeclaredFields()) {
if (field.isSynthetic()) {
fields.add(field.getName());
}
}
if (type.getSuperclass() != null) {
addOuterClassFields(type.getSuperclass(), fields);
}
}
@Override
public void flushCache() {
classOuterFields.keySet().retainAll(Collections.singletonList(Object.class.getName()));
}
}