Eclipse-PyUML/pyUml/src/pyUML/pythonTree/PythonTreeClass.java

1340 lines
48 KiB
Java
Executable File

package pyUML.pythonTree;
import java.io.File;
import java.io.IOException;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Hashtable;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.Vector;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.eclipse.core.resources.IFile;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IPath;
import org.eclipse.emf.common.util.BasicEList;
import org.eclipse.emf.common.util.EList;
import org.eclipse.emf.ecore.EObject;
import org.eclipse.gmf.runtime.common.core.util.StringUtil;
import org.eclipse.jface.dialogs.MessageDialog;
import org.eclipse.uml2.uml.Association;
import org.eclipse.uml2.uml.Class;
import org.eclipse.uml2.uml.Classifier;
import org.eclipse.uml2.uml.Element;
import org.eclipse.uml2.uml.Generalization;
import org.eclipse.uml2.uml.Interface;
import org.eclipse.uml2.uml.NamedElement;
import org.eclipse.uml2.uml.Operation;
import org.eclipse.uml2.uml.Property;
import org.eclipse.uml2.uml.Realization;
import org.eclipse.uml2.uml.Relationship;
import org.eclipse.uml2.uml.Type;
import org.eclipse.uml2.uml.VisibilityKind;
import org.python.pydev.parser.jython.ast.Assign;
import org.python.pydev.parser.jython.ast.Attribute;
import org.python.pydev.parser.jython.ast.Break;
import org.python.pydev.parser.jython.ast.ClassDef;
import org.python.pydev.parser.jython.ast.Expr;
import org.python.pydev.parser.jython.ast.FunctionDef;
import org.python.pydev.parser.jython.ast.Name;
import org.python.pydev.parser.jython.ast.NameTok;
import org.python.pydev.parser.jython.ast.Str;
import org.python.pydev.parser.jython.ast.exprType;
import org.python.pydev.parser.jython.ast.stmtType;
import pyUML.backend.EclipseHelperMethods;
import pyUML.backend.GlobalConstants;
import pyUML.backend.JavaHelperMethods;
import pyUML.backend.ParseHelpers;
import pyUML.exceptions.PyUMLCancelledException;
import pyUML.exceptions.PyUMLParseException;
import pyUML.exceptions.PyUMLSynchronizeCodeException;
import pyUML.refactoring.BicycleRefactoring;
import pyUML.refactoring.FileRefactoring;
/**
* Part of PythonTree representing a class
* - can write XMI ID to classes docstring
*
*/
public class PythonTreeClass extends PythonTreeNode {
private static final Pattern FROM_IMPORT_PATTERN = Pattern.compile("[\\s]*from[\\s]*([^\\s]*)[\\s]*import[\\s]*([^\\s#]*)([\\s#]?.*)");
ClassDef astNode;
String docString;
PythonTreeFile inFile;
int docStringLine;
int docStringCol;
int lastLine;
List<String> usedStereotypes;
Set<String> superClasses;
private String indent = " ";
Map<String, PythonTreeMethod> childMethodDict;
Map<String, PythonTreeAttribute> childStaticAttrDict;
Map<String, PythonTreeAttribute> childObjectAttrDict;
/**
* A map holding the full package name in the format mypackage/mysubpackage/MyClass
* indexed by the classname which might be the plain classname if it has been
* imported using a "from mypackage.mysubpackage import MyClass" statement
* or prefixed with the package / alias if imported with a simple
* "import mypackage.mysubpackage" or "import mypackage.mysubpackage as myalias".
*/
private Map<String, String> packageCache;
/**
* Constructor to create a PythonTreeClass of a python class AST
* @param parent the PythonTreePackage, which is parent of this class
* @param inFile the PythonTreeFile
* @param astNode the corresponding Python AST node
* @param lastLine the last line of this class definition; 0 if
* class ends with end of file
*/
public PythonTreeClass(PythonTreeNode parent, PythonTreeFile inFile, ClassDef astNode, int lastLine) throws PyUMLParseException, PyUMLCancelledException{
super(parent);
this.astNode = astNode;
this.inFile = inFile;
this.lastLine = lastLine;
this.superClasses = new HashSet<String>();
this.childMethodDict = new Hashtable<String, PythonTreeMethod>();
this.childStaticAttrDict = new Hashtable<String, PythonTreeAttribute>();
this.childObjectAttrDict = new Hashtable<String, PythonTreeAttribute>();
packageCache = new HashMap<String, String>();
this.initClassNode();
this.initSubStructures();
}
private void initClassNode() throws PyUMLParseException, PyUMLCancelledException{
this.name = ((NameTok)this.astNode.name).id;
this.docStringLine = this.astNode.beginLine;
this.docStringCol = this.astNode.beginColumn;
// get class Body
stmtType[] classBody = this.astNode.body;
if (classBody.length == 0) {
return;
}
// extract DocString, if it exists
if (classBody[0] instanceof Expr) {
Expr exp = (Expr) classBody[0];
if (exp.value instanceof Str) {
Str stringExp = (Str) exp.value;
if (stringExp.type == Str.TripleDouble) {
this.docString=stringExp.s;
this.docStringLine = stringExp.beginLine;
this.docStringCol = stringExp.beginColumn;
}
}
}
//extract XMI-ID from DocString
if (this.docString != null) {
this.xmi_id = ParseHelpers.extractXmiFromString(this.docString);
if (this.xmi_id != null)
this.getRoot().getXmiClassDict().put(this.xmi_id, this);
}
PythonTreeRoot root = this.getRoot();
// test if a class of this name already exists
String structuredName = this.getPackageStructure() + this.name;
this.getRoot().setSubTaskName(structuredName);
if (root.getClassDict().containsKey(structuredName) && root.isShowWarnings()) {
String message = "Duplicate class detected!\n" +
"More than one class with the name " + this.name
+ " was found \n"
+ "in the package "+this.getPackageStructure() +"."
+ "\nThis can lead to problems in PyUML.\n"
+ "Please rename one of the classes in the code!\n"
+ "Both classes will be ignored!";
this.getParent().getChildClasses().remove(this.getRoot().getClassDict().get(structuredName));
this.getRoot().getClassDict().remove(structuredName);
this.getRoot().getClassNameDict().remove(structuredName);
MessageDialog.openWarning(null, "Duplicate class detected", message);
return;
//throw new PyUMLParseException(message);
}
// initialize empty set of superclasses
root.getClassDict().put(structuredName, this);
root.getClassNameDict().put(structuredName, this.name);
for (int i = 0; i < this.astNode.bases.length; i++) {
exprType superClass = this.astNode.bases[i];
// check whether it's a plain string, which means a classname
// or separated by dots
if (superClass instanceof Name) {
String superName = ((Name)superClass).id;
String superClassPackString = this.getPackFromImports(superName);
if (superClassPackString != null)
this.superClasses.add(superClassPackString);
} else if (superClass instanceof Attribute) {
// Just extract the content in the paranthesis without
// any further analysis
// This can be a subclass or a class under a package
/// -> this cannot be recognized
Attribute superAtt = (Attribute) superClass;
String line = FileRefactoring.getLine(inFile, superAtt.beginLine);
// find i'th component of bases -> current component
String separator = "(";
for (int j=0; j <= i; j++) {
if (j > 0)
separator = ",";
line = line.substring(line.indexOf(separator) + 1);
}
if ( i < this.astNode.bases.length - 1)
separator = ",";
else
separator = ")";
String superClassFullName = line.substring(0, line.indexOf(separator));
superClassFullName = superClassFullName.replaceAll("[\\s]", "");
// currently we suppose that the class will be prefixed by
// either the fully qualified package name or an alias,
// nested classes are not supported yet!
String superClassPackString = this.getPackFromImports(superClassFullName);
if (superClassPackString != null) { // TODO: will a null value ever return?
this.superClasses.add(superClassPackString);
}
}
}
}
/**
* initializes all Substructures of this class
* - Methods (object or static)
* - Static attributes (in class body)
* - Object attributes (in __init(self))
*
* The PythonTree child objects are created and initialized
* in their constructor
*/
private void initSubStructures() throws PyUMLParseException{
// parse class body
stmtType[] statements = this.astNode.body;
for (int i=0; i<statements.length; i++) {
stmtType statement = statements[i];
// fix indent, if needed
String line = this.inFile.getSplittedFileContent()[statement.beginLine - 1];
int indentLength = 0;
if (line.length() > 0)
for (; line.charAt(indentLength) == ' ' && line.length() > indentLength +1; indentLength++) ;
if (this.indent.length() != indentLength && ! line.matches("^[\\s]*\\\"\\\"\\\".*$")) {
this.indent = "";
for (int j=0; j< indentLength; j++)
this.indent += " ";
}
// get functions and object attributes
int nextLine;
if (i < statements.length -1)
nextLine = statements[i+1].beginLine;
else
nextLine = (this.lastLine == -1) ? -1 : this.lastLine + 1;
if (statement instanceof FunctionDef) {
// get line of next statement -> for a static method, this should
// be the staticmethod call
PythonTreeMethod childMethod = new PythonTreeMethod(this, (FunctionDef) statement, nextLine);
this.childMethodDict.put(childMethod.getName(), childMethod);
// look for object attributes in __init__
if (childMethod.getName().equals("__init__")) {
stmtType[] methodStmts = childMethod.getAstNode().body;
// iterate over all method body statements
for (int j=0; j < methodStmts.length; j++) {
int nextStmtLine = nextLine;
if (j < methodStmts.length - 1 )
nextStmtLine = methodStmts[j+1].beginLine;
stmtType methodStmt = methodStmts[j];
if (methodStmt instanceof Assign) {
// need line of next statement to be able
// to multi-line-expressions
if (j < methodStmts.length-1)
nextStmtLine = methodStmts[j+1].beginLine;
PythonTreeAttribute childAtt = new PythonTreeAttribute(this, (Assign) methodStmt, false, nextStmtLine);
if (childAtt.isValid())
this.childObjectAttrDict.put(childAtt.getName(), childAtt);
}
}
}
}
// get static attributes
else if (statement instanceof Assign) {
PythonTreeAttribute childAtt = new PythonTreeAttribute(this, (Assign) statement, true, nextLine);
if (childAtt.isValid())
this.childStaticAttrDict.put(childAtt.getName(), childAtt);
}
}
}
/**
* Write/update the given XMI id to the classes docstring
* @param xmiID
*/
public void writeXmiID(String xmiID) {
if (this.docString != null) {
this.docString = docString.replaceAll("\\n[\\s]*# PyUML: [^\\n]*", "");
String lineToInsert ="";
lineToInsert += "# PyUML: Do not remove this line! # XMI_ID:" + xmiID+"\n";
if (! this.docString.endsWith("\n"))
lineToInsert = "\n"+this.indent + lineToInsert + this.indent ;
if (! this.docString.startsWith("\n"))
this.docString = "\n"+this.indent + this.docString;
this.docString = docString + lineToInsert;
FileRefactoring.replaceFromCoordinate(this.inFile, this.docStringLine, this.docStringCol+3, this.docString, "\"\"\"", true);
}else{
this.docString=this.indent+"\"\"\" # PyUML: Do not remove this line! # XMI_ID:" + xmiID + "\"\"\"\n";
FileRefactoring.insertAtLine(this.inFile, this.docStringLine + 1, this.docString);
}
}
/**
* Update the associations in the classes docstring.
*/
public boolean updateAssociations(Classifier modelClass) throws PyUMLSynchronizeCodeException {
if (this.docString != null) {
String lineToInsert="";
if (modelClass.getAssociations().size() > 0)
lineToInsert += "# PyUML: Associations of this class:\n";
associationsLoop:
for (Association as : modelClass.getAssociations()) {
String asName = "";
if (as.getName() != null && as.getName().length() > 0 )
asName = "'" + as.getName()+"' ";
List<Type> ends = as.getEndTypes();
String otherClass = "self ";
String otherEndLabel ="";
String ownEndLabel ="";
if (as.getMemberEnds().size() >1) {
Type end;
if (ends.size() > 1) {
Type end1 = ends.get(0);
Type end2 = ends.get(1);
end = (end1.equals(modelClass) ? end2 : end1);
otherEndLabel = (end1.equals(modelClass) ? as.getMemberEnds().get(1).getName()
: as.getMemberEnds().get(0).getName());
ownEndLabel = (! end1.equals(modelClass) ? as.getMemberEnds().get(1).getName()
: as.getMemberEnds().get(0).getName());
} else {
Type end1 = ends.get(0);;
Type end2 = as.getMemberEnds().get(1).getType();
if (end2 == null)
end = end1;
else
end = (end1.equals(modelClass)?end2:end1);
otherEndLabel = as.getMemberEnds().get(0).getName();
ownEndLabel = as.getMemberEnds().get(1).getName();
if (end.equals(modelClass)) {
// if the only known end of the navigatable association
// is this class, ignore this association.
continue associationsLoop;
}
}
if (otherEndLabel != null && otherEndLabel.length() > 0)
otherEndLabel = " (being '"+otherEndLabel + "')";
else
otherEndLabel = "";
if (ownEndLabel != null && ownEndLabel.length() > 0)
ownEndLabel = "(being '"+ownEndLabel + "') ";
else
ownEndLabel = "";
String endType = "";
if (end instanceof Classifier)
endType = "class";
else if (end instanceof Interface)
endType = "interface";
else endType = "model element";
otherClass = endType+" "+ end.getName()+otherEndLabel+" in package "+ParseHelpers.getModelPackageStructure(end)+" ";
}
lineToInsert += this.indent+"# PyUML: Association "+asName+ownEndLabel+"to " +otherClass+"\n";
}
String newDocString = this.docString.replaceAll("\\n[\\s]*# PyUML: Association[^\\n]*", "");
if (! newDocString.endsWith("\n" + this.indent))
lineToInsert = "\n" + this.indent + lineToInsert;
if (! newDocString.startsWith("\n"))
newDocString = "\n" + this.indent + newDocString;
newDocString += lineToInsert;
if (newDocString.equals(this.docString))
return false;
this.docString = newDocString;
FileRefactoring.replaceFromCoordinate(this.inFile, this.docStringLine, this.docStringCol+3, this.docString, "\"\"\"", true);
this.setChanged(this.getPackageStructure()+this.getName()+"-docString", 0);
return true;
}
return false;
}
/**
* gets a Set of PythonTreeClasses that are
* the superclasses of this class, which are located
* in the same project.
* This must be called only after a complete initialization
* of the Python Tree so that all super classes can be found
* in the root node's classNameDict.
*/
public Set<PythonTreeClass> getGeneralizationsInProject() {
// try to find PythonTreeClasses from the superclass Strings
Set<PythonTreeClass> superClassesInProject = new HashSet<PythonTreeClass>();
for (String superClass :this.superClasses) {
if (this.getRoot().getClassDict().containsKey(superClass))
superClassesInProject.add(this.getRoot().getClassDict().get(superClass));
}
return superClassesInProject;
}
/**
* gets a Set of PythonTreeClasses that are
* the superclasses of this class, which are located
* in the same project. -> Uses the model generalizations to find
* the general class, not the code.
* This must be called only after a complete initialization
* of the Python Tree so that all super classes can be found
* in the root node's classNameDict.
*/
public Set<PythonTreeClass> getModelGeneralizationsInProject(Classifier modelClass) {
Set<PythonTreeClass> superClassesInProject = new HashSet<PythonTreeClass>();
for (Relationship rel : modelClass.getRelationships()) {
if (rel instanceof Realization) {
Realization real = (Realization) rel;
if (real.getClients().size() > 0 && real.getClients().get(0) instanceof Classifier) {
Classifier client = (Classifier) real.getClients().get(0);
String packageStructure = ParseHelpers.getModelPackageStructure(client) + client.getName();
PythonTreeClass pyClass = this.getRoot().getClassDict().get(packageStructure);
superClassesInProject.add(pyClass);
}
}
}
for (Classifier superClass : modelClass.getGenerals()) {
String packageStructure = ParseHelpers.getModelPackageStructure(superClass) + superClass.getName();
PythonTreeClass pyClass = this.getRoot().getClassDict().get(packageStructure);
if (pyClass != null)
superClassesInProject.add(pyClass);
}
return superClassesInProject;
}
/**
* synchronize methods and attributes of the model with this class
* This is not done by xmi_id, but elements that changed name are always
* deleted and newly created
*/
public boolean synchronizeModel(NamedElement modelElement){
/* only classes in packages are supported. If we find a top-level-class,
* exit with warning! */
if (super.getParent().isRoot()) {
if (getRoot().isShowWarnings()) {
MessageDialog.openWarning(null, "Error: Top level class detected!",
"Please use in model and code *only* classes inside packages!\n" +
"The class was: " + this.getInFile().getName());
}
return false;
}
// run super method to save corresponding model element
super.synchronizeModel(modelElement);
Classifier modelClass = (Classifier) modelElement;
// set this class private in model, if its name starts with "__"
// use protected, if class starts with "_" (this is only python convention)
if (this.getName().startsWith("__")) {
modelClass.setVisibility(VisibilityKind.PRIVATE_LITERAL);
} else if (this.getName().startsWith("_")) {
modelClass.setVisibility(VisibilityKind.PROTECTED_LITERAL);
}
// ## process methods and attributes of this class ##
// create dictionary of methods / classes, so that
// a model element can be accessed by name
Map<String, Operation> modelChildMethods = new Hashtable<String, Operation>();
EList<Operation> ownedOperations = null;
if (modelClass instanceof Class)
ownedOperations = ((Class) modelClass).getOwnedOperations();
else if (modelClass instanceof Interface)
ownedOperations = ((Interface) modelClass).getOwnedOperations();
else ownedOperations = new BasicEList<Operation>();
for (Operation op : ownedOperations) {
modelChildMethods.put(op.getName(), op);
}
List<Operation> processedOperations = new Vector<Operation>();
Map<String, Property> modelChildProps = new Hashtable<String, Property>();
for (Property prop : modelClass.getAttributes()) {
modelChildProps.put(prop.getName(), prop);
}
List<Property> processedProperties = new Vector<Property>();
// recurse on methods / attributes, if they exist in model;
// otherwise create and recurse
for (PythonTreeMethod pyChildMeth : this.childMethodDict.values()) {
if (modelChildMethods.containsKey(pyChildMeth.getName())){
// everything is OK, child method with same name found.
// method parameters are edited in child element
// ### Synchronize child ###
boolean success = pyChildMeth.synchronizeModel(modelChildMethods.get(pyChildMeth.getName()));
if (! success)
return false;
processedOperations.add(modelChildMethods.get(pyChildMeth.getName()));
} else {
// Method does *not* exist -> create it and process children
Operation modelNewMethod = null;
if (modelClass instanceof Class)
modelNewMethod = ((Class) modelClass).createOwnedOperation(pyChildMeth.getName(), null, null);
else if (modelClass instanceof Interface)
modelNewMethod = ((Interface) modelClass).createOwnedOperation(pyChildMeth.getName(), null, null);
// ### Synchronize child ###
boolean success = pyChildMeth.synchronizeModel(modelNewMethod);
if (! success)
return false;
processedOperations.add(modelNewMethod);
}
}
Collection<PythonTreeAttribute> allAttributes = new Vector<PythonTreeAttribute>();
allAttributes.addAll(this.childStaticAttrDict.values());
allAttributes.addAll(this.childObjectAttrDict.values());
for (PythonTreeAttribute pyChildAtt : allAttributes) {
if (modelChildProps.containsKey(pyChildAtt.getName())){
// everything is OK, child method with same name found.
// method parameters are edited in child element
// ### Synchronize child ###
boolean success = pyChildAtt.synchronizeModel(modelChildProps.get(pyChildAtt.getName()));
if (! success)
return false;
processedProperties.add(modelChildProps.get(pyChildAtt.getName()));
} else {
// Attribute does *not* exist -> create it and process children
Property modelNewProp = null;
if (modelClass instanceof Class)
modelNewProp = ((Class) modelClass).createOwnedAttribute(pyChildAtt.getName(), null);
else if (modelClass instanceof Interface)
modelNewProp = ((Interface) modelClass).createOwnedAttribute(pyChildAtt.getName(), null);
// ### Synchronize child ###
boolean success = pyChildAtt.synchronizeModel(modelNewProp);
if (! success)
return false;
processedProperties.add(modelNewProp);
}
}
// delete all methods / attributes, that do no longer exist in code
for (Operation op : modelClass.getOperations()) {
if (! processedOperations.contains(op))
this.getRoot().getModelElementsToDestroy().add(op);
}
for (Property prop : modelClass.getAttributes()) {
if (! processedProperties.contains(prop))
this.getRoot().getModelElementsToDestroy().add(prop);
}
return true;
}
/////// Synchronize Code (see superclass) ///////
public boolean synchronizeCode(Classifier modelClass) throws PyUMLSynchronizeCodeException{
// get and filter Stereotypes of this model class
// (only PyUML Stereotypes, as defined in GlobalConstants, are saved)
this.usedStereotypes = new Vector<String>();
List<EObject> stypes = modelClass.getStereotypeApplications();
for (EObject stObject : stypes) {
String stereotypeName = stObject.eClass().getName();
if (GlobalConstants.getPossibleStereoTypes().contains(stereotypeName)) {
this.usedStereotypes.add(stereotypeName);
}
}
this.handleStereoTypes(modelClass);
// see if class was renamed:
if (! this.getName().equals(modelClass.getName())) {
// now, the file name is OK, rename the class in python code
this.renameClassInCode(modelClass);
this.getRoot().getRenamedClasses().add(this.getName());
return true;
}
// if class was renamed, and the old class name matches the file name,
// rename the file
// TODO: Works and can be uncommented,
// but generalizations/imports are not
// updated, so leaving that commented
/*
if (this.getRoot().getRenamedClasses().contains(this.inFile.getFilePath().lastSegment().replace(".py", ""))) {
IPath newFilePath = this.renameClassFile(modelClass.getName());
if (newFilePath != null) {
this.inFile.setFilePath(newFilePath.addFileExtension("py"));
return true;
}
else
throw new PyUMLSynchronizeCodeException();
}
*/
// see if list of superclasses changed
boolean superClassesChanged = false;
List<Realization> interfaceList = new Vector<Realization>();
for (Relationship rel : modelClass.getRelationships()) {
if (rel instanceof Realization) {
interfaceList.add((Realization) rel);
}
}
if (modelClass.getGeneralizations().size() + interfaceList.size() != this.superClasses.size())
superClassesChanged =true;
else for (Generalization gen : modelClass.getGeneralizations()) {
if (! this.superClasses.contains(
ParseHelpers.getModelPackageStructure(gen.getGeneral())
+ gen.getGeneral().getName())) {
superClassesChanged = true;
}
}
// if yes, rewrite list of generalizations
if (superClassesChanged)
this.rewriteGeneralizations(modelClass, interfaceList);
// if there are non-static parameters, make sure there is a
// __init__-method in the class
boolean hasStaticAttributes = false;
boolean hasNonStaticAttributes = false;
EList<Property> ownedProperties = null;
if (modelClass instanceof Class)
ownedProperties = ((Class) modelClass).getOwnedAttributes();
else if (modelClass instanceof Interface)
ownedProperties = ((Interface) modelClass).getOwnedAttributes();
for (Property modelProp : ownedProperties) {
if (! modelProp.isStatic())
hasNonStaticAttributes = true;
else
hasStaticAttributes = true;
}
if (hasNonStaticAttributes && (! childMethodDict.containsKey("__init__"))) {
this.createNewMethod("__init__", this.lastLine, false, null);
this.getRoot().setChangedFileLine("def __init__(self):"+this.lastLine);
return true;
}
if (hasNonStaticAttributes) {
// check the __init__ method; this is not necessarily checked automatically, as
// it might be not present in the model
boolean changed = PythonTreeMethod.fixAttributeDefinitions(modelClass, childMethodDict.get("__init__").getIndent(), this.childObjectAttrDict, this, childMethodDict.get("__init__").getLastLine(), false);
if (changed) return true;
}
if (hasStaticAttributes) {
// check if all static attributes in model are present in code
int insertLine;
if (this.astNode.body.length > 0)
insertLine = this.astNode.body[0].beginLine + 1;
else
insertLine = this.lastLine;
boolean changed = PythonTreeMethod.fixAttributeDefinitions(modelClass, " ", this.childStaticAttrDict, this, insertLine, true);
if (changed) return true;
}
// iterate over child elements of model
// save all method names in iteration to detect duplicate method names
List<String> childModelOpNames = new Vector<String>();
for (Element modelChild:modelClass.getOwnedElements()) {
//child method found
if (modelChild instanceof Operation) {
Operation childModelOp = (Operation) modelChild;
// test if a method with this name already existed in model.
// if yes, an exception is thrown -> overloading is not supported in Python
if (childModelOpNames.contains(childModelOp.getName())) {
throw new PyUMLSynchronizeCodeException("Multiple methods with same name are not\n" +
"supported in Python!\n" +
"Please correct this error in the UML model!\n" +
"Source:\n" +
"- Method: "+ childModelOp.getName() +
"\n- Class: "+ this.getName() +
"\n- Package: "+this.getPackageStructure());
} else {
childModelOpNames.add(childModelOp.getName());
}
PythonTreeMethod childPyOp = null;
for (PythonTreeMethod childMeth : this.childMethodDict.values()) {
if (childMeth.getName().equals(childModelOp.getName())) {
childPyOp = childMeth;
}
}
if (childPyOp != null) {
// everything OK, child method with same name found
boolean startNew = childPyOp.synchronizeCode(childModelOp);
if (startNew) return true;
}
else {
// look if method was existing and was renamed
String xmi_id = this.getRoot().getModelXmiDict().get(childModelOp);
Map<String, EObject> xmiModelDictOld = this.getRoot().getXmiModelDictOld();
if (xmiModelDictOld.containsKey(xmi_id)) {
Operation oldOp = (Operation) xmiModelDictOld.get(xmi_id);
String oldOpClassName = ((NamedElement)oldOp.getOwner()).getName();
if (oldOpClassName.equals(this.getName())) {
String oldName = oldOp.getName();
PythonTreeMethod renamedPyOp = null;
for (PythonTreeMethod childMeth : this.childMethodDict.values()) {
if (childMeth.getName().equals(oldName)) {
renamedPyOp = childMeth;
}
}
if (renamedPyOp != null) {
// found operation that was renamed.
// rename it also in code
int line = renamedPyOp.getAstNode().beginLine;
int col = renamedPyOp.getAstNode().name.beginColumn;
boolean result = BicycleRefactoring.doRenameObject(this.inFile,
childModelOp.getName(), line, col, this.getRoot().getProject());
if (! result)
return false;
setChanged(childModelOp.getName(), line);
return true;
}
// else -> was not renamed -> create new method
this.createNewMethod(childModelOp.getName(), this.lastLine, childModelOp.isStatic(), childModelOp);
setChanged(childModelOp.getName(), this.lastLine);
return true;
}
// else -> was not renamed -> create new method
this.createNewMethod(childModelOp.getName(),this.lastLine, childModelOp.isStatic(), childModelOp);
setChanged(childModelOp.getName(), this.lastLine);
return true;
}
// else -> was not renamed -> create new method
this.createNewMethod(childModelOp.getName(),this.lastLine, childModelOp.isStatic(), childModelOp);
setChanged(childModelOp.getName(), this.lastLine);
return true;
}
}
}
// check if imports are correct!
boolean changed= this.fixImports(modelClass);
if (changed)
return true;
changed= this.updateAssociations(modelClass);
if (changed)
return true;
return false;
}
/**
* Realize all recognized stereotypes;
* For the BeanClass Stereotype, this will simply create getter/setter
* methods for every non-static attribute in the model, which will then automatically
* be realized as code.
*/
public void handleStereoTypes(Classifier modelClassifier) {
if (! (modelClassifier instanceof Class)) {
return;
}
Class modelClass = (Class) modelClassifier;
if (this.usedStereotypes.contains(GlobalConstants.STEREOTYPE_BEANCLASS)) {
for (Property att : modelClass.getOwnedAttributes()) {
String attName = att.getName();
// make first letter uppercase for CamelCase bean standard
attName = attName.substring(0,1).toUpperCase() + attName.substring(1);
// add getter/setter
// check if method already exists
boolean getterExists = false;
boolean setterExists = false;
for (Operation op : modelClass.getOwnedOperations()) {
if (op.getName().equals("get"+attName))
getterExists = true;
if (op.getName().equals("set"+attName))
setterExists = true;
}
if (!getterExists) {
Operation op = modelClass.createOwnedOperation("get"+attName, null, null);
op.createOwnedParameter("self", null);
op.createOwnedComment().setBody("Getter Created by PyUML:"+att.getName());
}
if (!setterExists) {
Operation op = modelClass.createOwnedOperation("set"+attName, null, null);
op.createOwnedParameter("self", null);
op.createOwnedParameter("value", null);
op.createOwnedComment().setBody("Setter Created by PyUML:"+att.getName());
}
}
}
}
/**
* rename classes using bicycle repair man
* @param modelClass
* @return success
*/
public boolean renameClassInCode(Classifier modelClass) throws PyUMLSynchronizeCodeException{
// rename Class using Bicycle Repair Man
return BicycleRefactoring.doRenameObject(
this.inFile,
modelClass.getName(), this.astNode.name.beginLine,
this.astNode.name.beginColumn, this.getRoot().getProject());
}
/**
* Move this class in the python code to another package/folder
* * Rename the class file
*
* @param newPath the IPath where the class is to be moved
* @return true on success, false if package already exists
*/
public boolean moveClass(IPath newPath) {
IFile classFile = EclipseHelperMethods.createFile(
this.inFile.getFilePath().toOSString(), this.getProject());
if (! classFile.exists()) {
MessageDialog.openError(null, "Error moving class", "Class " + this.name + " cannot " +
"be renamed\n because it does not seem to exist!");
return false;
}
try {
newPath = EclipseHelperMethods.createFile(newPath.toOSString()+".py", this.getProject()).getFullPath();
classFile.move(newPath, true, null);
this.inFile.setFilePath(this.getProject().getLocation().append(newPath));
} catch (CoreException e) {
MessageDialog.openError(null, "Error moving class", "The class " +this.name+ " cannot" +
" be moved to "+ newPath +".\n Propably a class with the new name already exists!" +
"\n\nReason:\n" +e.getMessage());
return false;
}
PythonTreePackage.completeNewStart = true;
return true;
}
/**
* Rename this class file in the python code
*
* @param newName the new name of the class in the same directory
* @return the iPath of the created file, null if new class file already exists
*/
public IPath renameClassFile(String newName) {
IFile classFile = EclipseHelperMethods.createFile(this.inFile.getFilePath().toOSString(), this.getProject());
IPath newPath = classFile.getLocation().removeLastSegments(1).append(newName);
if (this.moveClass(newPath))
return newPath;
else
return null;
}
/**
* Add/remove generalizations from class in code
* @param modelClass
* @return success
*/
public boolean rewriteGeneralizations(Classifier modelClass, List<Realization> interfaceList) {
int line = this.astNode.beginLine;
String superClassString = "";
// write all available generalizations as a comma-separated string
for (Generalization gen : modelClass.getGeneralizations()) {
if (superClassString.length() > 0)
superClassString +=", ";
superClassString+= gen.getGeneral().getName();
}
for (Realization real :interfaceList) {
if (superClassString.length() > 0)
superClassString +=", ";
superClassString+= real.getClients().get(0).getName();
}
// write all generalizations that were in code, but are not available
// in model (e.g. system classes)
for (String gen : this.superClasses) {
// if this class is part of project, is should be in model
// -> if it is not in model, it is to be removed
if (this.getRoot().getClassNameDict().containsValue(
gen.substring(gen.lastIndexOf("/")+1)))
continue;
// otherwise, this generalization has to stay, because it cannot be
// present in model
if (superClassString.length() > 0)
superClassString +=", ";
superClassString+= gen.substring(gen.lastIndexOf("/")+1);
}
// if there were generalizations, surround them by parenthesis
if (superClassString.length() > 0) {
superClassString = "(" + superClassString + ")";
}
// replace generalizations in class definition
String classDefLine = FileRefactoring.getLine(this.inFile, line);
Pattern pattern = Pattern.compile("([\\s]*class[\\s]*[^:\\(]*[\\s]*)(\\(?[^:]*):");
Matcher matcher = pattern.matcher(classDefLine);
if (! matcher.find()){
return false;
}
String newClassDefLine = matcher.group(1) + superClassString + ":";
if (! classDefLine.equals(newClassDefLine))
FileRefactoring.replaceLine(this.inFile, line, newClassDefLine, true);
return true;
}
/**
* create a new class in a File with the classes name
* use all information of the model class
*
* @param modelClass
* @param parentPackage
* @return true on success, false if something went wrong
*/
public static boolean createNewClass(Classifier modelClass, PythonTreePackage parentPackage)
throws PyUMLSynchronizeCodeException{
String className = modelClass.getName();
File newClassFile = new File(parentPackage.getPackageDir().append(className).toOSString() + ".py");
// create class definition line to insert to file
String newClassLine = "";
newClassLine += "class " + className;
// create file
if (!newClassFile.exists()) {
try {
newClassFile.createNewFile();
} catch (IOException e) {
e.printStackTrace();
throw new PyUMLSynchronizeCodeException();
}
} else {
// if file already exists, append new content
newClassLine = ParseHelpers.fileToString(newClassFile, parentPackage.getProject())
+ "\n" + newClassLine;
}
// get all superclasses and add them to class definition
EList<Classifier> superClasses = modelClass.getGenerals();
if (superClasses.size() > 0) {
newClassLine = "\n" + newClassLine;
newClassLine +="(";
for (int i=0; i<superClasses.size(); i++) {
Classifier superClass = superClasses.get(i);
if (i > 0)
newClassLine += ", ";
newClassLine += superClass.getName();
}
newClassLine += ")";
}
newClassLine += ":";
String xmi_id= parentPackage.getRoot().getModelXmiDict().get(modelClass);
if (modelClass instanceof Class)
newClassLine += "\n \"\"\"\n Class created by PyUML";
else
newClassLine += "\n \"\"\"\n Interface created by PyUML";
newClassLine += "\n # PyUML: Do not remove this line! # XMI_ID:" + xmi_id +
"\n \"\"\"";
ParseHelpers.stringToFile(newClassFile, newClassLine, parentPackage.getProject());
parentPackage.getRoot().setChangedFileLine(newClassFile.getAbsolutePath());
return true;
}
/**
* Checks the current import statements to ensure all superclasses
* are imported
*
* @return true, if anything was changed, false otherwise
*/
public boolean fixImports(Classifier modelClass) throws PyUMLSynchronizeCodeException{
// return false; // See issues in FIXME below
// iterate over all superclasses in project
Map<String, String> superClassPackages = new HashMap<String, String>();
for (PythonTreeClass superClass : this.getModelGeneralizationsInProject(modelClass)) {
// get package structure, e.g. mypackage.subpackage.MyClass
String packageDef = "";
PythonTreePackage superPackage = superClass.getParent();
while (superPackage != null && (! (superPackage instanceof PythonTreeRoot))) {
packageDef = superPackage.getName() + "." + packageDef;
superPackage = superPackage.getParent();
}
String superClassModuleName = superClass.inFile.getName().replace(".py", "");
packageDef += superClassModuleName;
superClassPackages.put(superClass.getName(), packageDef);
}
// find import statements in code
// suppose, that all import statements are at the beginning of the
// file, only white or comment lines can be above them.
String[] fileContent = this.inFile.getFileContent().split("\n");
int lineNo = 0;
String line = "";
if (superClassPackages.size() > 0) {
do {
line = fileContent[lineNo];
String[] importParts = getImport(line);
if (importParts != null){
String className = importParts[0];
String packageDef = importParts[1];
String comment = importParts[2];
// if class is imported, but package changed -> rewrite line!
// FIXME: This is messed up since an import may be done without a "from" statement;
// it might look like "import mypackage" and refer to it by "mypackage.MyClass"
// or even like "import mypackage.mysubpackage as myalias"...
// therefore I disable import fixing as long as this issue has not been solved
// by Jakob
if (superClassPackages.containsKey(className) &&
( ! packageDef.equals(superClassPackages.get(className)))) {
String newString = "from " + superClassPackages.get(className) + " import " + className + comment;
FileRefactoring.replaceLine(this.inFile, lineNo, newString, true);
this.setChanged(null, lineNo);
return true;
}
if (superClassPackages.containsKey(className) && superClassPackages.get(className).equals(packageDef)) {
// line is OK -> remove them from new-import-to-insert-list
superClassPackages.remove(className);
}
}
lineNo ++;
} while(line.matches("[\\s]*[#]?") || line.matches("from.*import.*")||line.matches("import.*"));
// now, all lines were analyzed; the needed import, that were not covered
// by a line, must be inserted now!
if (superClassPackages.size() == 0)
return false;
String insertLines = "";
for (String superClassName : superClassPackages.keySet()) {
String packageLine = superClassPackages.get(superClassName);
insertLines += "from " + packageLine + " import " + superClassName + "\n";
}
// if last line is empty, don't insert new empty line
if (lineNo == 1)
// no empty lines are in fron of the insertion -> insert empty line
insertLines += "\n";
else if (line.length() > 0 && (! fileContent[lineNo-2].matches("^[\\s]*$"))) {
insertLines += "\n";
}
FileRefactoring.insertAtLine(this.inFile, lineNo-1, insertLines);
this.setChanged(null, lineNo);
return true;
}
return false;
}
/**
* Returns a string array containing the class name, package
* definition and the trailing comment at the zero, first and second
* indices for the given import line.
*
* @param line The python import line
*
* @return A string array containing class name and packege definition
* or null if not found.
*
* @see PythonTreeClass#FROM_IMPORT_PATTERN
*/
private String[] getImport(String line) {
Matcher matcher = FROM_IMPORT_PATTERN.matcher(line);
if (matcher.find()) {
String[] importParts = new String[3];
importParts[0] = matcher.group(2);
importParts[1] = matcher.group(1);
importParts[2] = matcher.group(3);
return importParts;
}
return null;
}
/**
* This creates a new method in the current class
*
* @param name the method's name
* @param lastClassLine the last line in code that is part of the class
* (or the first line after class -1)
* -> this is the line where the method is inserted!
* can be -1 if the method should be appended to end of file
* @param isStatic if true, method will have a staticmethod call
* @param modelOp the corresponding model operation; if there is a comment
* indicating that this is a getter/setter, the appropriate method content
* is created. Can be null.
*/
public void createNewMethod(String name, int lastClassLine, boolean isStatic, Operation modelOp) {
String newLine;
if (isStatic) {
newLine = "\n" + this.indent + "def " + name +"():\n \"\"\" Created by PyUML \"\"\"\n"
+ this.indent + name + " = staticmethod (" + name + ")\n";
} else {
newLine = "\n" + this.indent + "def " + name +"(self):\n \"\"\" Created by PyUML \"\"\"\n";
}
// If this is a getter/setter class, put content to method
if (modelOp != null && modelOp.getOwnedComments().size() > 0) {
String opComment = modelOp.getOwnedComments().get(0).getBody();
if (opComment.startsWith("Getter Created by PyUML:")) {
String attribute = opComment.replaceAll("Getter Created by PyUML:", "");
newLine += " return self."+attribute+"\n";
}
else if (opComment.startsWith("Setter Created by PyUML:")) {
String attribute = opComment.replaceAll("Setter Created by PyUML:", "");
newLine += " self."+attribute+" = value\n";
}
}
if (lastClassLine <= 0)
FileRefactoring.appendToFile(this.inFile, newLine);
else
FileRefactoring.insertAtLine(this.inFile, lastClassLine, newLine);
}
/**
* returns the parent package of this class
*/
public PythonTreePackage getParent() {
return this.inFile.getParent();
}
/**
* tells the position of current code change to PythonTreeRoot
* so that it can decide if the same line is changed again
* and again and thus detect an unfinished loop.
*
* @param pos Additional information. This is useful, if
* line is -1 (append), so that only dead-loops are detected
* @param line the changed line in the file
*/
private void setChanged(String pos, int line) throws PyUMLSynchronizeCodeException{
if (pos == null)
pos = "";
this.getRoot().setChangedFileLine(inFile.getFilePath().toOSString()+":"+line+":"+pos);
}
public Map<String, PythonTreeMethod> getChildMethodDict() {
return childMethodDict;
}
public Map<String, PythonTreeAttribute> getChildStaticAttrDict() {
return childStaticAttrDict;
}
public Map<String, PythonTreeAttribute> getChildObjectAttrDict() {
return childObjectAttrDict;
}
public void setChildStaticAttrDict(
Map<String, PythonTreeAttribute> childStaticAttrDict) {
this.childStaticAttrDict = childStaticAttrDict;
}
/**
* This creates a string containing the python package structure as
* "/packSup/packSub/" , where the last package contains the
* current class.
* @return a string containing the parent package structure
* of this class
*/
public String getPackageStructure() {
PythonTreePackage pack = this.getParent();
String structure = "";
while (! (pack instanceof PythonTreeRoot))
{
structure = "/"+pack.getName() + structure ;
pack = pack.getParent();
}
return structure + "/";
}
/**
* Gets a complete package structure (including class)
* from the class imports for a class (separated by "/"
* e.g. "/a/b/c/MyClass" for "MyClass", if there is
* an import line "from a.b.c.MyClassFile import MyClass"
* or "import a.b.c.MyClassFile" and class name is "MyClassFile.MyClass"
* or "import a.b.c.MyClassFile as myalias" and class name is "myalias.MyClass".
*
* @param className The name of the class too lookup
* @return The full package / class structure
*/
public String getPackFromImports(String className) {
search:
if (!packageCache.containsKey(className)) {
// the fully qualified package name in the format "/mypackage/mysubpackage/MyClass"
String packageName = null;
String fileContent = this.getInFile().getFileContent();
Pattern pattern = Pattern.compile(".*from[\\s]+([^\\s]+)\\.[^\\.\\s]+[\\s]+import[\\s]+" + className + ".*");
Matcher matcher = pattern.matcher(fileContent);
if (matcher.find()){
packageName = "/" + (matcher.group(1) + "." + className).replace(".", "/");
packageCache.put(className, packageName);
break search;
}
// lets check for fully qualified package imports
// e.g. "import mypackage.myclassfile" and "mypackage.myclassfile.MyClass"
// or aliases "import mypackage.mysubpackage.myclassfile as myalias" and "myalias.MyClass"
String[] parts = className.split("\\.");
if (parts.length > 1) { // there is at least one package, without the classfile
String strippedClassName = parts[parts.length - 1];
String prefix = join(parts, ".", 0, parts.length - 1);
pattern = Pattern.compile(".*import[\\s]+" + prefix + ".*");
matcher = pattern.matcher(fileContent);
if (matcher.find()) { // we have a full qualified import
packageName = "/" + join(parts, "/", 0 , 2) + "/" + strippedClassName;
packageCache.put(className, packageName);
break search;
}
pattern = Pattern.compile(".*import[\\s]+([^\\s]+)\\.[^\\.\\s]+[\\s]+as[\\s]+" + prefix + ".*");
matcher = pattern.matcher(fileContent);
if (matcher.find()) { // we have an alias
packageName = "/" + (matcher.group(1) + "." + strippedClassName).replace(".", "/");
packageCache.put(className, packageName);
break search;
}
}
// if no import (with class structure in front) was found, we assume that the class
// is in the same package
packageCache.put(className, this.getPackageStructure() + className);
}
return packageCache.get(className);
}
/**
* Utility method to create a string from a string array,
* joined with the given separator.
*
* @param parts The string array to get the parts to join from
* @param separator The string that glues the parts together
* @return The joined string from the given parts and separator
*
* @see PythonTreeClass#join(String[], String, int, int)
*/
private String join(String[] parts, String separator) {
return join(parts, separator, 0, parts.length);
}
/**
* Utility method to create a string from a (partial) string array,
* joined with the given separator.
* <br />
* E.g.:
* <code>
* String abc = join(new String[] { a, b, c }, "/", 0, 3);
* </code>
* will result in
* <code>
* "a/b/c"
* </code>
*
* @param parts The string array to get the parts to join from
* @param separator The string that glues the parts together
* @param offset From where to start in the parts array
* @param length How many elements from offset should be joined
* @return The joined string from the given parts and separator
*/
private String join(String[] parts, String separator, int offset, int length) {
if (parts.length - offset < length) {
return null;
}
StringBuffer buffer = new StringBuffer();
for (int i = offset; i < length; i++) {
buffer.append(parts[i]);
if (i < length - 1) {
buffer.append(separator);
}
}
return buffer.toString();
}
/**
* @return the file where this class is defined
*/
public PythonTreeFile getInFile() {
return inFile;
}
public List<String> getUsedStereotypes() {
return usedStereotypes;
}
public String getIndent() {
return indent;
}
}