/*******************************************************************************
 * Copyright (c) 2004, 2006 Richard Hoefter 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:
 *     Richard Hoefter (richard.hoefter@web.de) - initial API and implementation, bug 95300, bug 95297, bug 128104 
 *     IBM Corporation - nlsing and incorporating into Eclipse. 
 *                          Class created from combination of all utility classes of contribution
 *******************************************************************************/

package org.eclipse.ant.internal.ui.datatransfer;

import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
import java.io.StringWriter;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Comparator;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import java.util.TreeSet;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.Result;
import javax.xml.transform.Source;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerConfigurationException;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.TransformerFactoryConfigurationError;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;

import org.eclipse.ant.internal.ui.AntUIPlugin;
import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IFileModificationValidator;
import org.eclipse.core.resources.IMarker;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.resources.IWorkspace;
import org.eclipse.core.resources.IWorkspaceRoot;
import org.eclipse.core.resources.ResourcesPlugin;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IAdaptable;
import org.eclipse.core.runtime.IConfigurationElement;
import org.eclipse.core.runtime.IExtension;
import org.eclipse.core.runtime.IExtensionPoint;
import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Path;
import org.eclipse.core.runtime.Platform;
import org.eclipse.core.runtime.Status;
import org.eclipse.core.variables.VariablesPlugin;
import org.eclipse.jdt.core.Flags;
import org.eclipse.jdt.core.IClassFile;
import org.eclipse.jdt.core.IClasspathEntry;
import org.eclipse.jdt.core.ICompilationUnit;
import org.eclipse.jdt.core.IJavaElement;
import org.eclipse.jdt.core.IJavaModel;
import org.eclipse.jdt.core.IJavaModelMarker;
import org.eclipse.jdt.core.IJavaProject;
import org.eclipse.jdt.core.IMethod;
import org.eclipse.jdt.core.IPackageFragmentRoot;
import org.eclipse.jdt.core.IRegion;
import org.eclipse.jdt.core.ISourceReference;
import org.eclipse.jdt.core.IType;
import org.eclipse.jdt.core.JavaCore;
import org.eclipse.jdt.core.JavaModelException;
import org.eclipse.jdt.core.Signature;
import org.eclipse.jdt.core.search.IJavaSearchConstants;
import org.eclipse.jdt.core.search.IJavaSearchScope;
import org.eclipse.jdt.core.search.SearchEngine;
import org.eclipse.jdt.core.search.SearchMatch;
import org.eclipse.jdt.core.search.SearchParticipant;
import org.eclipse.jdt.core.search.SearchPattern;
import org.eclipse.jdt.core.search.SearchRequestor;
import org.eclipse.jdt.launching.IJavaLaunchConfigurationConstants;
import org.eclipse.jface.dialogs.MessageDialog;
import org.eclipse.jface.viewers.ISelection;
import org.eclipse.jface.viewers.IStructuredSelection;
import org.eclipse.swt.widgets.Shell;
import org.eclipse.team.core.RepositoryProvider;
import org.eclipse.ui.IFileEditorInput;
import org.w3c.dom.Document;
import org.xml.sax.SAXException;

/**
 * Eclipse API shortcuts.
 */
public class ExportUtil
{
    private ExportUtil()
    {
    }

    /**
     * Get resource from selection.
     */
    public static IResource getResource(ISelection selection)
    {
        if (selection instanceof IStructuredSelection)
        {
            for (Iterator iter = ((IStructuredSelection) selection).iterator(); iter.hasNext();)
            {
                IAdaptable adaptable = (IAdaptable) iter.next();
                return (IResource) adaptable.getAdapter(IResource.class);
            }
        }
        return null;
    }

    /**
     * Get Java project from resource.
     */
    public static IJavaProject getJavaProjectByName(String name)
    {
        IWorkspaceRoot rootWorkspace = ResourcesPlugin.getWorkspace().getRoot();
        IJavaModel javaModel = JavaCore.create(rootWorkspace);
        IJavaProject[] javaProjects;
        try
        {
            javaProjects = javaModel.getJavaProjects();
        }
        catch (JavaModelException e)
        {
            return null;
        }
        for (int i = 0; i < javaProjects.length; i++)
        {
            IJavaProject javaProject = javaProjects[i];
            if (name.equals(javaProject.getProject().getName()))
            {
                return javaProject;
            }
        }      
        return null;
    }

    /**
     * Get project root for given project.
     */
    public static String getProjectRoot(IJavaProject project)
    {
        return project.getResource().getLocation().toString();
    }

    /**
     * Convert Eclipse path to absolute filename. 
     * @param file      Project root optionally followed by resource name.
     *                  An absolute path is simply converted to a string.
     * @return full qualified path
     */
    public static String resolve(IPath file)
    {
        if (file == null)
        {
            return null;
        }
        try
        {
            IFile f = ResourcesPlugin.getWorkspace().getRoot().getFile(file);
            IPath p = f.getLocation();
            return (p != null) ? p.toString() : f.toString();
        }
        catch (IllegalArgumentException e)
        {
            // resource is missing
            String projectName = ExportUtil.removePrefix(file.toString(), "/"); //$NON-NLS-1$
            IJavaProject project = ExportUtil.getJavaProjectByName(projectName);
            return ExportUtil.getProjectRoot(project);
        }
    }

    /**
     * Get Java project for given root.
     */
    public static IJavaProject getJavaProject(String root)
    {
        IWorkspaceRoot rootWorkspace = ResourcesPlugin.getWorkspace().getRoot();
        IJavaModel javaModel = JavaCore.create(rootWorkspace);
        IJavaProject[] javaProjects;
        try
        {
            javaProjects = javaModel.getJavaProjects();
        }
        catch (JavaModelException e)
        {
            return null;
        }
        for (int i = 0; i < javaProjects.length; i++)
        {
            IJavaProject javaProject = javaProjects[i];
            if (root.equals(javaProject.getPath().toString()))
            {
                return javaProject;
            }
        }      
        return null;
    }

    /**
     * Remove project root from given project file.
     */
    public static String removeProjectRoot(String file, IProject project)
    {
        String res = removePrefix(file, '/' + project.getName() + '/');
        if (res.equals('/' + project.getName()))
        {
            return "."; //$NON-NLS-1$
        }
        return res;
    }

    /**
     * Remove project root from given project file.
     * @param newProjectRoot    replace project root, e.g. with a variable ${project.location}
     */
    public static String replaceProjectRoot(String file, IProject project, String newProjectRoot)
    {
        String res = removeProjectRoot(file, project);
        if (res.equals(".")) //$NON-NLS-1$
        {
            return newProjectRoot;
        }
        if (newProjectRoot == null)
        {
            return res;
        }
        if (!res.equals(file))
        {
            return newProjectRoot + '/' + res;
        }
        return res;
    }
    
    /**
     * Get for given project all directly dependent projects.
     * 
     * @return set of IJavaProject objects
     */
    public static List getClasspathProjects(IJavaProject project) throws JavaModelException
    {
        List projects = new ArrayList();
        IClasspathEntry entries[] = project.getRawClasspath();
        addClasspathProjects(projects, entries);
        return sortProjectsUsingBuildOrder(projects);
    }
    
    private static void addClasspathProjects(List projects, IClasspathEntry[] entries) {
        for (int i = 0; i < entries.length; i++)
        {
            IClasspathEntry classpathEntry = entries[i];
            if (classpathEntry.getContentKind() == IPackageFragmentRoot.K_SOURCE &&
                classpathEntry.getEntryKind() == IClasspathEntry.CPE_PROJECT) {
                    // found required project on build path
                    String subProjectRoot = classpathEntry.getPath().toString();
                    IJavaProject subProject = ExportUtil.getJavaProject(subProjectRoot);
                    // is project available in workspace
                    if (subProject != null)
                    {
                        projects.add(subProject);
                    }
            }
        }
    }
    
    /**
     * Get for given project all directly and indirectly dependent projects.
     * 
     * @return set of IJavaProject objects
     */
    public static List getClasspathProjectsRecursive(IJavaProject project) throws JavaModelException
    {
        LinkedList result = new LinkedList();
        getClasspathProjectsRecursive(project, result);
        return sortProjectsUsingBuildOrder(result);
    }
    
    private static void getClasspathProjectsRecursive(IJavaProject project, LinkedList result) throws JavaModelException
    {
        List projects = getClasspathProjects(project);
        for (Iterator iter = projects.iterator(); iter.hasNext();)
        {
            IJavaProject javaProject = (IJavaProject) iter.next();
            if (!result.contains(javaProject)) {
                result.addFirst(javaProject);
                getClasspathProjectsRecursive(javaProject, result); // recursion
            }
        }
    }
    
    /**
     * Sort projects according to General -&gt; Workspace -&gt; Build Order.
     * @param projects list of IJavaProject objects
     * @return list of IJavaProject objects with new order
     */
    private static List sortProjectsUsingBuildOrder(List javaProjects) {
    	if (javaProjects.isEmpty()) {
    		return javaProjects;
    	}
        List result = new ArrayList(javaProjects.size());
        IWorkspace workspace = ResourcesPlugin.getWorkspace();
        String[] buildOrder= workspace.getDescription().getBuildOrder();
        if (buildOrder == null) {//default build order
        	IProject[] projects= new IProject[javaProjects.size()];
        	int i= 0;
        	for (Iterator iter = javaProjects.iterator(); iter.hasNext(); i++) {
        		IJavaProject javaProject = (IJavaProject) iter.next();
        		projects[i]= javaProject.getProject();             
        	}
        	IWorkspace.ProjectOrder po = ResourcesPlugin.getWorkspace().computeProjectOrder(projects);
        	projects=po.projects;
        	buildOrder= new String[projects.length];
        	for (i = 0; i < projects.length; i++) {
				buildOrder[i]= projects[i].getName();
			}
        }
        
        for (int i = 0; i < buildOrder.length && !javaProjects.isEmpty(); i++) {
        	String projectName = buildOrder[i];            
        	for (Iterator iter = javaProjects.iterator(); iter.hasNext();) {
        		IJavaProject javaProject = (IJavaProject) iter.next();
        		if (javaProject.getProject().getName().equals(projectName)) {
        			result.add(javaProject);
        			iter.remove();
        		}                
        	}
        }
        //add any remaining projects not specified in the build order
        result.addAll(javaProjects);
        return result;
    }
    
    /**
     * Check if given project has a cyclic dependency.
     * 
     * <p>See org.eclipse.jdt.core.tests.model.ClasspathTests.numberOfCycleMarkers.
     */
    public static boolean hasCyclicDependency(IJavaProject javaProject)
            throws CoreException
    {
        IMarker[] markers = javaProject.getProject().findMarkers(
                IJavaModelMarker.BUILDPATH_PROBLEM_MARKER, false,
                IResource.DEPTH_ONE);
        for (int i = 0; i < markers.length; i++)
        {
            IMarker marker = markers[i];
            String cycleAttr = (String) marker.getAttribute(IJavaModelMarker.CYCLE_DETECTED);
            if (cycleAttr != null && cycleAttr.equals("true")) //$NON-NLS-1$
            {
                return true;
            }
        }
        return false;
    }

    /**
     * Find JUnit tests. Same tests are also returned by Eclipse run configuration wizard.
     * 
     * <p>NOTE: Copied from <code>org.eclipse.jdt.internal.junit.launcher.JUnitBaseLaunchConfiguration#findTestsInContainer}</code>
     * to use non-api functionality
     *  
     * @param containerHandle    project, package or source folder
     */
    public static IType[] findTestsInContainer(String containerHandle)
    {
        IJavaElement container= JavaCore.create(containerHandle);
        if (container == null) {
            return new IType[0];
        }
        final Object[] elements = new Object[] { container };
        final Set result= new HashSet();            
        if (elements.length > 0) {
            doFindTests(elements, result);
        }
        return (IType[]) result.toArray(new IType[result.size()]) ;
    }
    
    // copied from org.eclipse.jdt.internal.junit.util.TestSearchEngine
    // to avoid dependency to other component
    private static class JUnitSearchResultCollector extends SearchRequestor {
        List fList;
        Set fFailed = new HashSet();
        Set fMatches = new HashSet();
        
        public JUnitSearchResultCollector(List list) {
            fList = list;
        }
        
        public void acceptSearchMatch(SearchMatch match) throws CoreException {
            Object enclosingElement= match.getElement();
            if (!(enclosingElement instanceof IMethod)) 
                return;
            
            IMethod method= (IMethod)enclosingElement;      
            
            IType declaringType= method.getDeclaringType();
            if (fMatches.contains(declaringType) || fFailed.contains(declaringType))
                return;
            if (!hasSuiteMethod(declaringType) && !isTestType(declaringType)) {
                fFailed.add(declaringType);
                return;
            }
            fMatches.add(declaringType);
        }
        
        public void endReporting() {
            fList.addAll(fMatches);
        }
    }
    
    // copied from org.eclipse.jdt.internal.junit.util.TestSearchEngine
    private static List searchMethod(final IJavaSearchScope scope) throws CoreException {
        final List typesFound= new ArrayList(200);  
        searchMethod(typesFound, scope);
        return typesFound;  
    }

    // copied from org.eclipse.jdt.internal.junit.util.TestSearchEngine
    private static List searchMethod(final List v, IJavaSearchScope scope) throws CoreException {      
        SearchRequestor requestor= new JUnitSearchResultCollector(v);
        int matchRule = SearchPattern.R_EXACT_MATCH | SearchPattern.R_CASE_SENSITIVE | SearchPattern.R_ERASURE_MATCH;
        SearchPattern suitePattern= SearchPattern.createPattern("suite() Test", IJavaSearchConstants.METHOD, IJavaSearchConstants.DECLARATIONS, matchRule); //$NON-NLS-1$
        SearchParticipant[] participants= new SearchParticipant[] {SearchEngine.getDefaultSearchParticipant()};
        new SearchEngine().search(suitePattern, participants, scope, requestor, null); 
        return v;
    }

    // copied from org.eclipse.jdt.internal.junit.util.TestSearchEngine
    private static void doFindTests(Object[] elements, Set result) {
        int nElements= elements.length;
        for (int i = 0; i < nElements; i++) {
            try {
                collectTypes(elements[i], result);
            } catch (CoreException e) {
                AntUIPlugin.log(e.getStatus());
            }
        }
    }

    // copied from org.eclipse.jdt.internal.junit.util.TestSearchEngine
    private static void collectTypes(Object element, Set result) throws CoreException/*, InvocationTargetException*/ {
        element = computeScope(element);
        while ((element instanceof ISourceReference) && !(element instanceof ICompilationUnit)) {
            if (element instanceof IType) {
                if (hasSuiteMethod((IType)element) || isTestType((IType)element)) {
                    result.add(element);
                    return;
                }
            }
            element = ((IJavaElement)element).getParent();
        }
        if (element instanceof ICompilationUnit) {
            ICompilationUnit cu= (ICompilationUnit)element;
            IType[] types= cu.getAllTypes();

            for (int i= 0; i < types.length; i++) {
                if (hasSuiteMethod(types[i])  || isTestType(types[i]))
                    result.add(types[i]);
            }
        } 
        else if (element instanceof IJavaElement) {
            List testCases= findTestCases((IJavaElement)element);
            List suiteMethods= searchSuiteMethods((IJavaElement)element);            
            while (!suiteMethods.isEmpty()) {
                if (!testCases.contains(suiteMethods.get(0))) {
                    testCases.add(suiteMethods.get(0));
                }
                suiteMethods.remove(0);
            }
            result.addAll(testCases);
        }
    }

    // copied from org.eclipse.jdt.internal.junit.util.TestSearchEngine
    private static List findTestCases(IJavaElement element) throws JavaModelException {
        List found = new ArrayList();
        IJavaProject javaProject= element.getJavaProject();

        if (javaProject == null) // bug 120804
            return found; 

        IType testCaseType = testCaseType(javaProject); 
        if (testCaseType == null)
            return found;
        
        IType[] subtypes= javaProject.newTypeHierarchy(testCaseType, getRegion(javaProject), null).getAllSubtypes(testCaseType);
            
        if (subtypes == null)
            throw new JavaModelException(new CoreException(new Status(IStatus.ERROR, AntUIPlugin.PI_ANTUI, IJavaLaunchConfigurationConstants.ERR_UNSPECIFIED_MAIN_TYPE, null/*JUnitMessages.JUnitBaseLaunchConfiguration_error_notests*/, null))); 

        for (int i = 0; i < subtypes.length; i++) {
            try {
                if (element.equals(subtypes[i].getAncestor(element.getElementType())) && hasValidModifiers(subtypes[i]))
                    found.add(subtypes[i]);
            } catch (JavaModelException e) {
                AntUIPlugin.log(e.getStatus());
            }
        }
        return found;
    }

    // copied from org.eclipse.jdt.internal.junit.util.TestSearchEngine
    private static IType testCaseType(IJavaProject javaProject) {
        try {
            return javaProject.findType("junit.framework.TestCase"); //$NON-NLS-1$
        } catch (JavaModelException e) {
            AntUIPlugin.log(e.getStatus());
            return null;
        } 
    }

    // copied from org.eclipse.jdt.internal.junit.util.TestSearchEngine
    private static IRegion getRegion(IJavaProject javaProject) throws JavaModelException{
        IRegion region = JavaCore.newRegion();
        IJavaElement[] elements= javaProject.getChildren();
        for(int i=0; i<elements.length; i++) {
            if (((IPackageFragmentRoot)elements[i]).isArchive())
                continue;
            region.add(elements[i]);
        }
        return region;
    }

    // copied from org.eclipse.jdt.internal.junit.util.TestSearchEngine
    private static Object computeScope(Object element) throws JavaModelException {
        if (element instanceof IFileEditorInput)
            element= ((IFileEditorInput)element).getFile();
        if (element instanceof IResource)
            element= JavaCore.create((IResource)element);
        if (element instanceof IClassFile) {
            IClassFile cf= (IClassFile)element;
            element= cf.getType();
        }
        return element;
    }

    // copied from org.eclipse.jdt.internal.junit.util.TestSearchEngine
    private static List searchSuiteMethods(IJavaElement element) throws CoreException {    
        // fix for bug 36449  JUnit should constrain tests to selected project [JUnit]
        IJavaSearchScope scope= SearchEngine.createJavaSearchScope(new IJavaElement[] { element },
                IJavaSearchScope.SOURCES);
        return searchMethod(scope);
    }

    // copied from org.eclipse.jdt.internal.junit.util.TestSearchEngine
    private static boolean hasSuiteMethod(IType type) throws JavaModelException {
        IMethod method= type.getMethod("suite", new String[0]); //$NON-NLS-1$
        if (method == null || !method.exists()) 
            return false;
        
        if (!Flags.isStatic(method.getFlags()) ||   
            !Flags.isPublic(method.getFlags()) ||           
            !Flags.isPublic(method.getDeclaringType().getFlags())) { 
            return false;
        }
        if (!Signature.getSimpleName(Signature.toString(method.getReturnType())).equals("Test" /*JUnitPlugin.SIMPLE_TEST_INTERFACE_NAME*/)) { //$NON-NLS-1$
            return false;
        }
        return true;
    }

    // copied from org.eclipse.jdt.internal.junit.util.TestSearchEngine
    private static boolean isTestType(IType type) throws JavaModelException {
        if (!hasValidModifiers(type))
            return false;
        
        IType[] interfaces= type.newSupertypeHierarchy(null).getAllSuperInterfaces(type);
        for (int i= 0; i < interfaces.length; i++)
            if(interfaces[i].getFullyQualifiedName('.').equals("junit.framework.Test" /*JUnitPlugin.TEST_INTERFACE_NAME*/)) //$NON-NLS-1$
                return true;
        return false;
    }

    // copied from org.eclipse.jdt.internal.junit.util.TestSearchEngine
    private static boolean hasValidModifiers(IType type) throws JavaModelException {
        if (Flags.isAbstract(type.getFlags())) 
            return false;
        if (!Flags.isPublic(type.getFlags())) 
            return false;
        return true;
    }

    /**
     * Compares projects by project name.
     */
    public static synchronized Comparator getJavaProjectComparator()
    {
        if (javaProjectComparator == null)
        {
            javaProjectComparator = new JavaProjectComparator();
        }
        return javaProjectComparator;
    }
    
    private static Comparator javaProjectComparator;
    
    private static class JavaProjectComparator implements Comparator
    {
        public int compare(Object o1, Object o2)
        {
            IJavaProject j1 = (IJavaProject) o1;
            IJavaProject j2 = (IJavaProject) o2;
            return j1.getProject().getName().compareTo(j2.getProject().getName());
        }
        
        public boolean equals(Object obj)
        {
            if (obj == null)
            {
                return false;
            }
            return compare(this, obj) == 0;
        }
    }
    
    /**
     * Compares objects by classname.
     */
    public static synchronized Comparator getClassnameComparator()
    {
        if (classnameComparator == null)
        {
            classnameComparator = new ClassnameComparator();
        }
        return classnameComparator;
    }
    
    private static Comparator classnameComparator;
    
    private static class ClassnameComparator implements Comparator
    {
        public int compare(Object o1, Object o2)
        {
            return o1.getClass().getName().compareTo(o2.getClass().getName());
        }

        public boolean equals(Object obj)
        {
            if (obj == null)
            {
                return false;
            }
            return compare(this, obj) == 0;
        }
    }

    /**
     * Compares IFile objects.
     */
    public static synchronized Comparator getIFileComparator()
    {
        if (fileComparator == null)
        {
            fileComparator = new IFileComparator();
        }
        return fileComparator;
    }
    
    private static Comparator fileComparator;
    
    private static class IFileComparator implements Comparator
    {
        public int compare(Object o1, Object o2)
        {
            IFile f1 = (IFile) o1;
            IFile f2 = (IFile) o2;
            return f1.toString().compareTo(f2.toString());
        }

        public boolean equals(Object obj)
        {
            if (obj == null)
            {
                return false;
            }
            return compare(this, obj) == 0;
        }
    }
    
    /**
     * Compares IType objects.
     */
    public static synchronized Comparator getITypeComparator()
    {
        if (typeComparator == null)
        {
            typeComparator = new TypeComparator();
        }
        return typeComparator;
    }
    
    private static Comparator typeComparator;
    
    private static class TypeComparator implements Comparator
    {
        public int compare(Object o1, Object o2)
        {
            IType t1 = (IType) o1;
            IType t2 = (IType) o2;
            return t1.getFullyQualifiedName().compareTo(t2.getFullyQualifiedName());
        }

        public boolean equals(Object obj)
        {
            if (obj == null)
            {
                return false;
            }
            return compare(this, obj) == 0;
        }
    }

    /**
     * Platform specific newline character(s).
     */
    public static final String NEWLINE = System.getProperty("line.separator"); //$NON-NLS-1$

    public static String removePrefix(String s, String prefix)
    {
        if (s == null)
        {
            return null;
        }
        if (s.startsWith(prefix))
        {
            return s.substring(prefix.length());
        }
        return s;
    }

    /**
     * Remove suffix from given string.
     */
    public static String removeSuffix(String s, String suffix)
    {
        if (s == null)
        {
            return null;
        }
        if (s.endsWith(suffix))
        {
            return s.substring(0, s.length() - suffix.length());
        }
        return s;
    }

    /**
     * Remove prefix and suffix from given string.
     */
    public static String removePrefixAndSuffix(String s, String prefix, String suffix)
    {
        return removePrefix(removeSuffix(s, suffix), prefix);
    }
    
    /**
     * Convert document to formatted XML string.
     */
    public static String toString(Document doc) throws TransformerConfigurationException, TransformerFactoryConfigurationError, TransformerException
    {
        // NOTE: There are different transformer implementations in the wild, which are configured differently
        //       regarding the indent size:
        //       Java 1.4: org.apache.xalan.transformer.TransformerIdentityImpl 
        //       Java 1.5: com.sun.org.apache.xalan.internal.xsltc.trax.TransformerImpl

        StringWriter writer = new StringWriter();
        Source source = new DOMSource(doc);
        Result result = new StreamResult(writer);
        TransformerFactory factory = TransformerFactory.newInstance();
        boolean indentFallback = false;
        try
        {
            // indent using TransformerImpl
            factory.setAttribute("indent-number", "4"); //$NON-NLS-1$ //$NON-NLS-2$
        }
        catch (IllegalArgumentException e)
        {
            // option not supported, set indent size below
            indentFallback = true;
        }
        Transformer transformer = factory.newTransformer();
        transformer.setOutputProperty(OutputKeys.INDENT, "yes"); //$NON-NLS-1$
        if (indentFallback)
        {
            // indent using TransformerIdentityImpl
            transformer.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "4"); //$NON-NLS-1$ //$NON-NLS-2$
        }
        transformer.transform(source, result);
        return writer.toString();
    }
   
    /**
     * Read XML file.
     */
    public static Document parseXmlFile(File file) throws SAXException, IOException, ParserConfigurationException
    {
        DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
        factory.setValidating(false);
        Document doc = factory.newDocumentBuilder().parse(file);
        return doc;
    }

    /**
     * Read XML string.
     */
    public static Document parseXmlString(String s) throws SAXException, IOException, ParserConfigurationException
    {
        DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
        factory.setValidating(false);
        Document doc = factory.newDocumentBuilder().parse(new ByteArrayInputStream(s.getBytes()));
        return doc;
    }

    /**
     * Converts collection to a separated string. 
     * @param c            collection
     * @param separator    string to separate items
     * @return             collection items separated with given separator
     */
    public static String toString(Collection c, String separator)
    {
        StringBuffer b = new StringBuffer();
        for (Iterator iter = c.iterator(); iter.hasNext();)
        {
            b.append((String) iter.next());
            b.append(separator);
        }
        if (c.size() > 0) {
            b.delete(b.length() - separator.length(), b.length());
        }
        return b.toString();
    }

    /**
     * Remove duplicates preserving original order.
     * @param l list to remove duplicates from
     * @return new list without duplicates 
     */
    public static List removeDuplicates(List l)
    {
        List res = new ArrayList();
        for (Iterator iter = l.iterator(); iter.hasNext();)
        {
            Object element = iter.next();
            if (!res.contains(element))
            {
                res.add(element);
            }
        }
        return res;
    }

    /**
     * Check if given file exists that was not written by this export.
     */
    public static boolean existsUserFile(String filename)
    {
        File buildFile = new File(filename);
        if (buildFile.exists())
        {
            BufferedReader in = null;
            try
            {
                in = new BufferedReader(new FileReader(buildFile));
                int i = BuildFileCreator.WARNING.indexOf(NEWLINE);
                String warning = BuildFileCreator.WARNING.substring(0, i);
                String line;
                while ((line = in.readLine()) != null)
                {
                    if (line.indexOf(warning) != -1)
                    {
                        return false;
                    }
                }
                return true;
            } catch (FileNotFoundException e) {
               return false;
            } catch (IOException e) {
               return false;
            } finally
            {
                try
                {
                    if (in != null)
                    {
                        in.close();
                    }
                }
                catch (IOException e)
                {
                    // ignore
                }
            }
        }
        return false;
    }

    /**
     * Asks user for confirmation.
     * @param message    message to display
     * @param shell      parent instance for dialogs
     */
    public static boolean confirm(String message, Shell shell)
    {
        return MessageDialog.openConfirm(shell,
                DataTransferMessages.AntBuildfileExportPage_4, message);
    }
    
    /**
     * Get file modification validator for given file.
     */
    public static IFileModificationValidator getFileModificationValidator(IFile file)
    {
        List files = new ArrayList();
        files.add(file);
        Map validator2files = getFileModificationValidator(files);
        return (IFileModificationValidator) validator2files.keySet().iterator().next();
    }
    
    /**
     * Get file modification validators for given files.
     * Each file may under control of a different file modification validator,
     * so it's required to use a map to represent result.
     * 
     * @param files                  IFile objects
     * @return                       Validator map. Key is validator, value is
     *                               List of IFile objects.
     */
    public static Map getFileModificationValidator(List files)
    {
        Map validator2files = new TreeMap(getClassnameComparator());
        for (Iterator iter = files.iterator(); iter.hasNext();)
        {
            IFile file = (IFile) iter.next();
            RepositoryProvider rp = RepositoryProvider.getProvider(file.getProject());
            IFileModificationValidator validator = (rp != null)
                ? rp.getFileModificationValidator()
                : getDefaultValidator();
            if (validator == null)
            {
                // some version control plugins don't provide a validator
                // (e.g. subversion)
                validator = getDefaultValidator();
            }
            List list =  (List) validator2files.get(validator);
            if (list == null)
            {
                list = new ArrayList();
            }
            list.add(file);
            validator2files.put(validator, list);
        }
        return validator2files;
    }
    
    /**
     * Request write access to given file.
     * Depending on the version control plugin opens a confirm checkout dialog.
     * 
     * @param shell              parent instance for dialogs
     * @param file               file to request write access for 
     * @return                   true if user confirmed checkout
     * @throws CoreException     thrown if project is under version control, but
     *                           not connected
     */
    public static boolean validateEdit(Shell shell, IFile file) throws CoreException
    {
        List files = new ArrayList();
        files.add(file);
        Map validator2files = getFileModificationValidator(files);
        return validateEdit(shell, validator2files).size() > 0;
    }
    
    /**
     * Request write access to given files in validatorname2files map.
     * Depending on the version control plugin opens a confirm checkout dialog.
     * 
     * @param shell              parent instance for dialogs
     * @param validator2files    Validator map. Key is validator name, value is
     *                           List of IFile objects.
     * @return                   IFile objects for which user confirmed checkout
     * @throws CoreException     thrown if project is under version control,
     *                           but not connected
     */
    public static Set validateEdit(Shell shell, Map validator2files) throws CoreException
    {
        Set confirmedFiles = new TreeSet(getIFileComparator());
        for (Iterator iter = validator2files.keySet().iterator(); iter.hasNext();)
        {
            IFileModificationValidator validator = (IFileModificationValidator) iter.next();
            List files = (List) validator2files.get(validator);
            IFile[] filesArray = (IFile[]) files.toArray(new IFile[files.size()]);
            IStatus status = validator.validateEdit(filesArray, shell);
            if (status.isMultiStatus() && status.getChildren().length > 0)
            {
                for (int i = 0; i < status.getChildren().length; i++)
                {
                    IStatus statusChild = status.getChildren()[i];
                    if (statusChild.isOK())
                    {
                        confirmedFiles.add(filesArray[i]);
                    }
                }
            }
            else if (status.isOK())
            {
                for (Iterator iterator = files.iterator(); iterator.hasNext();)
                {
                    IFile file = (IFile) iterator.next();
                    confirmedFiles.add(file);                    
                }
            }
            // default validator sets error status if user pressed cancel, so
            // ignore its error
            if (validator != getDefaultValidator() &&
                status.getSeverity() == IStatus.ERROR)
            {
                // not possible to checkout files: not connected to version
                // control plugin or hijacked files and made read-only, so
                // collect error messages provided by validator and rethrow
                StringBuffer message = new StringBuffer(status.getPlugin() + ": " //$NON-NLS-1$
                        + status.getMessage() + NEWLINE);
                if (status.isMultiStatus())
                {
                    for (int i = 0; i < status.getChildren().length; i++)
                    {
                        IStatus statusChild = status.getChildren()[i];
                        message.append(statusChild.getMessage() + NEWLINE);
                    }
                }
                throw new CoreException(new Status(IStatus.ERROR, AntUIPlugin.PI_ANTUI, 0,
                        message.toString(), null));               
            }
            
        }
        return confirmedFiles;
    }
    
    private static IFileModificationValidator defaultValidator;        

    /**
     * Get default file modification validator.
     */
    public static synchronized IFileModificationValidator getDefaultValidator() {
        if (defaultValidator == null) {
            defaultValidator = loadUIValidator();
        }
        return defaultValidator;
    }

    // copied from org.eclipse.team.internal.core.DefaultFileModificationValidator
    private static IFileModificationValidator loadUIValidator() {
        IExtensionPoint extension = Platform.getExtensionRegistry().getExtensionPoint("org.eclipse.team.core", "defaultFileModificationValidator"); //$NON-NLS-1$ //$NON-NLS-2$
        if (extension != null) {
            IExtension[] extensions =  extension.getExtensions();
            if (extensions.length > 0) {
                IConfigurationElement[] configElements = extensions[0].getConfigurationElements();
                if (configElements.length > 0) {
                    try {
                        Object o = configElements[0].createExecutableExtension("class"); //$NON-NLS-1$
                        if (o instanceof IFileModificationValidator) {
                            return (IFileModificationValidator)o;
                        }
                    } catch (CoreException e) {
                        AntUIPlugin.log(e);
                    }
                }
            }
        }
        // default validator not found, return validator that always accepts 
        return new IFileModificationValidator()
        {
            public IStatus validateEdit(IFile[] files, Object context)
            {
                return Status.OK_STATUS;
            }

            public IStatus validateSave(IFile file)
            {
                return Status.OK_STATUS;
            }           
        };
    }

    /**
     * Check if given classpath is a reference to the default classpath of the project.
     * Ideal for testing if runtime classpath was customized.
     */
    public static boolean isDefaultClasspath(IJavaProject project, EclipseClasspath classpath)
    {
        List list = removeDuplicates(classpath.rawClassPathEntries);
        if (list.size() != 1)
        {
            return false;
        }
        String entry = (String) list.iterator().next();
        if (EclipseClasspath.isProjectReference(entry))
        {
            IJavaProject referencedProject = EclipseClasspath.resolveProjectReference(entry); 
            if (referencedProject == null)
            {
                // project was not loaded in workspace
                return false;
            }
            else if (referencedProject.getProject().getName().equals(project.getProject().getName()))
            {               
                return true;
            }
        }
            
        return false;
    }

    /**
     * Add variable/value for Eclipse variable. If given string is no variable, nothing is added.
     * 
     * @param variable2valueMap   property map to add variable/value
     * @param s                   String which may contain Eclipse variables, e.g. ${project_name}
     */
    public static void addVariable(Map variable2valueMap, String s, String projectRoot)
    {
        if (s == null || s.equals("")) //$NON-NLS-1$
        {
            return;
        }
        Pattern pattern = Pattern.compile("\\$\\{.*?\\}"); // ${var} //$NON-NLS-1$
        Matcher matcher = pattern.matcher(s);
        while (matcher.find())
        {           
            String variable = matcher.group();
            String value;
            try
            {
                value = VariablesPlugin.getDefault().getStringVariableManager().performStringSubstitution(variable);    
            }
            catch (CoreException e)
            {
                // cannot resolve variable
                value = variable;
            }
            variable = removePrefixAndSuffix(variable, "${", "}"); //$NON-NLS-1$ //$NON-NLS-2$
            // if it is an environment variable, convert to Ant environment syntax 
            if (variable.startsWith("env_var:")) //$NON-NLS-1$
            {
                value = "env." + variable.substring("env_var:".length()); //$NON-NLS-1$ //$NON-NLS-2$
            }
            File file = new File(value);
            if (file.exists()) {
                value = ExportUtil.getRelativePath(file.getAbsolutePath(), projectRoot);
            }
            variable2valueMap.put(variable, value);
        }
    }

    /**
     * Returns a path which is equivalent to the given location relative to the
     * specified base path.
     */
    public static String getRelativePath(String otherLocation, String basePath) {

        IPath location= new Path(otherLocation);
        IPath base= new Path(basePath);
        if ((location.getDevice() != null && !location.getDevice().equalsIgnoreCase(base.getDevice())) || !location.isAbsolute()) {
            return otherLocation;
        }
        int baseCount = base.segmentCount();
        int count = base.matchingFirstSegments(location);
        String temp = ""; //$NON-NLS-1$
        for (int j = 0; j < baseCount - count; j++) {
            temp += "../"; //$NON-NLS-1$
        }
        String relative= new Path(temp).append(location.removeFirstSegments(count)).toString();
        if (relative.length() == 0) {
            relative=  "."; //$NON-NLS-1$
        }
        
        return relative;
    }
}