/*
* Copyright (C) 2012-2015 DataStax Inc.
*
* 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.datastax.driver.mapping;
import com.datastax.driver.core.CCMTestsSupport;
import com.datastax.driver.mapping.annotations.PartitionKey;
import com.datastax.driver.mapping.annotations.Table;
import com.google.common.io.Closeables;
import org.objectweb.asm.*;
import org.testng.annotations.Test;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Constructor;
import static org.testng.Assert.assertEquals;
@SuppressWarnings("unused")
public class SyntheticFieldsMapperTest extends CCMTestsSupport {
@Override
public void onTestContextInitialized() {
execute("CREATE TABLE synthetic_fields (id int PRIMARY KEY)");
}
/**
* Test that synthetic fields are ignored by the {@code AnnotationParser} (JAVA-465).
* <p/>
* This test creates a modified version of this class where {@code futureSynthetic} is marked as synthetic, and
* therefore ignored by the {@code AnnotationParser}. If the test throws an error "Cannot find matching getter and
* setter for field 'futureSynthetic'", it means that the field is not ignored by the mapped. However,
* if it succeed, we know that the field was ignored and that the fix does the right job.
* <p/>
* If the class {@code ClassWithSyntheticField} was used as-is, the {@code Mapper} would complain that there are
* no getter and setter for the field {@code futureSynthetic}.
* <p/>
* Note that we could also have used not-static inner classes, but the {@code Mapper} is never able to instantiate
* those as they require a reference to their enclosing class that is not provided at instantiation time.
*/
@Test(groups = "short")
public void should_ignore_synthetic_fields() {
Class<?> classWithSyntheticFields = makeTestClassWithSyntheticFields();
Object instance = instantiateNewClass(classWithSyntheticFields, 42);
// Here we cannot use ClassWithSyntheticField since it is not the one the source file was compiled against
// Instead, it is a different Class (identity + ClassLoader), a modified version of ClassWithSyntheticField
// Nice ClassCastException will be thrown if we try to cast it to ClassWithSyntheticField
@SuppressWarnings("unchecked")
Mapper<Object> m = (Mapper<Object>) new MappingManager(session()).mapper(classWithSyntheticFields);
m.save(instance);
assertEquals(m.get(42), instance);
}
private Class<?> makeTestClassWithSyntheticFields() {
InputStream stream = null;
try {
// Get class bytes
Class<ClassWithSyntheticField> c = ClassWithSyntheticField.class;
String classAsPath = c.getName().replace('.', '/') + ".class";
stream = c.getClassLoader().getResourceAsStream(classAsPath);
// Make "futureSynthetic" field actually synthetic
ClassWriter cw = new ClassWriter(0);
ClassVisitor cv = new SyntheticFieldCreator(Opcodes.ASM5, cw);
ClassReader cr = new ClassReader(stream);
cr.accept(cv, 0);
byte[] updatedClassBytes = cw.toByteArray();
// Build the new class
return new InterceptingClassLoader().defineClass(
ClassWithSyntheticField.class.getName(),
updatedClassBytes);
} catch (IOException e) {
throw new RuntimeException("Could not read Class bytes", e);
} finally {
try {
Closeables.close(stream, true);
} catch (IOException ignored) {
}
}
}
private Object instantiateNewClass(Class<?> classWithSyntheticFields, int id) {
try {
Constructor<?> declaredConstructor = classWithSyntheticFields.getDeclaredConstructor(int.class);
return declaredConstructor.newInstance(id);
} catch (Exception e) {
throw new RuntimeException("Could not instantiate Class", e);
}
}
@Table(name = "synthetic_fields")
public static class ClassWithSyntheticField {
@PartitionKey
private int id;
// Intentionally broken class: there is no getter/setter for this field
private int futureSynthetic;
// This constructor will be used by the Mapper
@SuppressWarnings("unused")
public ClassWithSyntheticField() {
}
// This constructor is invoked using reflection
@SuppressWarnings("unused")
public ClassWithSyntheticField(int id) {
this.id = id;
}
// Getters & Setters used by the Mapper
@SuppressWarnings("unused")
public int getId() {
return id;
}
// Getters & Setters used by the Mapper
@SuppressWarnings("unused")
public void setId(int id) {
this.id = id;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
ClassWithSyntheticField that = (ClassWithSyntheticField) o;
return id == that.id;
}
@Override
public int hashCode() {
return id;
}
}
private static class SyntheticFieldCreator extends ClassVisitor {
public SyntheticFieldCreator(int api, ClassVisitor cv) {
super(api, cv);
}
@Override
public FieldVisitor visitField(int access, String name, String desc, String signature, Object value) {
int newAccesses = access;
if ("futureSynthetic".equals(name)) {
newAccesses = access + Opcodes.ACC_SYNTHETIC;
}
return super.visitField(newAccesses, name, desc, signature, value);
}
}
private static class InterceptingClassLoader extends ClassLoader {
public Class defineClass(String name, byte[] b) {
return defineClass(name, b, 0, b.length);
}
}
}