blob: 0a15a514e3218356d50ae9260b213331290ad737 [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2009 Oracle. All rights reserved.
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v1.0, which accompanies this distribution
* and is available at http://www.eclipse.org/legal/epl-v10.html.
*
* Contributors:
* Oracle - initial API and implementation
******************************************************************************/
package org.eclipse.jpt.core.internal.jpa2;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.Comparator;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.TreeSet;
import java.util.Map.Entry;
import org.eclipse.jdt.core.IPackageFragment;
import org.eclipse.jdt.core.IPackageFragmentRoot;
import org.eclipse.jdt.core.ISourceReference;
import org.eclipse.jdt.core.JavaModelException;
import org.eclipse.jpt.core.context.AttributeMapping;
import org.eclipse.jpt.core.context.PersistentAttribute;
import org.eclipse.jpt.core.context.PersistentType;
import org.eclipse.jpt.core.jpa2.PersistentTypeMetamodelSynchronizer;
import org.eclipse.jpt.core.jpa2.context.AttributeMapping2_0;
import org.eclipse.jpt.core.jpa2.context.MetamodelField;
import org.eclipse.jpt.core.jpa2.resource.java.JPA2_0;
import org.eclipse.jpt.utility.Filter;
import org.eclipse.jpt.utility.internal.ClassTools;
import org.eclipse.jpt.utility.internal.CollectionTools;
import org.eclipse.jpt.utility.internal.IndentingPrintWriter;
import org.eclipse.jpt.utility.internal.StringTools;
import org.eclipse.jpt.utility.internal.Transformer;
import org.eclipse.jpt.utility.internal.iterables.FilteringIterable;
import org.eclipse.jpt.utility.internal.iterables.TransformationIterable;
import com.ibm.icu.text.Collator;
import com.ibm.icu.text.DateFormat;
/**
*
*/
@SuppressWarnings("nls")
public class GenericPersistentTypeMetamodelSynchronizer
implements PersistentTypeMetamodelSynchronizer
{
protected final Owner owner;
protected final PersistentType persistentType;
protected final String metamodelClassName;
public GenericPersistentTypeMetamodelSynchronizer(Owner owner, PersistentType persistentType) {
super();
this.owner = owner;
this.persistentType = persistentType;
this.metamodelClassName = this.buildMetamodelClassName();
}
// TODO
protected String buildMetamodelClassName() {
return this.buildMetamodelClassName(this.persistentType.getName());
}
protected String buildMetamodelClassName(String className) {
return className + '_';
}
// TODO
protected String getPackageName() {
return ClassTools.packageNameForClassNamed(this.metamodelClassName);
}
protected IPackageFragment buildPackageFragment() {
IPackageFragmentRoot sourceFolder = this.owner.getSourceFolder();
String pkgName = this.getPackageName();
IPackageFragment packageFragment = sourceFolder.getPackageFragment(pkgName);
if (packageFragment.exists()) {
return packageFragment;
}
try {
return sourceFolder.createPackageFragment(pkgName, true, null);
} catch (JavaModelException ex) {
throw new RuntimeException(ex);
}
}
public void synchronize() {
try {
this.synchronize_();
} catch (JavaModelException ex) {
throw new RuntimeException(ex);
}
}
protected void synchronize_() throws JavaModelException {
IPackageFragment pkg = this.buildPackageFragment();
String fileName = ClassTools.shortNameForClassNamed(this.metamodelClassName) + ".java";
ISourceReference oldSourceRef = pkg.getCompilationUnit(fileName);
String oldSource = oldSourceRef.exists() ? oldSourceRef.getSource() : null;
if (oldSource == null) { // write a new file
pkg.createCompilationUnit(fileName, this.buildSource(), false, null); // false=no force
return;
}
int oldBegin = oldSource.indexOf(DALI_TAG);
if (oldBegin == -1) {
return;
}
oldBegin += DALI_TAG_LENGTH;
int oldEnd = oldSource.indexOf('"', oldBegin);
if (oldEnd == -1) {
return;
}
String oldSource2 = oldSource.replace(oldSource.substring(oldBegin, oldEnd), "");
String newSource = this.buildSource();
int newBegin = newSource.indexOf(DALI_TAG) + DALI_TAG_LENGTH;
int newEnd = newSource.indexOf('"', newBegin);
String newSource2 = newSource.replace(newSource.substring(newBegin, newEnd), "");
if ( ! newSource2.equals(oldSource2)) { // replace the old file
pkg.createCompilationUnit(fileName, newSource, true, null);
}
}
protected static final String DALI_TAG = "@Generated(\"Dali";
protected static final int DALI_TAG_LENGTH = DALI_TAG.length();
/**
* build the "body" source first; then build the "package" and "imports" source
* and concatenate the "body" source to it
*/
protected String buildSource() {
// build the body source first so we can gather up the import statements
BodySourceWriter bodySourceWriter = this.buildBodySourceWriter();
StringWriter sw = new StringWriter(bodySourceWriter.getLength() + 2000);
PrintWriter pw = new PrintWriter(sw);
this.printPackageAndImportsOn(pw, bodySourceWriter);
pw.print(bodySourceWriter.getSource());
return sw.toString();
}
protected BodySourceWriter buildBodySourceWriter() {
BodySourceWriter pw = new BodySourceWriter(this.getPackageName(), this.metamodelClassName);
this.printBodySourceOn(pw);
return pw;
}
protected void printBodySourceOn(BodySourceWriter pw) {
this.printClassDeclarationOn(pw);
pw.print(" {");
pw.println();
pw.indent();
this.printAttributesOn(pw);
pw.undent();
pw.print('}');
pw.println(); // EOF
}
// ********** class declaration **********
protected void printClassDeclarationOn(BodySourceWriter pw) {
this.printGeneratedAnnotationOn(pw);
this.printStaticMetamodelAnnotationOn(pw);
pw.print("public class ");
pw.printTypeDeclaration(this.metamodelClassName);
PersistentType superPersistentType = this.persistentType.getSuperPersistentType();
if (superPersistentType != null) {
pw.print(" extends ");
pw.printTypeDeclaration(this.buildMetamodelClassName(superPersistentType.getName()));
}
}
protected void printStaticMetamodelAnnotationOn(BodySourceWriter pw) {
pw.printAnnotation(JPA2_0.STATIC_METAMODEL);
pw.print('(');
pw.printTypeDeclaration(this.persistentType.getName());
pw.print(".class");
pw.print(')');
pw.println();
}
// TODO - comment?
protected void printGeneratedAnnotationOn(BodySourceWriter pw) {
pw.printAnnotation("javax.annotation.Generated");
pw.print('(');
pw.printStringLiteral("Dali" + ' ' + DateFormat.getDateTimeInstance().format(new Date()));
pw.print(')');
pw.println();
}
// ********** attributes **********
protected void printAttributesOn(BodySourceWriter pw) {
for (Iterator<PersistentAttribute> stream = this.persistentType.attributes(); stream.hasNext(); ) {
this.printAttributeOn(stream.next(), pw);
}
}
protected void printAttributeOn(PersistentAttribute persistentAttribute, BodySourceWriter pw) {
AttributeMapping attributeMapping = persistentAttribute.getMapping();
if (attributeMapping != null) { // probably shouldn't be null?
this.printAttributeMappingOn(attributeMapping, pw);
}
}
protected void printAttributeMappingOn(AttributeMapping attributeMapping, BodySourceWriter pw) {
MetamodelField field = ((AttributeMapping2_0) attributeMapping).getMetamodelField();
if (field != null) {
this.printFieldOn(field, pw);
}
}
protected void printFieldOn(MetamodelField field, BodySourceWriter pw) {
for (String modifier : field.getModifiers()) {
pw.print(modifier);
pw.print(' ');
}
pw.printTypeDeclaration(field.getTypeName());
pw.print('<');
for (Iterator<String> stream = field.getTypeArgumentNames().iterator(); stream.hasNext(); ) {
pw.printTypeDeclaration(stream.next());
if (stream.hasNext()) {
pw.print(", ");
}
}
pw.print('>');
pw.print(' ');
pw.print(field.getName());
pw.print(';');
pw.println();
}
// ********** package and imports **********
protected void printPackageAndImportsOn(PrintWriter pw, BodySourceWriter bodySourceWriter) {
if (this.getPackageName().length() != 0) {
pw.print("package ");
pw.print(this.getPackageName());
pw.print(';');
pw.println();
pw.println();
}
for (String import_ : bodySourceWriter.getImports()) {
pw.print("import ");
pw.print(import_);
pw.print(';');
pw.println();
}
pw.println();
}
// ********** source writer **********
/**
* Extend IndentingPrintWriter with some methods that facilitate building
* class source code.
*/
protected static class BodySourceWriter
extends IndentingPrintWriter
{
protected final String packageName;
protected final String className;
// key = short class name; value = import package
protected final HashMap<String, ImportPackage> imports = new HashMap<String, ImportPackage>();
protected BodySourceWriter(String packageName, String className) {
super(new StringWriter(2000));
this.packageName = packageName;
this.className = className;
}
protected String getSource() {
return this.out.toString();
}
protected int getLength() {
return ((StringWriter) this.out).getBuffer().length();
}
protected void printVisibility(String visibilityModifier) {
if (visibilityModifier.length() != 0) {
this.print(visibilityModifier);
this.print(' ');
}
}
protected void printAnnotation(String annotationName) {
this.print('@');
this.printTypeDeclaration(annotationName);
}
protected void printTypeDeclaration(String typeDeclaration) {
this.print(this.buildImportedTypeDeclaration(typeDeclaration));
}
protected void printField(String fieldName, String typeDeclaration, String visibility) {
this.printVisibility(visibility);
this.printTypeDeclaration(typeDeclaration);
this.print(' ');
this.print(fieldName);
this.print(';');
this.println();
this.println();
}
protected void printParameterizedField(String fieldName, String typeDeclaration, String parameterTypeDeclaration, String visibility) {
this.printVisibility(visibility);
this.printTypeDeclaration(typeDeclaration);
this.print('<');
this.printTypeDeclaration(parameterTypeDeclaration);
this.print('>');
this.print(' ');
this.print(fieldName);
this.print(';');
this.println();
this.println();
}
/**
* Convert the specified string to a String Literal and print it,
* adding the surrounding double-quotes and escaping characters
* as necessary.
*/
void printStringLiteral(String string) {
StringTools.convertToJavaStringLiteralOn(string, this);
}
// ********** imports **********
// ***** writing
/**
* Return the specified class's "imported" name.
* The class declaration must be of the form:
* "int"
* "int[]" (not "[I")
* "java.lang.Object"
* "java.lang.Object[]" (not "[Ljava.lang.Object;")
* "java.util.Map.Entry" (not "java.util.Map$Entry")
* "java.util.Map.Entry[][]" (not "[[Ljava.util.Map$Entry;")
*
* To really do this right, we would need to gather all the types from
* the "unamed" (default) package that were referenced in the
* compilation unit beforehand. *Any* collisions with one of these
* types would have to be fully qualified (whether it was from
* 'java.lang' or the same package as the current compilation unit).
* In other words, if we have any types from the "unnamed" package,
* results are unpredictable....
*/
protected String buildImportedTypeDeclaration(String typeDeclaration) {
if (this.typeDeclarationIsMemberClass(typeDeclaration)) {
// no need for an import, just return the partially-qualified name
return this.buildMemberClassTypeDeclaration(typeDeclaration);
}
int last = typeDeclaration.lastIndexOf('.');
String currentPackageName = (last == -1) ? "" : typeDeclaration.substring(0, last);
String shortTypeDeclaration = typeDeclaration.substring(last + 1);
String shortElementTypeName = shortTypeDeclaration;
while (shortElementTypeName.endsWith("[]")) {
shortElementTypeName = shortElementTypeName.substring(0, shortElementTypeName.length() - 2);
}
ImportPackage prev = this.imports.get(shortElementTypeName);
if (prev == null) {
// this is the first class with this short element type name
this.imports.put(shortElementTypeName, new ImportPackage(currentPackageName));
return shortTypeDeclaration;
}
if (prev.packageName.equals(currentPackageName)) {
// this element type has already been imported
return shortTypeDeclaration;
}
if (currentPackageName.equals(this.packageName) &&
prev.packageName.equals("java.lang")) {
// we force the 'java.lang' class to be explicitly imported
prev.collision = true;
}
// another class with the same short element type name has been
// previously imported, so this one must be used fully-qualified
return typeDeclaration;
}
/**
* e.g. "foo.bar.Employee.PK" will return true
*/
protected boolean typeDeclarationIsMemberClass(String typeDeclaration) {
return (typeDeclaration.length() > this.className.length())
&& typeDeclaration.startsWith(this.className)
&& (typeDeclaration.charAt(this.className.length()) == '.');
}
/**
* e.g. "foo.bar.Employee.PK" will return "Employee.PK"
* this prevents collisions with other imported classes (e.g. "joo.jar.PK")
*/
protected String buildMemberClassTypeDeclaration(String typeDeclaration) {
int index = this.packageName.length();
if (index != 0) {
index++; // bump past the '.'
}
return typeDeclaration.substring(index);
}
// ***** reading
protected Iterable<String> getImports() {
return this.getSortedRequiredImports();
}
/**
* transform our map entries to class names
*/
protected Iterable<String> getSortedRequiredImports() {
return new TransformationIterable<Map.Entry<String, ImportPackage>, String>(this.getSortedRequiredImportEntries(), this.buildImportEntriesTransformer());
}
protected Transformer<Map.Entry<String, ImportPackage>, String> buildImportEntriesTransformer() {
return IMPORT_ENTRIES_TRANSFORMER;
}
protected static final Transformer<Map.Entry<String, ImportPackage>, String> IMPORT_ENTRIES_TRANSFORMER = new ImportEntriesTransformer();
protected static class ImportEntriesTransformer
implements Transformer<Map.Entry<String, ImportPackage>, String>
{
public String transform(Entry<String, ImportPackage> importEntry) {
String pkg = importEntry.getValue().packageName;
String type = importEntry.getKey();
StringBuilder sb = new StringBuilder(pkg.length() + 1 + type.length());
sb.append(pkg);
sb.append('.');
sb.append(type);
return sb.toString();
}
}
/**
* sort by package first, then class (*not* by fully-qualified class name)
*/
protected Iterable<Map.Entry<String, ImportPackage>> getSortedRequiredImportEntries() {
TreeSet<Map.Entry<String, ImportPackage>> sortedEntries = new TreeSet<Map.Entry<String, ImportPackage>>(this.buildImportEntriesComparator());
CollectionTools.addAll(sortedEntries, this.getRequiredImportEntries());
return sortedEntries;
}
protected Comparator<Map.Entry<String, ImportPackage>> buildImportEntriesComparator() {
return IMPORT_ENTRIES_COMPARATOR;
}
protected static final Comparator<Map.Entry<String, ImportPackage>> IMPORT_ENTRIES_COMPARATOR = new ImportEntriesComparator();
protected static class ImportEntriesComparator
implements Comparator<Map.Entry<String, ImportPackage>>
{
public int compare(Map.Entry<String, ImportPackage> e1, Map.Entry<String, ImportPackage> e2) {
Collator collator = Collator.getInstance();
int pkg = collator.compare(e1.getValue().packageName, e2.getValue().packageName);
return (pkg == 0) ? collator.compare(e1.getKey(), e2.getKey()) : pkg;
}
}
/**
* strip off any non-required imports (e.g. "java.lang.Object')
*/
protected Iterable<Map.Entry<String, ImportPackage>> getRequiredImportEntries() {
return new FilteringIterable<Map.Entry<String, ImportPackage>, Map.Entry<String, ImportPackage>>(this.imports.entrySet(), this.buildRequiredImportEntriesFilter());
}
protected Filter<Map.Entry<String, ImportPackage>> buildRequiredImportEntriesFilter() {
return new RequiredImportEntriesFilter();
}
protected class RequiredImportEntriesFilter
implements Filter<Map.Entry<String, ImportPackage>>
{
public boolean accept(Map.Entry<String, ImportPackage> importEntry) {
return this.packageMustBeImported(importEntry.getValue());
}
protected boolean packageMustBeImported(ImportPackage importPackage) {
String pkg = importPackage.packageName;
if (pkg.equals("")) {
// cannot import a type from the "unnamed" package
return false;
}
if (pkg.equals("java.lang")) {
// we must import from 'java.lang' if we also have a class in the same package
return importPackage.collision;
}
if (pkg.equals(BodySourceWriter.this.packageName)) {
// we never need to import a class from the same package
return false;
}
return true;
}
}
/**
* We need a 'collision' flag for when we encounter a class from
* 'java.lang' followed by a class from the current compilation unit's
* package. We will need to include the explicit import of the
* 'java.lang' class and all the references to the other class will
* have to be fully-qualified.
*
* If the classes are encountered in the opposite order (i.e. the class
* from the current compilation unit's package followed by the class
* from 'java.lang'), we do *not* need to import the first class while
* all the references to the 'java.lang' class will be fully-qualified.
*
* Unfortunately, we still have a problem: if we reference a class from
* 'java.lang' and there is a conflicting class from the current
* compilation unit's package (but that class is *not* revealed to us
* here), the simple name will be resolved to the non-'java.lang' class.
* Unless we simply force an import of *all* 'java.lang' classes.... :-(
*
* This shouldn't happen very often. :-)
*/
protected static class ImportPackage {
protected final String packageName;
protected boolean collision = false;
protected ImportPackage(String packageName) {
super();
this.packageName = packageName;
}
}
}
}