diff --git a/pyUml/src/pyUML/pythonTree/PythonTreeClass.java b/pyUml/src/pyUML/pythonTree/PythonTreeClass.java index 2f61cf9..57793e2 100755 --- a/pyUml/src/pyUML/pythonTree/PythonTreeClass.java +++ b/pyUml/src/pyUML/pythonTree/PythonTreeClass.java @@ -19,6 +19,7 @@ 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; @@ -35,6 +36,7 @@ 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; @@ -61,7 +63,10 @@ import pyUML.refactoring.FileRefactoring; * - can write XMI ID to classes docstring * */ -public class PythonTreeClass extends PythonTreeNode{ +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; @@ -74,7 +79,16 @@ public class PythonTreeClass extends PythonTreeNode{ Map childMethodDict; Map childStaticAttrDict; Map 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 packageCache; + /** * Constructor to create a PythonTreeClass of a python class AST * @param parent the PythonTreePackage, which is parent of this class @@ -92,13 +106,14 @@ public class PythonTreeClass extends PythonTreeNode{ this.childMethodDict = new Hashtable(); this.childStaticAttrDict = new Hashtable(); this.childObjectAttrDict = new Hashtable(); + packageCache = new HashMap(); this.initClassNode(); this.initSubStructures(); } private void initClassNode() throws PyUMLParseException, PyUMLCancelledException{ - this.name=((NameTok) this.astNode.name).id; + this.name = ((NameTok)this.astNode.name).id; this.docStringLine = this.astNode.beginLine; this.docStringCol = this.astNode.beginColumn; @@ -131,7 +146,7 @@ public class PythonTreeClass extends PythonTreeNode{ PythonTreeRoot root = this.getRoot(); // test if a class of this name already exists - String structuredName = this.getPackageStructure()+this.name; + String structuredName = this.getPackageStructure() + this.name; this.getRoot().setSubTaskName(structuredName); if (root.getClassDict().containsKey(structuredName) && root.isShowWarnings()) { @@ -155,15 +170,16 @@ public class PythonTreeClass extends PythonTreeNode{ root.getClassDict().put(structuredName, this); root.getClassNameDict().put(structuredName, this.name); - for (int i=0; i < this.astNode.bases.length; i++) { + 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) { + 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 @@ -187,9 +203,13 @@ public class PythonTreeClass extends PythonTreeNode{ String superClassFullName = line.substring(0, line.indexOf(separator)); superClassFullName = superClassFullName.replaceAll("[\\s]", ""); - - - this.superClasses.add(superClassFullName); + // 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); + } } } } @@ -990,75 +1010,105 @@ public class PythonTreeClass extends PythonTreeNode{ * @return true, if anything was changed, false otherwise */ public boolean fixImports(Classifier modelClass) throws PyUMLSynchronizeCodeException{ - // iterate over all superclasses in project - Map superClassPackages = new HashMap(); - 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); + return false; // See issues in FIXME below +// // iterate over all superclasses in project +// Map superClassPackages = new HashMap(); +// 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; } - - // 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]; - Pattern pattern = Pattern.compile("[\\s]*from[\\s]*([^\\s]*)[\\s]*import[\\s]*([^\\s#]*)([\\s#]?.*)"); - Matcher matcher = pattern.matcher(line); - if (matcher.find()){ - String className = matcher.group(2); - String packageDef = matcher.group(1); - // if class is imported, but package changed -> rewrite line! - if (superClassPackages.containsKey(className) && - ( ! packageDef.equals(superClassPackages.get(className)))) { - String newString = "from " + superClassPackages.get(className) + " import " + className + matcher.group(3); - 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; + return null; } /** @@ -1163,23 +1213,101 @@ public class PythonTreeClass extends PythonTreeNode{ /** * 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 + * e.g. "/a/b/c/MyClass" for "MyClass", if there is * an import line "from a.b.c.MyClassFile import MyClass" - * @param className the name of the class - * @return the full package/class structure + * 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) { - String fileContent = this.getInFile().getFileContent(); - Pattern pattern = Pattern.compile(".*from[\\s]+([^\\s]+)\\.[^\\.\\s]+[\\s]+import[\\s]+"+className+".*"); - Matcher matcher = pattern.matcher(fileContent); - - // if no import (with class structure in front) was found, we assume that the class - // is in the same package - if (! matcher.find()){ - return this.getPackageStructure() + 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); } - String result = "/" + (matcher.group(1) + "." + className).replace(".", "/"); - return result; + 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. + *
+ * E.g.: + * + * String abc = join(new String[] { a, b, c }, "/", 0, 3); + * + * will result in + * + * "a/b/c" + * + * + * @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(); } /**