| /******************************************************************************* |
| * Copyright (c) 2009, 2010 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.Serializable; |
| 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.core.resources.IFile; |
| import org.eclipse.jdt.core.ICompilationUnit; |
| import org.eclipse.jdt.core.IPackageFragment; |
| import org.eclipse.jdt.core.IPackageFragmentRoot; |
| import org.eclipse.jdt.core.JavaModelException; |
| import org.eclipse.jpt.core.JptCorePlugin; |
| 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.JpaProject2_0; |
| import org.eclipse.jpt.core.jpa2.context.AttributeMapping2_0; |
| import org.eclipse.jpt.core.jpa2.context.MetamodelField; |
| import org.eclipse.jpt.core.jpa2.context.PersistentType2_0; |
| import org.eclipse.jpt.core.jpa2.resource.java.JPA2_0; |
| import org.eclipse.jpt.core.jpa2.resource.java.JavaResourcePersistentType2_0; |
| import org.eclipse.jpt.utility.Filter; |
| import org.eclipse.jpt.utility.internal.ClassName; |
| 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; |
| import com.ibm.icu.text.SimpleDateFormat; |
| |
| /** |
| * For now, the "synchronization" is simple brute-force: we generate the source |
| * code and then compare it with what is already present in the file. |
| * If the new source is different, we replace the file contents; otherwise, we |
| * leave the file unchanged. |
| */ |
| @SuppressWarnings("nls") |
| public class GenericPersistentTypeMetamodelSynchronizer |
| implements PersistentType2_0.MetamodelSynchronizer |
| { |
| protected final PersistentType2_0 persistentType; |
| |
| |
| public GenericPersistentTypeMetamodelSynchronizer(PersistentType2_0 persistentType) { |
| super(); |
| this.persistentType = persistentType; |
| } |
| |
| public IFile getFile() { |
| return (IFile) this.getPackageFragment().getCompilationUnit(this.getFileName()).getResource(); |
| } |
| |
| |
| // ********** synchronize ********** |
| |
| public void synchronize() { |
| try { |
| this.synchronize_(); |
| } catch (JavaModelException ex) { |
| JptCorePlugin.log(ex); |
| } |
| } |
| |
| protected void synchronize_() throws JavaModelException { |
| IPackageFragment pkg = this.getPackageFragment(); |
| String fileName = this.getFileName(); |
| |
| ICompilationUnit compilationUnit = pkg.getCompilationUnit(fileName); |
| if (compilationUnit.exists()) { |
| // overwrite existing file if it has changed (ignoring the timestamp) |
| String newSource = this.buildSource(compilationUnit); |
| if (newSource != null) { |
| pkg.createCompilationUnit(fileName, newSource, true, null); // true=force |
| } |
| } else { |
| // write a new file, creating the package folders if necessary |
| if ( ! pkg.exists()) { |
| this.getSourceFolder().createPackageFragment(pkg.getElementName(), true, null); // true=force |
| } |
| pkg.createCompilationUnit(fileName, this.buildSource(), false, null); // false=no force |
| } |
| } |
| |
| /** |
| * pre-condition: the compilation unit exists |
| * |
| * return null if the old source is not to be replaced |
| */ |
| protected String buildSource(ICompilationUnit compilationUnit) throws JavaModelException { |
| IFile file = (IFile) compilationUnit.getResource(); |
| JavaResourcePersistentType2_0 genType = this.getJpaProject().getGeneratedMetamodelType(file); |
| if (genType == null) { |
| return null; // the file exists, but its source is not a generated metamodel class |
| } |
| |
| String oldSource = compilationUnit.getSource(); |
| int oldLength = oldSource.length(); |
| |
| String newSource = this.buildSource(); |
| int newLength = newSource.length(); |
| if (newLength != oldLength) { |
| return newSource; |
| } |
| |
| String date = genType.getGeneratedAnnotation().getDate(); // if we get here, this will be non-empty |
| int dateBegin = oldSource.indexOf(date); |
| if (dateBegin == -1) { |
| return null; // hmmm... |
| } |
| int dateEnd = dateBegin + date.length(); |
| if (dateEnd > oldLength) { |
| return null; // hmmm... |
| } |
| |
| if (newSource.regionMatches(0, oldSource, 0, dateBegin) && |
| newSource.regionMatches(dateEnd, oldSource, dateEnd, oldLength - dateEnd)) { |
| return null; |
| } |
| return newSource; |
| } |
| |
| |
| // ********** package/file ********** |
| |
| protected IPackageFragment getPackageFragment() { |
| return this.getSourceFolder().getPackageFragment(this.getPackageName()); |
| } |
| |
| protected IPackageFragmentRoot getSourceFolder() { |
| return this.getJpaProject().getMetamodelPackageFragmentRoot(); |
| } |
| |
| protected JpaProject2_0 getJpaProject() { |
| return (JpaProject2_0) this.persistentType.getJpaProject(); |
| } |
| |
| // TODO |
| protected String getPackageName() { |
| // the default is to store the metamodel in the same package as the model |
| return ClassName.getPackageName(this.getMetamodelClassName()); |
| } |
| |
| protected String getFileName() { |
| return ClassName.getSimpleName(this.getMetamodelClassName()) + ".java"; |
| } |
| |
| protected String getMetamodelClassName() { |
| return this.buildMetamodelClassName(this.persistentType.getName()); |
| } |
| |
| // TODO |
| protected String buildMetamodelClassName(String className) { |
| // the default is to simply append an underscore to the model class name |
| return className + '_'; |
| } |
| |
| |
| // ********** source code ********** |
| |
| /** |
| * 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.getMetamodelClassName()); |
| 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.getMetamodelClassName()); |
| 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(); |
| } |
| |
| protected void printGeneratedAnnotationOn(BodySourceWriter pw) { |
| pw.printAnnotation("javax.annotation.Generated"); |
| pw.print('('); |
| pw.print("value="); |
| pw.printStringLiteral(JavaResourcePersistentType2_0.METAMODEL_GENERATED_ANNOTATION_VALUE); |
| pw.print(", "); |
| pw.print("date="); |
| pw.printStringLiteral(format(new Date())); |
| pw.print(')'); |
| pw.println(); |
| } |
| |
| /** |
| * {@link SimpleDateFormat} is not thread-safe. |
| */ |
| protected static synchronized String format(Date date) { |
| return DATE_FORMAT.format(date); |
| } |
| /** |
| * Recommended date format is ISO 8601. |
| * @see javax.annotation.Generated |
| */ |
| private static final DateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSZ"); |
| |
| |
| // ********** 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>>, Serializable |
| { |
| 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>>(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; |
| } |
| } |
| |
| } |
| |
| } |