blob: cee8f03a4da61c0d6a625125b4b9dea8e72d4d63 [file] [log] [blame]
/*
* 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.openjpa.enhance;
import java.io.Externalizable;
import java.io.ObjectInput;
import java.lang.reflect.Constructor;
import java.lang.reflect.Member;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.Collection;
import org.apache.openjpa.lib.log.Log;
import org.apache.openjpa.lib.util.Localizer;
import org.apache.openjpa.lib.util.Localizer.Message;
import org.apache.openjpa.lib.util.StringUtil;
import org.apache.openjpa.meta.AccessCode;
import org.apache.openjpa.meta.ClassMetaData;
import org.apache.openjpa.meta.FieldMetaData;
import org.apache.openjpa.util.InternalException;
import org.apache.openjpa.util.UserException;
import serp.bytecode.BCClass;
import serp.bytecode.BCField;
import serp.bytecode.BCMethod;
/**
* <p>Validates that a given type meets the JPA contract, plus a few
* OpenJPA-specific additions for subclassing / redefinition:
*
* <ul>
* <li>must have an accessible no-args constructor</li>
* <li>must be a public or protected class</li>
* <li>must not be final</li>
* <li>must not extend an enhanced class</li>
* <li>all persistent data represented by accessible setter/getter
* methods (persistent properties)</li>
* <li>if versioning is to be used, exactly one persistent property for
* the numeric version data</li> <!-- ##### is this true? -->
*
* <li>When using property access, the backing field for a persistent
* property must be:
* <ul>
* <!-- ##### JPA validation of these needs to be tested -->
* <li>private</li>
* <li>set only in the designated setter,
* in the constructor, or in {@link Object#clone()},
* <code>readObject(ObjectInputStream)</code>, or
* {@link Externalizable#readExternal(ObjectInput)}.</li>
* <li>read only in the designated getter and the
* constructor.</li>
* </ul>
* </li>
* </ul>
*
* <p>If you use this technique and use the <code>new</code> keyword instead
* of a OpenJPA-supplied construction routine, OpenJPA will need to do extra
* work with persistent-new-flushed instances, since OpenJPA cannot in this
* case track what happens to such an instance.</p>
*
* @since 1.0.0
*/
public class PCSubclassValidator {
private static final Localizer loc =
Localizer.forPackage(PCSubclassValidator.class);
private final ClassMetaData meta;
private final BCClass pc;
private final Log log;
private final boolean failOnContractViolations;
private Collection errors;
private Collection contractViolations;
public PCSubclassValidator(ClassMetaData meta, BCClass bc, Log log,
boolean enforceContractViolations) {
this.meta = meta;
this.pc = bc;
this.log = log;
this.failOnContractViolations = enforceContractViolations;
}
public void assertCanSubclass() {
Class superclass = meta.getDescribedType();
String name = superclass.getName();
if (superclass.isInterface())
addError(loc.get("subclasser-no-ifaces", name), meta);
if (Modifier.isFinal(superclass.getModifiers()))
addError(loc.get("subclasser-no-final-classes", name), meta);
if (Modifier.isPrivate(superclass.getModifiers()))
addError(loc.get("subclasser-no-private-classes", name), meta);
if (PersistenceCapable.class.isAssignableFrom(superclass))
addError(loc.get("subclasser-super-already-pc", name), meta);
try {
Constructor c = superclass.getDeclaredConstructor(new Class[0]);
if (!(Modifier.isProtected(c.getModifiers())
|| Modifier.isPublic(c.getModifiers())))
addError(loc.get("subclasser-private-ctor", name), meta);
}
catch (NoSuchMethodException e) {
addError(loc.get("subclasser-no-void-ctor", name),
meta);
}
// if the BCClass we loaded is already pc and the superclass is not,
// then we should never get here, so let's make sure that the
// calling context is caching correctly by throwing an exception.
if (pc.isInstanceOf(PersistenceCapable.class) &&
!PersistenceCapable.class.isAssignableFrom(superclass))
throw new InternalException(
loc.get("subclasser-class-already-pc", name));
if (AccessCode.isProperty(meta.getAccessType()))
checkPropertiesAreInterceptable();
if (errors != null && !errors.isEmpty())
throw new UserException(errors.toString());
else if (contractViolations != null &&
!contractViolations.isEmpty() && log.isWarnEnabled())
log.warn(contractViolations.toString());
}
private void checkPropertiesAreInterceptable() {
// just considers accessor methods for now.
FieldMetaData[] fmds = meta.getFields();
for (FieldMetaData fmd : fmds) {
Method getter = getBackingMember(fmd);
if (getter == null) {
addError(loc.get("subclasser-no-getter",
fmd.getName()), fmd);
continue;
}
BCField returnedField = checkGetterIsSubclassable(getter, fmd);
Method setter = setterForField(fmd);
if (setter == null) {
addError(loc.get("subclasser-no-setter", fmd.getName()),
fmd);
continue;
}
BCField assignedField = checkSetterIsSubclassable(setter, fmd);
if (assignedField == null)
continue;
if (assignedField != returnedField)
addContractViolation(loc.get
("subclasser-setter-getter-field-mismatch",
fmd.getName(), returnedField, assignedField),
fmd);
// ### scan through all the rest of the class to make sure it
// ### doesn't use the field.
}
}
private Method getBackingMember(FieldMetaData fmd) {
Member back = fmd.getBackingMember();
if (Method.class.isInstance(back))
return (Method)back;
Method getter = Reflection.findGetter(meta.getDescribedType(),
fmd.getName(), false);
if (getter != null)
fmd.backingMember(getter);
return getter;
}
private Method setterForField(FieldMetaData fmd) {
try {
return fmd.getDeclaringType().getDeclaredMethod(
"set" + StringUtil.capitalize(fmd.getName()),
new Class[]{ fmd.getDeclaredType() });
}
catch (NoSuchMethodException e) {
return null;
}
}
/**
* @return the name of the field that is returned by <code>meth</code>, or
* <code>null</code> if something other than a single field is
* returned, or if it cannot be determined what is returned.
*/
private BCField checkGetterIsSubclassable(Method meth, FieldMetaData fmd) {
checkMethodIsSubclassable(meth, fmd);
BCField field = PCEnhancer.getReturnedField(getBCMethod(meth));
if (field == null) {
addContractViolation(loc.get("subclasser-invalid-getter",
fmd.getName()), fmd);
return null;
} else {
return field;
}
}
/**
* @return the field that is set in <code>meth</code>, or
* <code>null</code> if something other than a single field is
* set, or if it cannot be determined what is set.
*/
private BCField checkSetterIsSubclassable(Method meth, FieldMetaData fmd) {
checkMethodIsSubclassable(meth, fmd);
BCField field = PCEnhancer.getAssignedField(getBCMethod(meth));
if (field == null) {
addContractViolation(loc.get("subclasser-invalid-setter",
fmd.getName()), fmd);
return null;
} else {
return field;
}
}
private BCMethod getBCMethod(Method meth) {
BCClass bc = pc.getProject().loadClass(meth.getDeclaringClass());
return bc.getDeclaredMethod(meth.getName(), meth.getParameterTypes());
}
private void checkMethodIsSubclassable(Method meth, FieldMetaData fmd) {
String className = fmd.getDefiningMetaData().
getDescribedType().getName();
if (!(Modifier.isProtected(meth.getModifiers())
|| Modifier.isPublic(meth.getModifiers())))
addError(loc.get("subclasser-private-accessors-unsupported",
className, meth.getName()), fmd);
if (Modifier.isFinal(meth.getModifiers()))
addError(loc.get("subclasser-final-methods-not-allowed",
className, meth.getName()), fmd);
if (Modifier.isNative(meth.getModifiers()))
addContractViolation(loc.get
("subclasser-native-methods-not-allowed", className,
meth.getName()),
fmd);
if (Modifier.isStatic(meth.getModifiers()))
addError(loc.get("subclasser-static-methods-not-supported",
className, meth.getName()), fmd);
}
private void addError(Message s, ClassMetaData cls) {
if (errors == null)
errors = new ArrayList();
errors.add(loc.get("subclasser-error-meta", s,
cls.getDescribedType().getName(),
cls.getSourceFile()));
}
private void addError(Message s, FieldMetaData fmd) {
if (errors == null)
errors = new ArrayList();
errors.add(loc.get("subclasser-error-field", s,
fmd.getFullName(),
fmd.getDeclaringMetaData().getSourceFile()));
}
private void addContractViolation(Message m, FieldMetaData fmd) {
// add the violation as an error in case we're processing violations
// as errors; this keeps them in the order that they were found rather
// than just adding the violations to the end of the list.
if (failOnContractViolations)
addError(m, fmd);
if (contractViolations == null)
contractViolations = new ArrayList();
contractViolations.add(loc.get
("subclasser-contract-violation-field", m.getMessage(),
fmd.getFullName(), fmd.getDeclaringMetaData().getSourceFile()));
}
}