blob: 39a1d8b6c9bac80841b1f2fac95fbae51c366492 [file] [log] [blame]
/*******************************************************************************
* 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();
}
}
}
}
}