Java tutorial
/******************************************************************************* * Copyright (c) 2007-2010 Dennis Wagelaar, Vrije Universiteit Brussel. * 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: * Dennis Wagelaar, Vrije Universiteit Brussel *******************************************************************************/ package org.eclipselabs.jar2uml; import java.io.IOException; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Set; import java.util.jar.JarFile; import org.apache.bcel.classfile.AccessFlags; import org.apache.bcel.classfile.JavaClass; import org.eclipse.core.resources.IContainer; import org.eclipse.core.resources.IFile; import org.eclipse.core.resources.IProject; import org.eclipse.core.resources.IResource; import org.eclipse.core.resources.ResourcesPlugin; import org.eclipse.core.runtime.CoreException; import org.eclipse.core.runtime.IPath; import org.eclipse.core.runtime.IProgressMonitor; import org.eclipse.emf.common.util.EMap; import org.eclipse.emf.common.util.URI; import org.eclipse.emf.ecore.EAnnotation; import org.eclipse.emf.ecore.resource.Resource; import org.eclipse.emf.ecore.resource.ResourceSet; import org.eclipse.emf.ecore.resource.impl.ResourceSetImpl; import org.eclipse.jdt.core.IClasspathEntry; import org.eclipse.jdt.core.IJavaModel; import org.eclipse.jdt.core.IJavaProject; import org.eclipse.jdt.core.JavaCore; import org.eclipse.jdt.core.JavaModelException; import org.eclipse.uml2.common.util.UML2Util; import org.eclipse.uml2.uml.Classifier; import org.eclipse.uml2.uml.Comment; import org.eclipse.uml2.uml.Element; import org.eclipse.uml2.uml.Model; import org.eclipse.uml2.uml.NamedElement; import org.eclipse.uml2.uml.Type; import org.eclipse.uml2.uml.UMLFactory; import org.eclipse.uml2.uml.UMLPackage; import org.eclipse.uml2.uml.VisibilityKind; import org.eclipse.uml2.uml.resource.UMLResource; import org.eclipselabs.jar2uml.ui.JarToUMLPlugin; /** * Main class for the jar to UML converter. Start conversion by invoking {@link #run()}. * @author Dennis Wagelaar <dennis.wagelaar@vub.ac.be> */ public final class JarToUML extends JarToUMLRunnable { public static final String EANNOTATION = "Jar2UML"; //$NON-NLS-1$ public static final String MAJOR_BYTECODE_FORMAT_VERSION = "majorBytecodeFormatVersion"; public static final String MINOR_BYTECODE_FORMAT_VERSION = "minorBytecodeFormatVersion"; public static final String PREVERIFIED = "preverified"; private static final int WORK_CREATE_MODEL = 1; private static final int WORK_PARSE_CLASSES = 100; private static final int WORK_ADD_CLASSIFIERS = 200; private static final int WORK_ADD_PROPERTIES = 400; private static final int WORK_INFERRED_TAGS = 1; private static final int WORK_REMOVE_EMPTY = 1; private static final int WORK_ADD_METADATA = 1; private static final int WORK_TOTAL = WORK_CREATE_MODEL + WORK_PARSE_CLASSES + WORK_ADD_CLASSIFIERS + WORK_ADD_PROPERTIES + WORK_INFERRED_TAGS + WORK_REMOVE_EMPTY + WORK_ADD_METADATA; /** * @param args */ public static void main(String[] args) { try { JarToUML jarToUML = new JarToUML(); jarToUML.setFilter(new JavaAPIFilter()); jarToUML.addJar(new JarFile(args[0])); jarToUML.setOutputFile(args[1]); jarToUML.setOutputModelName(args[2]); jarToUML.run(); if (jarToUML.isRunComplete()) { jarToUML.getModel().eResource().save(Collections.EMPTY_MAP); } } catch (Exception e) { JarToUMLResources.report(e); JarToUMLResources.logger.severe(JarToUMLResources.getString("JarToUML.usage")); //$NON-NLS-1$ } } /** * @param path * @return The Java project for the given path, or null */ public static IJavaProject getJavaProject(IPath path) { IJavaModel model = JavaCore.create(ResourcesPlugin.getWorkspace().getRoot()); IResource resource = ResourcesPlugin.getWorkspace().getRoot().findMember(path); IProject project = resource.getProject(); return model.getJavaProject(project.getName()); } /** * Retrieves the project references for javaProject and stores them in refs * @param javaProject * @param refs the project references for javaProject */ public static void findJavaProjectReferences(IJavaProject javaProject, Set<IJavaProject> refs) { try { for (IClasspathEntry cpe : javaProject.getResolvedClasspath(true)) { IPath cpePath; switch (cpe.getEntryKind()) { case IClasspathEntry.CPE_PROJECT: cpePath = cpe.getPath(); IJavaProject ref = getJavaProject(cpePath); refs.add(ref); break; } } } catch (JavaModelException e) { JarToUMLPlugin.getPlugin().report(e); } } /** * @return A new ResourceSet with support for UML models outside Eclipse */ public static ResourceSet createResourceSet() { ResourceSet resourceSet = new ResourceSetImpl(); resourceSet.getPackageRegistry().put(UMLPackage.eNS_URI, UMLPackage.eINSTANCE); resourceSet.getResourceFactoryRegistry().getExtensionToFactoryMap().put(UMLResource.FILE_EXTENSION, UMLResource.Factory.INSTANCE); return resourceSet; } /** * @param javaClass * @return True if the name of javaClass does not parse as an Integer. */ public static boolean isNamedClass(JavaClass javaClass) { final String javaClassName = javaClass.getClassName(); final String leafName = javaClassName.substring(javaClassName.lastIndexOf('$') + 1); try { Integer.parseInt(leafName); return false; } catch (NumberFormatException e) { //everything allright, not an anonymous class } return true; } /** * @param flags * @return The UML representation of flags. */ public static VisibilityKind toUMLVisibility(AccessFlags flags) { if (flags.isPublic()) { return VisibilityKind.PUBLIC_LITERAL; } else if (flags.isProtected()) { return VisibilityKind.PROTECTED_LITERAL; } else if (flags.isPrivate()) { return VisibilityKind.PRIVATE_LITERAL; } else { return VisibilityKind.PACKAGE_LITERAL; } } /** * @param element * @return The qualified name of element */ public static String qualifiedName(final NamedElement element) { final String qName = element.getQualifiedName(); final int sepIndex = qName.indexOf("::"); if (sepIndex > 0) { return qName.substring(sepIndex + 2); } else { return qName; } } /** * @param elements * @return A {@link List} of the qualified names of elements. */ public static List<String> getNameList(Collection<? extends NamedElement> elements) { List<String> list = new ArrayList<String>(); for (NamedElement e : elements) { list.add(qualifiedName(e)); } return list; } /** * Creates a key-value entry under a "Jar2UML" {@link EAnnotation}. * @param element * @param key * @param value */ public static final void annotate(final Element element, final String key, final String value) { EAnnotation ann = element.getEAnnotation(EANNOTATION); if (ann == null) { ann = element.createEAnnotation(EANNOTATION); } final EMap<String, String> details = ann.getDetails(); details.put(key, value); } /** * Removes a key-value entry under a "Jar2UML" {@link EAnnotation}. * Removes the "Jar2UML" annotation if no key-value pairs left. * @param element * @param key */ public static final void deannotate(final Element element, final String key) { final EAnnotation ann = element.getEAnnotation(EANNOTATION); if (ann != null) { final EMap<String, String> details = ann.getDetails(); if (details.containsKey(key)) { details.removeKey(key); } if (details.isEmpty()) { element.getEAnnotations().remove(ann); } } } /** * @param element * @param key * @return The value of the element annotation with the given key, or <code>null</code>. */ public static final String getAnnotationValue(final Element element, final String key) { final EAnnotation ann = element.getEAnnotation(EANNOTATION); if (ann != null) { final EMap<String, String> details = ann.getDetails(); if (details.containsKey(key)) { return details.get(key); } } return null; } /** * Finds the {@link Comment} owned by element that contains the comment string. * The {@link Comment} is created if necessary. * @param element * @param comment * @return the {@link Comment} owned by element containing the comment string */ public static Comment getOwnedComment(Element element, String comment) { for (Comment c : element.getOwnedComments()) { if (comment.equals(c.getBody())) { return c; } } final Comment c = element.createOwnedComment(); c.setBody(comment); return c; } private FindReferredTypesSwitch findReferredTypes = new FindReferredTypesSwitch(); private Model model; private List<JarFile> jars = new ArrayList<JarFile>(); private List<IContainer> paths = new ArrayList<IContainer>(); private List<JarFile> cpJars = new ArrayList<JarFile>(); private List<IContainer> cpPaths = new ArrayList<IContainer>(); private List<JavaClass> parsedClasses = new ArrayList<JavaClass>(); private List<JavaClass> parsedCpClasses = new ArrayList<JavaClass>(); private Filter filter; private String outputFile = "api.uml"; //$NON-NLS-1$ private String outputModelName = "api"; //$NON-NLS-1$ private boolean includeInstructionReferences = false; private boolean includeFeatures = true; private boolean dependenciesOnly = false; private boolean includeComment = true; private boolean updateExistingFile; /** * Performs the actual jar to UML conversion. */ protected void runWithMonitor(final IProgressMonitor monitor) { try { assert getOutputFile() != null : JarToUMLResources.getString("JarToUML.nullOutputFile"); //$NON-NLS-1$ beginTask(monitor, String.format(JarToUMLResources.getString("JarToUML.startingFor"), getOutputFile()), WORK_TOTAL); //$NON-NLS-1$ // // 1 // subTask(monitor, JarToUMLResources.getString("JarToUML.creatingUML")); //$NON-NLS-1$ if (getModel() == null) { final ResourceSet resourceSet = createResourceSet(); final Resource res; if (isUpdateExistingFile()) { res = resourceSet.getResource(URI.createURI(getOutputFile()), true); if (res == null) { throw new IOException( String.format(JarToUMLResources.getString("JarToUML.nullRes"), getOutputFile())); //$NON-NLS-1$ } setModel((Model) UML2Util.load(resourceSet, URI.createURI(getOutputFile()), UMLPackage.eINSTANCE.getModel())); } else { res = resourceSet.createResource(URI.createURI(getOutputFile())); if (res == null) { throw new IOException( String.format(JarToUMLResources.getString("JarToUML.nullRes"), getOutputFile())); //$NON-NLS-1$ } } if (getModel() == null) { final Model newModel = UMLFactory.eINSTANCE.createModel(); res.getContents().add(newModel); newModel.setName(getOutputModelName()); setModel(newModel); } } final Model model = getModel(); assert model != null; worked(monitor, JarToUMLResources.getString("JarToUML.createdUML")); //$NON-NLS-1$ // // 2 // subTask(monitor, JarToUMLResources.getString("JarToUML.parsing")); //$NON-NLS-1$ final Filter filter = getFilter(); final ParseClasses parseClasses = new ParseClasses(filter, monitor, WORK_PARSE_CLASSES); final List<JavaClass> parsedClasses = getParsedClasses(); final List<JavaClass> parsedCpClasses = getParsedCpClasses(); parseClasses.beginTask(JarToUMLResources.getString("JarToUML.parsing"), ParseClasses.getJarWork(getJars()) + ParseClasses.getJarWork(getCpJars()) + ParseClasses.getPathWork(getPaths()) + ParseClasses.getPathWork(getCpPaths())); //$NON-NLS-1$ List<JarFile> jars = getJars(); List<IContainer> paths = getPaths(); List<JarFile> cpJars = getCpJars(); List<IContainer> cpPaths = getCpPaths(); //promote classpath entries to main entries if no main entries exist if (jars.isEmpty() && paths.isEmpty()) { jars = cpJars; paths = cpPaths; cpJars = Collections.emptyList(); cpPaths = Collections.emptyList(); } for (JarFile jar : jars) { parseClasses.parseClasses(jar, parsedClasses, parsedCpClasses); checkCancelled(monitor); } for (IContainer path : paths) { parseClasses.parseClasses(path, parsedClasses); checkCancelled(monitor); } for (JarFile jar : cpJars) { parseClasses.parseClasses(jar, parsedCpClasses, parsedCpClasses); checkCancelled(monitor); } for (IContainer path : cpPaths) { parseClasses.parseClasses(path, parsedCpClasses); checkCancelled(monitor); } worked(null, JarToUMLResources.getString("JarToUML.parsed")); //$NON-NLS-1$ // // 3 // subTask(monitor, JarToUMLResources.getString("JarToUML.addingClassifiers")); //$NON-NLS-1$ final boolean includeFeatures = isIncludeFeatures(); final boolean includeInstructionReferences = isIncludeInstructionReferences(); final AddClassifiers addClassifiers = new AddClassifiers(filter, monitor, WORK_ADD_CLASSIFIERS, model, includeFeatures, includeInstructionReferences); addClassifiers.beginTask(JarToUMLResources.getString("JarToUML.addingClassifiers"), parsedClasses.size() + parsedCpClasses.size()); //$NON-NLS-1$ addClassifiers.addAllClassifiers(parsedClasses); final List<JavaClass> skippedClasses = addClassifiers.addClassifiersClosure(parsedCpClasses); parsedCpClasses.removeAll(skippedClasses); worked(null, JarToUMLResources.getString("JarToUML.addedClassifiers")); //$NON-NLS-1$ // // 4 // subTask(monitor, JarToUMLResources.getString("JarToUML.addingProperties")); //$NON-NLS-1$ final AddProperties addProperties = new AddProperties(filter, monitor, WORK_ADD_PROPERTIES, model, includeFeatures, includeInstructionReferences); addProperties.beginTask(JarToUMLResources.getString("JarToUML.addingProperties"), parsedClasses.size() + parsedCpClasses.size()); //$NON-NLS-1$ addProperties.addAllProperties(parsedClasses); addProperties.addAllProperties(parsedCpClasses); worked(null, JarToUMLResources.getString("JarToUML.addedProperties")); //$NON-NLS-1$ // // 5 // final MarkInferredClassifiers markInferredClassifiers = new MarkInferredClassifiers(filter, monitor, WORK_INFERRED_TAGS, model); final Set<Classifier> containedClassifiers = markInferredClassifiers .findContainedClassifiers(getParsedClasses()); containedClassifiers.addAll(markInferredClassifiers.findContainedClassifiers(getParsedCpClasses())); final RemoveFromModel removeFromModel = new RemoveFromModel(filter, monitor, WORK_REMOVE_EMPTY, model); if (isDependenciesOnly()) { subTask(monitor, JarToUMLResources.getString("JarToUML.removingClassifiers")); //$NON-NLS-1$ final Set<Classifier> inferredClassifiers = markInferredClassifiers .findInferredClassifiers(containedClassifiers); final Set<Type> referredTypes = findReferredTypes.findAllReferredTypes(inferredClassifiers); final Set<Type> containerTypes = FindReferredTypesSwitch.findContainerTypes(referredTypes); // also retain container types of referred types, otherwise we still get dangling refs. referredTypes.addAll(containerTypes); final Set<Classifier> removeClassifiers = new HashSet<Classifier>(containedClassifiers); if (removeClassifiers.removeAll(referredTypes)) { containedClassifiers.retainAll(referredTypes); JarToUMLResources.logger .warning(String.format(JarToUMLResources.getString("JarToUML.cyclicDepsFound"), getNameList(containedClassifiers))); // Keep referred classifiers, but strip their properties removeFromModel.removeAllProperties(containedClassifiers); } // Remove all classifiers before tagging removeFromModel.removeAllClassifiers(removeClassifiers); // Tag contained classifiers as "inferred" by the inferred classifiers markInferredClassifiers.addAllInferredTags(inferredClassifiers); worked(monitor, JarToUMLResources.getString("JarToUML.removedClassifiers")); //$NON-NLS-1$ } else { subTask(monitor, JarToUMLResources.getString("JarToUML.addingInferred")); //$NON-NLS-1$ markInferredClassifiers.addAllInferredTags(containedClassifiers); worked(monitor, JarToUMLResources.getString("JarToUML.addedInferred")); //$NON-NLS-1$ } // // 6 // subTask(monitor, JarToUMLResources.getString("JarToUML.removingEmpty")); //$NON-NLS-1$ removeFromModel.removeEmptyPackages(model); worked(monitor, JarToUMLResources.getString("JarToUML.removedEmpty")); //$NON-NLS-1$ // // 7 // subTask(monitor, JarToUMLResources.getString("JarToUML.addingMetadata")); //$NON-NLS-1$ if (isIncludeComment()) { getOwnedComment(model, String.format(JarToUMLResources.getString("JarToUML.generatedBy"), JarToUMLPlugin.getPlugin().getBundle().getVersion(), getInputList())); //$NON-NLS-1$ } annotate(model, MAJOR_BYTECODE_FORMAT_VERSION, String.valueOf(parseClasses.getMajorFormatVersion())); //$NON-NLS-1$ annotate(model, MINOR_BYTECODE_FORMAT_VERSION, String.valueOf(parseClasses.getMinorFormatVersion())); //$NON-NLS-1$ annotate(model, PREVERIFIED, String.valueOf(addProperties.isPreverified())); //$NON-NLS-1$ worked(monitor, JarToUMLResources.getString("JarToUML.addedMetadata")); } catch (IOException e) { throw new JarToUMLException(e); } catch (CoreException e) { throw new JarToUMLException(e); } } /** * @return A comma-separated list of all Jar and Path inputs */ private String getInputList() { StringBuffer b = null; List<JarFile> jars = getJars(); List<IContainer> paths = getPaths(); //promote classpath entries to main entries if no main entries exist if (jars.isEmpty() && paths.isEmpty()) { jars = getCpJars(); paths = getCpPaths(); } for (JarFile jar : jars) { if (b == null) { b = new StringBuffer(); } else { b.append(", "); } String jarName = jar.getName(); b.append(jarName.substring(jarName.lastIndexOf('/') + 1)); } for (IContainer path : paths) { if (b == null) { b = new StringBuffer(); } else { b.append(", "); } b.append(path.getFullPath()); } if (b != null) { return b.toString(); } return ""; } /** * Adds jar to the collection of jars to process. * @param jar */ public void addJar(JarFile jar) { if (!this.jars.contains(jar)) { this.jars.add(jar); } } /** * Removes jar from the collection of jars to process. * @param jar */ public void removeJar(JarFile jar) { this.jars.remove(jar); } /** * Clears the collection of jars to process. */ public void clearJars() { this.jars.clear(); } /** * @return The collection of jars to process. */ public List<JarFile> getJars() { return jars; } /** * @return The generated UML model. */ public Model getModel() { return model; } /** * Sets the UML model to store generated elements in. * Overrides {@link #setOutputFile(String)} and {@link #setOutputModelName(String)}. * @param model */ public void setModel(Model model) { this.model = model; } /** * @return The Java element filter to apply. */ public Filter getFilter() { return filter; } /** * Sets the Java element filter to apply. * @param filter */ public void setFilter(Filter filter) { this.filter = filter; } /** * @return The output file location (understandable to EMF). */ public String getOutputFile() { return outputFile; } /** * Sets the output file location (understandable to EMF). * @param outputFile */ public void setOutputFile(String outputFile) { this.outputFile = outputFile; } /** * @return The name of the UML Model root element. */ public String getOutputModelName() { return outputModelName; } /** * Sets the name of the UML Model root element. * @param outputModelName */ public void setOutputModelName(String outputModelName) { this.outputModelName = outputModelName; } /** * @return Whether or not to include Java elements that are only * referred to by bytecode instructions. Defaults to false. */ public boolean isIncludeInstructionReferences() { return includeInstructionReferences; } /** * Sets whether or not to include Java elements that are only * referred to by bytecode instructions. Defaults to false. * @param includeInstructionReferences */ public void setIncludeInstructionReferences(boolean includeInstructionReferences) { this.includeInstructionReferences = includeInstructionReferences; } /** * Whether or not to include classifier operations and attributes. Defaults to true. * @return the includeFeatures */ public boolean isIncludeFeatures() { return includeFeatures; } /** * Whether or not to include classifier operations and attributes. Defaults to true. * @param includeFeatures the includeFeatures to set */ public void setIncludeFeatures(boolean includeFeatures) { this.includeFeatures = includeFeatures; } /** * If true, includes only the dependencies instead of the contained elements. * @return the dependenciesOnly */ public boolean isDependenciesOnly() { return dependenciesOnly; } /** * If true, includes only the dependencies instead of the contained elements. * @param dependenciesOnly the dependenciesOnly to set */ public void setDependenciesOnly(boolean dependenciesOnly) { this.dependenciesOnly = dependenciesOnly; } /** * @return the paths */ public List<IContainer> getPaths() { return paths; } /** * Empties the list of paths */ public void clearPaths() { this.paths.clear(); } /** * @param path the path to add */ public void addPath(IContainer path) { if (!this.paths.contains(path)) { this.paths.add(path); } } /** * @param path the path to remove */ public void removePath(IContainer path) { this.paths.remove(path); } /** * Adds all relevant class file paths for javaProject * @param javaProject * @param includeWorkspaceReferences Include referenced projects and jar files in workspace * @throws JavaModelException * @throws IOException */ public void addPaths(IJavaProject javaProject, boolean includeWorkspaceReferences) throws JavaModelException, IOException { for (IClasspathEntry cpe : javaProject.getResolvedClasspath(true)) { IPath cpePath; switch (cpe.getEntryKind()) { case IClasspathEntry.CPE_SOURCE: cpePath = cpe.getOutputLocation(); if (cpePath == null) { cpePath = javaProject.getOutputLocation(); } IContainer container = (IContainer) ResourcesPlugin.getWorkspace().getRoot().findMember(cpePath); addPath(container); break; case IClasspathEntry.CPE_LIBRARY: cpePath = cpe.getPath(); IResource resource = ResourcesPlugin.getWorkspace().getRoot().findMember(cpePath); if ((resource != null) && (includeWorkspaceReferences || javaProject.getProject().equals(resource.getProject()))) { if (resource instanceof IFile) { addCpJar(new JarFile(resource.getLocation().toFile())); } else if (resource instanceof IContainer) { addCpPath((IContainer) resource); } else { throw new IOException(String .format(JarToUMLResources.getString("JarToUML.unexpectedResourceKind"), resource)); //$NON-NLS-1$ } } break; } } if (includeWorkspaceReferences) { Set<IJavaProject> refs = new HashSet<IJavaProject>(); findJavaProjectReferences(javaProject, refs); for (IJavaProject ref : refs) { addPaths(ref, includeWorkspaceReferences); } } } /** * The jar files for which the class files should * only be reverse engineered as they are referenced * by the main class files. * @return the jars on the classpath */ public List<JarFile> getCpJars() { return cpJars; } /** * Adds cpJar to the classpath jars. * @param cpJar */ public void addCpJar(JarFile cpJar) { if (!this.cpJars.contains(cpJar)) { this.cpJars.add(cpJar); } } /** * Removes cpJar from the classpath jars. * @param cpJar */ public void removeCpJar(JarFile cpJar) { this.cpJars.remove(cpJar); } /** * Clears the classpath jars. */ public void clearCpJars() { this.cpJars.clear(); } /** * The paths for which the class files should * only be reverse engineered as they are referenced * by the main class files. * @return the paths on the classpath */ public List<IContainer> getCpPaths() { return cpPaths; } /** * Adds cpPath to the classpath paths. * @param cpPath */ public void addCpPath(IContainer cpPath) { if (!this.cpPaths.contains(cpPath)) { this.cpPaths.add(cpPath); } } /** * Remove cpPath from the classpath paths. * @param cpPath */ public void removeCpPath(IContainer cpPath) { this.cpPaths.remove(cpPath); } /** * Clears the classpath paths. */ public void clearCpPaths() { this.cpPaths.clear(); } /** * @return the parsed classes */ public List<JavaClass> getParsedClasses() { return parsedClasses; } /** * @return the parsed classpath classes * @see #getCpJars() * @see #getCpPaths() */ public List<JavaClass> getParsedCpClasses() { return parsedCpClasses; } /** * Whether or not to include a generator comment. Defaults to true. * @return the includeComment */ public boolean isIncludeComment() { return includeComment; } /** * Whether or not to include a generator comment. Defaults to true. * @param includeComment the includeComment to set */ public void setIncludeComment(boolean includeComment) { this.includeComment = includeComment; } /** * @return the updateExistingFile */ public boolean isUpdateExistingFile() { return updateExistingFile; } /** * @param updateExistingFile the updateExistingFile to set */ public void setUpdateExistingFile(boolean updateExistingFile) { this.updateExistingFile = updateExistingFile; } }