/*
* RHQ Management Platform
* Copyright (C) 2005-2010 Red Hat, Inc.
* All rights reserved.
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation version 2 of the License.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
*/
package org.rhq.helpers.perftest.support.jpa.mapping;
import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import javax.persistence.Column;
import javax.persistence.DiscriminatorValue;
import javax.persistence.Id;
import javax.persistence.IdClass;
import javax.persistence.JoinColumn;
import javax.persistence.JoinColumns;
import javax.persistence.JoinTable;
import javax.persistence.ManyToOne;
import javax.persistence.Table;
import org.rhq.helpers.perftest.support.jpa.Edge;
import org.rhq.helpers.perftest.support.jpa.JPAUtil;
import org.rhq.helpers.perftest.support.jpa.Node;
/**
* Translates the {@link Node} and its {@link Edge}s into table and column names.
*
* @author Lukas Krejci
*/
public class MappingTranslator {
private static class AnnotatedField {
private Field field;
private Annotation[] annotations;
public AnnotatedField(Field f) {
field = f;
annotations = new Annotation[f.getAnnotations().length];
System.arraycopy(f.getAnnotations(), 0, annotations, 0, annotations.length);
}
public void addAnnotations(Annotation[] annotations) {
Annotation[] thisAnnotations = this.annotations;
this.annotations = new Annotation[this.annotations.length + annotations.length];
System.arraycopy(thisAnnotations, 0, this.annotations, 0, thisAnnotations.length);
System.arraycopy(annotations, 0, this.annotations, thisAnnotations.length, annotations.length);
}
public Annotation[] getAnnotations() {
return annotations;
}
public <T extends Annotation> T getAnnotation(Class<T> type) {
for (int i = 0; i < annotations.length; ++i) {
if (annotations[i].annotationType().equals(type)) {
return type.cast(annotations[i]);
}
}
return null;
}
public String getName() {
return field.getName();
}
}
public EntityTranslation translate(Node node) {
EntityTranslation translation = new EntityTranslation();
translation.setTableName(getTableName(node));
translation.setPkColumns(getPkColumns(node));
return translation;
}
public RelationshipTranslation translate(Edge edge) {
switch (edge.getDependencyType()) {
case ONE_TO_ONE:
return analyzeOneToOne(edge);
case ONE_TO_MANY:
return analyzeOneToMany(edge);
case MANY_TO_MANY:
return analyzeManyToMany(edge);
default:
return null;
}
}
private static String getTableName(Node node) {
return getTableName(node.getEntity());
}
private static String[] getPkColumns(Node node) {
Set<AnnotatedField> pkFields = getIdFields(node.getEntity());
if (pkFields.isEmpty()) {
return null;
}
String[] columns = new String[pkFields.size()];
int idx = 0;
for (AnnotatedField f : pkFields) {
columns[idx] = getColumnName(f);
if (columns[idx] == null) {
//check for the special case, @Id with @JoinColumn and @ManyToOne
ManyToOne manyToOne = f.getAnnotation(ManyToOne.class);
if (manyToOne == null) {
columns[idx] = f.getName().toUpperCase();
} else {
JoinColumn joinColumn = f.getAnnotation(JoinColumn.class);
columns[idx] = joinColumn.name();
if (columns[idx].isEmpty()) {
columns[idx] = f.getName().toUpperCase();
}
}
}
idx++;
}
return columns;
}
private static String getColumnName(Field field) {
if (field == null) {
return null;
}
Column colSpec = field.getAnnotation(Column.class);
if (colSpec != null && !colSpec.name().isEmpty()) {
return colSpec.name();
} else {
return null;
}
}
private static List<JoinColumn> getJoinColumns(Field field) {
if (field == null) {
return null;
}
JoinColumn colSpec = field.getAnnotation(JoinColumn.class);
if (colSpec != null) {
//a single join column specified
return Collections.singletonList(colSpec);
} else {
//see, if there are more join cols
JoinColumns joinColumns = field.getAnnotation(JoinColumns.class);
if (joinColumns != null) {
JoinColumn[] cols = joinColumns.value();
List<JoinColumn> ret = new ArrayList<JoinColumn>();
for(int i = 0; i < cols.length; ++i) {
ret.add(cols[i]);
}
return ret;
}
}
return null;
}
private static String[] referencedJoinColumnNames(Field field) {
if (field == null) {
return null;
}
JoinColumn colSpec = field.getAnnotation(JoinColumn.class);
if (colSpec != null) {
//a single join column specified
return new String[] { colSpec.referencedColumnName().toUpperCase() };
} else {
//see, if there are more join cols
JoinColumns joinColumns = field.getAnnotation(JoinColumns.class);
if (joinColumns != null) {
JoinColumn[] cols = joinColumns.value();
String[] ret = new String[cols.length];
for(int i = 0; i < cols.length; ++i) {
ret[i] = cols[i].referencedColumnName().toUpperCase();
}
return ret;
}
}
return null;
}
private static String getColumnName(AnnotatedField field) {
if (field == null) {
return null;
}
Column colSpec = field.getAnnotation(Column.class);
if (colSpec != null && !colSpec.name().isEmpty()) {
return colSpec.name().toUpperCase();
} else {
return null;
}
}
public static String getTableName(Class<?> entity) {
Table tableAnnotation = entity.getAnnotation(Table.class);
if (tableAnnotation != null) {
String name = tableAnnotation.name();
if (name.isEmpty()) {
name = entity.getSimpleName();
}
return name.toUpperCase();
} else {
DiscriminatorValue discriminatorValueAnnotation = entity.getAnnotation(DiscriminatorValue.class);
if (discriminatorValueAnnotation != null) {
return getTableName(entity.getSuperclass());
}
}
return null;
}
public static Set<AnnotatedField> getIdFields(Class<?> entity) {
//we have 3 ways of defining ids of an entity
//1) single @Id
//2) @IdClass and multiple @Id fields
//3) these rules applied recursively in @Embedded fields
//3 is handled implicitly by JPAUtil
Set<Field> idFields = JPAUtil.getJPAFields(entity, Id.class);
if (idFields.size() == 0) {
return null;
} else if (idFields.size() == 1) {
return Collections.singleton(new AnnotatedField(idFields.iterator().next()));
} else {
//@IdClass
Class<?> idClass = entity.getAnnotation(IdClass.class).value();
Set<AnnotatedField> ret = new HashSet<AnnotatedField>();
for (Field f : idFields) {
AnnotatedField af = new AnnotatedField(f);
try {
Field idF = idClass.getDeclaredField(f.getName());
af.addAnnotations(idF.getAnnotations());
} catch (Exception e) {
}
ret.add(af);
}
return ret;
}
}
private static RelationshipTranslation analyzeOneToOne(Edge relationship) {
Field fromField = relationship.getFromField();
Field toField = relationship.getToField();
RelationshipTranslation translation = new RelationshipTranslation();
List<JoinColumn> joins = getJoinColumns(fromField);
String[] fCols = new String[joins.size()];
String[] tCols = new String[joins.size()];
int i = 0;
for(JoinColumn c : joins) {
String fkey = c.name().toUpperCase();
String refCol = c.referencedColumnName().toUpperCase();
//determine whether we have the foreign key in the from or to table
if (toField == null) {
//unidirectional mapping, the fkey is in the from table
fCols[i] = fkey;
if (!refCol.isEmpty()) {
tCols[i] = refCol;
}
} else if (refCol.isEmpty()) {
//bidirectional with no referenced column definition.
//the fkey is in the from table, referencing the pk in the target table
fCols[i] = fkey;
} else {
//bidirectional with referenced column definition.
//the fkey is in the from table, referencing the refCol
fCols[i] = fkey;
tCols[i] = refCol;
}
++i;
}
//now fill in the empty fCols and tCols with the corresponding primary keys of the to table
String[] fromPks = relationship.getFrom().getTranslation().getPkColumns();
for(i = 0; i < fCols.length; ++i) {
if (fCols[i] == null) {
fCols[i] = fromPks[i];
}
}
String[] toPks = relationship.getTo().getTranslation().getPkColumns();
for(i = 0; i < tCols.length; ++i) {
if (tCols[i] == null) {
tCols[i] = toPks[i];
}
}
translation.setFromColumns(fCols);
translation.setToColumns(tCols);
return translation;
}
private static RelationshipTranslation analyzeOneToMany(Edge relationship) {
Field fromField = relationship.getFromField();
Field toField = relationship.getToField();
RelationshipTranslation translation = new RelationshipTranslation();
if (toField == null) {
JoinTable t = fromField.getAnnotation(JoinTable.class);
JoinColumn c = fromField.getAnnotation(JoinColumn.class);
if (t != null) {
analyzeJoinTable(translation, t);
} else if (c != null) {
//join column represents the column in the target table.
String fromColumn = getColumnName(fromField);
if (fromColumn == null) {
translation.setFromColumns(relationship.getFrom().getTranslation().getPkColumns());
} else {
translation.setFromColumns(new String[] { fromColumn });
}
translation.setToColumns(joinColumnNames(new JoinColumn[] { c }));
} else {
throw new IllegalArgumentException("Default mappings on @OneToMany not implemented.");
}
} else {
String fromColumn = getColumnName(fromField);
if (fromColumn == null) {
translation.setFromColumns(relationship.getFrom().getTranslation().getPkColumns());
} else {
translation.setFromColumns(new String[] { fromColumn });
}
String toColumn = getColumnName(toField);
if (toColumn == null) {
AnnotatedField fullField = getFieldWithFullAnnotations(toField);
JoinColumn joinColumn = fullField.getAnnotation(JoinColumn.class);
if (joinColumn == null) {
translation.setToColumns(relationship.getTo().getTranslation().getPkColumns());
} else {
translation.setToColumns(joinColumnNames(new JoinColumn[] { joinColumn }));
}
} else {
translation.setToColumns(new String[] { toColumn });
}
}
return translation;
}
private static RelationshipTranslation analyzeManyToMany(Edge relationship) {
Field fromField = relationship.getFromField();
Field toField = relationship.getToField();
JoinTable t = fromField.getAnnotation(JoinTable.class);
if (t == null) {
t = toField.getAnnotation(JoinTable.class);
}
if (t == null) {
throw new IllegalStateException("Default values for a @JoinTable are not supported.");
}
RelationshipTranslation translation = new RelationshipTranslation();
analyzeJoinTable(translation, t);
return translation;
}
private static void analyzeJoinTable(RelationshipTranslation translation, JoinTable joinTable) {
JoinColumn[] joinCols = joinTable.joinColumns();
JoinColumn[] inverseCols = joinTable.inverseJoinColumns();
String tableName = joinTable.name().toUpperCase();
translation.setRelationTable(tableName);
translation.setRelationTableFromColumns(joinColumnNames(joinCols));
translation.setRelationTableToColumns(joinColumnNames(inverseCols));
translation.setFromColumns(new String[joinCols.length]);
translation.setToColumns(new String[inverseCols.length]);
updateWithJoinColumnReferencedNames(translation.getFromColumns(), joinCols);
updateWithJoinColumnReferencedNames(translation.getToColumns(), inverseCols);
}
private static String[] joinColumnNames(JoinColumn[] columns) {
String[] ret = new String[columns.length];
for (int i = 0; i < columns.length; ++i) {
ret[i] = columns[i].name().toUpperCase();
}
return ret;
}
private static void updateWithJoinColumnReferencedNames(String[] names, JoinColumn[] columns) {
if (names.length != columns.length) {
return;
}
for (int i = 0; i < columns.length; ++i) {
if (!columns[i].referencedColumnName().isEmpty()) {
names[i] = columns[i].referencedColumnName().toUpperCase();
}
}
}
/**
* This method returns something else than just wrapped f only in case
* when f is a part of composite id defined by an {@link IdClass @IdClass}.
* In that case the returned annotated field contains annotations defined on
* both the field itself and the corresponding field from the id class.
*
* @param f
* @return
*/
private static AnnotatedField getFieldWithFullAnnotations(Field f) {
AnnotatedField ret = new AnnotatedField(f);
IdClass idClass = f.getDeclaringClass().getAnnotation(IdClass.class);
if (idClass != null) {
try {
Field correspondingIdField = idClass.value().getDeclaredField(f.getName());
ret.addAnnotations(correspondingIdField.getAnnotations());
} catch (Exception e) {
}
}
return ret;
}
}