/******************************************************************************* * Copyright (c) 2008 IBM Corporation and others. * 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: * IBM Corporation - initial API and implementation *******************************************************************************/ package org.eclipse.wst.xsd.ui.internal.common.util; import java.io.IOException; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Set; import java.util.StringTokenizer; import org.eclipse.core.resources.IFile; import org.eclipse.core.runtime.CoreException; import org.eclipse.emf.ecore.EObject; import org.eclipse.emf.ecore.EReference; import org.eclipse.emf.ecore.EStructuralFeature.Setting; import org.eclipse.wst.sse.core.StructuredModelManager; import org.eclipse.wst.xml.core.internal.provisional.document.IDOMModel; import org.eclipse.wst.xml.core.internal.provisional.document.IDOMNode; import org.eclipse.wst.xsd.ui.internal.editor.XSDEditorPlugin; import org.eclipse.wst.xsd.ui.internal.text.XSDModelAdapter; import org.eclipse.xsd.XSDConcreteComponent; import org.eclipse.xsd.XSDImport; import org.eclipse.xsd.XSDInclude; import org.eclipse.xsd.XSDNamedComponent; import org.eclipse.xsd.XSDRedefine; import org.eclipse.xsd.XSDSchema; import org.eclipse.xsd.XSDSchemaDirective; import org.eclipse.xsd.XSDTypeDefinition; import org.eclipse.xsd.impl.XSDSchemaImpl; import org.eclipse.xsd.util.XSDConstants; import org.eclipse.xsd.util.XSDUtil; import org.w3c.dom.Document; import org.w3c.dom.Element; import org.w3c.dom.NamedNodeMap; import org.w3c.dom.Node; import org.w3c.dom.NodeList; /** * This class performs cleanup/removal of unused XSD imports and includes from XML Schemas * and xmlns entries in the namespace table */ public class XSDDirectivesManager { protected static final String XMLNS = "xmlns"; //$NON-NLS-1$ // List of all the unused directives. These will be removed protected List unusedDirectives = new ArrayList(); // List of all the included XSDSchema's protected List usedIncludeSchemas = new ArrayList(); // List of all the XSDInclude's that are indirectly used protected List usedIndirectIncludes = new ArrayList(); // List of the XSDInclude's that are directly used protected List usedDirectIncludes = new ArrayList(); // Used to track the included schemas that were analyzed to avoid circular loop protected List analyzedIncludeSchemas = new ArrayList(); // List of all the imported used schemas protected List usedSchemas = new ArrayList(); // List of all the indirectly used includes protected Map usedIndirectIncludesMap = new HashMap(); // Keep track of all the unused prefixes protected Set unusedPrefixes; // Keep track of all the used prefixes protected Set usedPrefixes; // Map of each schema's unused prefixes protected Map schemaToPrefixMap; public static void removeUnusedXSDImports(XSDSchema schema) { // Only do the removal if the preference is turned on if (XSDEditorPlugin.getDefault().getRemoveImportSetting()) { XSDDirectivesManager mgr = new XSDDirectivesManager(); mgr.performRemoval(schema); mgr.cleanup(); } } /** * Main method to do the cleanup * @param schema */ public void performRemoval(XSDSchema schema) { // Compute unused imports and unused prefixes computeUnusedImports(schema); // Remove the imports removeUnusedImports(); // Remove the prefixes removeUnusedPrefixes(); } /** * Clients can manually clean the lists */ public void cleanup() { clearMaps(); } /** * After performing the cleanup, return the list of unused XSD directives * * @return list of unused XSD directives */ public List getUnusedXSDDirectives() { return unusedDirectives; } /** * After performing the cleanup, return the map of each schemas unused prefixes * * @return map of each schemas unused prefixes */ public Map getSchemaToPrefixMap() { return schemaToPrefixMap; } /** * Returns the set of unused prefixes from the XML namespace table * * @return set of unused prefixes */ public Set getUnusedPrefixes() { return unusedPrefixes; } /** * Returns the set of used prefixes from the XML namespace table * * @return set of used prefixes */ public Set getUsedPrefixes() { return usedPrefixes; } /** * Perform any cleanup after computing and removing the unused imports. */ protected void clearMaps() { if (schemaToPrefixMap != null) { schemaToPrefixMap.clear(); } } /** * Remove the list of all unused imports. Imports is used in the generic term here. */ protected void removeUnusedImports() { Iterator iter = unusedDirectives.iterator(); while (iter.hasNext()) { XSDSchemaDirective xsdDirective = (XSDSchemaDirective) iter.next(); removeXSDDirective(xsdDirective); } } /** * Removes the directive from the model * @param xsdImport */ protected void removeXSDDirective(XSDSchemaDirective xsdImport) { XSDSchema schema = xsdImport.getSchema(); Element element = xsdImport.getElement(); Document doc = element.getOwnerDocument(); if (doc instanceof IDOMNode) ((IDOMNode)doc).getModel().aboutToChangeModel(); try { if (!removeTextNodesBetweenNextElement(element)) { removeTextNodeBetweenPreviousElement(element); } element.getParentNode().removeChild(element); } finally { if (doc instanceof IDOMNode) { ((IDOMNode)doc).getModel().changedModel(); } schema.update(true); } } /** * This computes the list of unused imports for a schema * @param schema */ protected void computeUnusedImports(XSDSchema schema) { unusedDirectives = new ArrayList(); usedSchemas = new ArrayList(); usedPrefixes = new HashSet(); schemaToPrefixMap = new HashMap(); try { // Step One. Find unused imports using cross referencer Map xsdNamedComponentUsage = TopLevelComponentCrossReferencer.find(schema); doCrossReferencer(schema, usedSchemas, xsdNamedComponentUsage); // Step Two. Update the unusedImport list given the list of used schemas obtained from cross referencing addToUnusedImports(schema, usedSchemas); // Step Three. Compute unused prefixes to be removed computeUnusedPrefixes(schema); } catch (Exception e) { unusedDirectives.clear(); schemaToPrefixMap.clear(); } } /** * Computes the list of unused prefixes from the XML namespace table given a schema * @param schema */ protected void computeUnusedPrefixes(XSDSchema schema) { Map prefixMap = schema.getQNamePrefixToNamespaceMap(); Set definedPrefixes = prefixMap.keySet(); Set actualSet = new HashSet(); NamedNodeMap attributes = schema.getElement().getAttributes(); Iterator iter = definedPrefixes.iterator(); while (iter.hasNext()) { String pref = (String)iter.next(); if (pref == null) { if (attributes.getNamedItem(XMLNS) != null) actualSet.add(null); } else { if (attributes.getNamedItem(XMLNS + ":" + pref) != null) //$NON-NLS-1$ actualSet.add(pref); } } unusedPrefixes = new HashSet(actualSet); usedPrefixes.add(schema.getSchemaForSchemaQNamePrefix()); Element element = schema.getElement(); NodeList childElements = element.getChildNodes(); int length = childElements.getLength(); for (int i = 0; i < length; i++) { Node node = childElements.item(i); if (node instanceof Element) { traverseDOMElement((Element)node, schema); } } // compute the used prefixes computeUsedXSDPrefixes(schema); // remove the used prefixes from the unused to get the list of unused prefixes unusedPrefixes.removeAll(usedPrefixes); // perform additional process from extenders doAdditionalProcessing(schema); schemaToPrefixMap.put(schema, unusedPrefixes); } /** * Remove unused prefixes from the XML namespace table */ protected void removeUnusedPrefixes() { Set schemaSet = schemaToPrefixMap.keySet(); Iterator iter = schemaSet.iterator(); while (iter.hasNext()) { XSDSchema schema = (XSDSchema)iter.next(); Map prefixMap = schema.getQNamePrefixToNamespaceMap(); Set prefixesToRemove = (Set)schemaToPrefixMap.get(schema); Iterator iter2 = prefixesToRemove.iterator(); while (iter2.hasNext()) { String string = (String)iter2.next(); if (prefixMap.containsKey(string)) prefixMap.remove(string); } } } /** * Extenders can customize * @param schema */ protected void doAdditionalProcessing(XSDSchema schema) { // Do nothing for XSD } /** * * @param schema */ private void computeUsedXSDPrefixes(XSDSchema schema) { Map prefixMap = schema.getQNamePrefixToNamespaceMap(); Set definedPrefixes = prefixMap.keySet(); boolean foundEntryForTargetNamespace = false; String targetNamespace = schema.getTargetNamespace(); for (Iterator iter = usedPrefixes.iterator(); iter.hasNext(); ) { String key = (String) iter.next(); String value = (String) prefixMap.get(key); if (targetNamespace == null && value == null) { foundEntryForTargetNamespace = true; break; } else if (targetNamespace != null && value != null) { if (targetNamespace.equals(value)) { foundEntryForTargetNamespace = true; break; } } } if (!foundEntryForTargetNamespace) { for (Iterator iter = definedPrefixes.iterator(); iter.hasNext();) { String key = (String) iter.next(); String value = (String) prefixMap.get(key); if (targetNamespace == null && value == null) { usedPrefixes.add(null); break; } else if (targetNamespace != null && value != null) { if (targetNamespace.equals(value)) { usedPrefixes.add(key); break; } } } } } /** * Find prefixes that are in the document. * @param element * @param schema */ private void traverseDOMElement(Element element, XSDSchema schema) { String prefix = element.getPrefix(); usedPrefixes.add(prefix); NamedNodeMap attrs = element.getAttributes(); int numOfAttrs = attrs.getLength(); for (int i = 0; i < numOfAttrs; i++) { Node node = attrs.item(i); String attrPrefix = node.getPrefix(); if (attrPrefix != null) { usedPrefixes.add(attrPrefix); } String attr = node.getLocalName(); if (attr != null) { String value = node.getNodeValue(); if (value == null) continue; if (attr.equals(XSDConstants.REF_ATTRIBUTE) || attr.equals(XSDConstants.REFER_ATTRIBUTE) || attr.equals(XSDConstants.TYPE_ATTRIBUTE) || attr.equals(XSDConstants.BASE_ATTRIBUTE) || attr.equals(XSDConstants.SUBSTITUTIONGROUP_ATTRIBUTE) || attr.equals(XSDConstants.ITEMTYPE_ATTRIBUTE)) { try { usedPrefixes.add(extractPrefix(value)); } catch (IndexOutOfBoundsException e) { } } else if (attr.equals(XSDConstants.MEMBERTYPES_ATTRIBUTE)) { StringTokenizer tokenizer = new StringTokenizer(value); while (tokenizer.hasMoreTokens()) { try { String token = tokenizer.nextToken(); usedPrefixes.add(extractPrefix(token)); } catch (IndexOutOfBoundsException e) { } } } } } NodeList childElements = element.getChildNodes(); int length = childElements.getLength(); for (int i = 0; i < length; i++) { Node node = childElements.item(i); if (node instanceof Element) { traverseDOMElement((Element)node, schema); } } } /** * Extract the prefix from the given string. For example, pref:attr returns pref. * @param value * @return the prefix */ protected String extractPrefix(String value) { int index = value.indexOf(':'); if (index < 0) return null; else return value.substring(0, index); } /** * This determines the list of referenced components and hence the used schemas from which * we can determine what are the unused directives * * @param schema * @param unusedImportList * @param usedSchemas */ protected void doCrossReferencer(XSDSchema schema, List usedSchemas, Map xsdNamedComponentUsage) { // Calculate additional unused imports that may have the same // namespace that did not get added in the initial pass Iterator iterator = xsdNamedComponentUsage.keySet().iterator(); // First determine the used schemas from the cross referencer while (iterator.hasNext()) { XSDNamedComponent namedComponent = (XSDNamedComponent) iterator.next(); XSDSchema namedComponentSchema = namedComponent.getSchema(); // If the named component belongs to the same schema, then continue...we // want to check the external references if (namedComponentSchema == schema) { continue; } Collection collection = (Collection) xsdNamedComponentUsage.get(namedComponent); Iterator iterator2 = collection.iterator(); while (iterator2.hasNext()) { Setting setting = (Setting) iterator2.next(); Object obj = setting.getEObject(); if (isComponentUsed(obj, schema, namedComponentSchema)) { if (!usedSchemas.contains(namedComponentSchema)) usedSchemas.add(namedComponentSchema); } } } } /** * Determines if the object to be analyzed is referenced by the schema * @param obj * @param schema * @param targetSchema * @return true if the component is referenced by the schema, false if not referenced */ protected boolean isComponentUsed(Object obj, XSDSchema schema, XSDSchema targetSchema) { if (obj instanceof XSDConcreteComponent) { XSDConcreteComponent component = (XSDConcreteComponent) obj; if (component == schema || component instanceof XSDSchema) { return false; } if (!usedIncludeSchemas.contains(targetSchema)) usedIncludeSchemas.add(targetSchema); return true; } return false; } /** * From a list of used schemas, update the unusedImports list for the given schema * * @param schema * @param unusedImports * @param usedSchemas */ protected void addToUnusedImports(XSDSchema schema, List usedSchemas) { // now that we have the list of usedSchemas, get the list of unused // schemas by comparing this list to what is actually in the schema Iterator iter = schema.getContents().iterator(); while(iter.hasNext()) { Object o = iter.next(); if (o instanceof XSDSchemaDirective) { XSDSchemaDirective directive = (XSDSchemaDirective) o; boolean isUsed = false; Iterator iter2 = usedSchemas.iterator(); while (iter2.hasNext()) { XSDSchema usedSchema = (XSDSchema) iter2.next(); if (directive instanceof XSDImport && directive.getResolvedSchema() == usedSchema) { isUsed = true; break; } if (directive instanceof XSDInclude && ((XSDInclude)directive).getIncorporatedSchema() == usedSchema) { isUsed = true; usedDirectIncludes.add(usedSchema); break; } // blindly accept redefines as used if (directive instanceof XSDRedefine) { isUsed = true; break; } } // If it is an include, we need to check if it is used indirectly if (directive instanceof XSDInclude && !isUsed) { XSDInclude inc = (XSDInclude)directive; XSDSchema incSchema = inc.getIncorporatedSchema(); if (incSchema != null) { XSDSchema usedSchema = getUsedIncludeSchema(incSchema, inc); if (usedSchema != null) { usedIndirectIncludes.add(directive); usedIndirectIncludesMap.put(directive, usedSchema); isUsed = true; } else { isUsed = false; } } } // If resolved directives are determined unused // If resolved directives are not already in the unused list // Also any redefines should be considered used if (!isUsed && !unusedDirectives.contains(directive) && !(directive instanceof XSDRedefine)) { unusedDirectives.add(directive); } } } Iterator iter3 = usedIndirectIncludes.iterator(); while (iter3.hasNext()) { Object o = iter3.next(); if (o instanceof XSDInclude) { XSDInclude inc = (XSDInclude)o; XSDSchema targetSchema = (XSDSchema)usedIndirectIncludesMap.get(inc); if (usedIncludeSchemas.contains(targetSchema) && usedDirectIncludes.contains(targetSchema)) { unusedDirectives.add(inc); } else { usedDirectIncludes.add(targetSchema); } } } } /** * Includes can be used indirectly. If the schema includes A which includes B, but the schema * references something in B, then A is indirectly used, and hence A cannot be removed. * * @param schema * @param xsdInclude * @return the referenced schema if used, null if not used */ private XSDSchema getUsedIncludeSchema(XSDSchema schema, XSDInclude xsdInclude) { XSDSchema refSchema = null; boolean isUsed = false; Iterator iter = schema.getContents().iterator(); while (iter.hasNext()) { Object o = iter.next(); if (o instanceof XSDInclude) { XSDInclude inc = (XSDInclude)o; XSDSchema incSchema = inc.getIncorporatedSchema(); if (incSchema != null) { Iterator iter2 = usedIncludeSchemas.iterator(); while (iter2.hasNext()) { XSDSchema xsdSch = (XSDSchema)iter2.next(); if (incSchema == xsdSch) { isUsed = true; refSchema = incSchema; break; } } if (!isUsed) { if (!analyzedIncludeSchemas.contains(incSchema)) // To prevent infinite cycle { analyzedIncludeSchemas.add(incSchema); refSchema = getUsedIncludeSchema(incSchema, inc); } } if (isUsed || refSchema != null) { return refSchema; } } } else { break; } } return refSchema; } /** * See cross reference for more details. */ protected static class TopLevelComponentCrossReferencer extends XSDUtil.XSDNamedComponentCrossReferencer { private static final long serialVersionUID = 1L; XSDSchema schemaForSchema = XSDUtil.getSchemaForSchema(XSDConstants.SCHEMA_FOR_SCHEMA_URI_2001); XSDSchema schemaForXSI = XSDSchemaImpl.getSchemaInstance(XSDConstants.SCHEMA_INSTANCE_URI_2001); protected TopLevelComponentCrossReferencer(EObject arg0) { super(arg0); } /** * Returns a map of all XSDNamedComponent cross references in the content * tree. */ public static Map find(EObject eObject) { TopLevelComponentCrossReferencer result = new TopLevelComponentCrossReferencer(eObject); result.crossReference(); result.done(); return result; } protected boolean crossReference(EObject eObject, EReference eReference, EObject crossReferencedEObject) { if (crossReferencedEObject instanceof XSDNamedComponent) { XSDNamedComponent namedComponent = (XSDNamedComponent) crossReferencedEObject; if (namedComponent.getContainer() == schemaForSchema || namedComponent.getContainer() == schemaForXSI || crossReferencedEObject.eContainer() == eObject || namedComponent.getName() == null) { return false; } if (namedComponent instanceof XSDTypeDefinition) { XSDTypeDefinition typeDefinition = (XSDTypeDefinition) namedComponent; if (!(typeDefinition.getContainer() instanceof XSDSchema)) { return false; } if (typeDefinition.getName() == null) { return false; } } return true; } return false; } } /** * Helper method to remove Text nodes * @param element * @return */ protected boolean removeTextNodesBetweenNextElement(Element element) { List nodesToRemove = new ArrayList(); for (Node node = element.getNextSibling(); node != null; node = node.getNextSibling()) { if (node.getNodeType() == Node.TEXT_NODE) { nodesToRemove.add(node); } else if (node.getNodeType() == Node.ELEMENT_NODE) { for (Iterator j = nodesToRemove.iterator(); j.hasNext();) { Node nodeToRemove = (Node) j.next(); nodeToRemove.getParentNode().removeChild(nodeToRemove); } return true; } } return false; } /** * Helper method to remove Text nodes. * @param element * @return */ protected boolean removeTextNodeBetweenPreviousElement(Element element) { List nodesToRemove = new ArrayList(); for (Node node = element.getPreviousSibling(); node != null; node = node.getPreviousSibling()) { if (node.getNodeType() == Node.TEXT_NODE) { nodesToRemove.add(node); } else if (node.getNodeType() == Node.ELEMENT_NODE) { for (Iterator j = nodesToRemove.iterator(); j.hasNext();) { Node nodeToRemove = (Node) j.next(); nodeToRemove.getParentNode().removeChild(nodeToRemove); } return true; } } return false; } /** * * @param iFile * @param checkPreference - if false, ignore checking the preference setting * @throws CoreException * @throws IOException */ public static void removeUnusedXSDImports(IFile iFile, boolean checkPreference) throws CoreException, IOException { if (!checkPreference || XSDEditorPlugin.getDefault().getRemoveImportSetting()) { IDOMModel model = (IDOMModel) StructuredModelManager.getModelManager().getModelForEdit(iFile); if (model != null) { Document document = model.getDocument(); if (document != null) { XSDSchema schema = XSDModelAdapter.lookupOrCreateSchema(document); XSDDirectivesManager mgr = new XSDDirectivesManager(); mgr.performRemoval(schema); mgr.cleanup(); model.save(); } } } } }