package com.pmease.quickbuild.plugin.scm.svn;

import java.io.File;
import java.io.StringReader;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import java.util.Stack;

import org.dom4j.Attribute;
import org.dom4j.Document;
import org.dom4j.DocumentException;
import org.dom4j.Element;
import org.dom4j.io.SAXReader;
import org.hibernate.validator.constraints.NotEmpty;
import org.joda.time.DateTime;
import org.joda.time.DateTimeZone;
import org.joda.time.format.DateTimeFormat;
import org.joda.time.format.DateTimeFormatter;

import com.pmease.quickbuild.Context;
import com.pmease.quickbuild.Quickbuild;
import com.pmease.quickbuild.QuickbuildException;
import com.pmease.quickbuild.annotation.Advanced;
import com.pmease.quickbuild.annotation.Editable;
import com.pmease.quickbuild.annotation.Password;
import com.pmease.quickbuild.annotation.ScriptApi;
import com.pmease.quickbuild.annotation.Scriptable;
import com.pmease.quickbuild.execution.Commandline;
import com.pmease.quickbuild.execution.Commandline.ExecuteResult;
import com.pmease.quickbuild.execution.LineConsumer;
import com.pmease.quickbuild.execution.SecretMasker;
import com.pmease.quickbuild.migration.VersionedDocument;
import com.pmease.quickbuild.repositorysupport.Changeset;
import com.pmease.quickbuild.repositorysupport.Modification;
import com.pmease.quickbuild.repositorysupport.Repository;
import com.pmease.quickbuild.repositorysupport.RevisionInfo;
import com.pmease.quickbuild.repositorysupport.SourceViewSupport;
import com.pmease.quickbuild.util.Constants;
import com.pmease.quickbuild.util.FileUtils;
import com.pmease.quickbuild.util.Pair;
import com.pmease.quickbuild.util.StringUtils;
import com.thoughtworks.xstream.annotations.XStreamOmitField;

@Editable(name="Subversion", description=
	"Configure a Subversion repository here. By default, QuickBuild executes \"svn\" to " +
	"checkout from the repository, and expects this file to be on the system path. If " +
	"not, you will need to specify path to this file by configuring the Subversion plugin " +
	"through the plugin management page.<br><br>" +
	"<strong>NOTE:</strong> QuickBuild does not work with cygwin Subversion binary, " +
	"please use a native Windows subversion binary if you check out code on Windows " +
	"platform.")
@ScriptApi
public class SvnRepository extends Repository<SvnRevision> {
	private static final long serialVersionUID = 1L;

	private static final DateTimeFormatter INPUT_DATETIME_FORMATTER = 
			DateTimeFormat.forPattern("yyyy-MM-dd'T'HH:mm:ss'Z'").withZone(DateTimeZone.UTC);
	private static final DateTimeFormatter OUTPUT_DATETIME_FORMATTER =
            DateTimeFormat.forPattern("yyyy-MM-dd'T'HH:mm:ss.SSS").withZone(DateTimeZone.UTC);
	
	private String url;
	
	private String destPath;
	
	private String userName;
	
	private String password;
	
	private String buildRevision;
	
	private boolean recursive = true;
	
	private boolean exportMode;

	private boolean ignoreExternals;
	
	private String labelUrl;
	
	private SvnProofBuildSupport proofBuildSupport;
	
	@XStreamOmitField
	private Pair<String, String> normalizedUrls;
	
	@XStreamOmitField
	private Boolean supportTrustServerCert;
	
	@Editable(order=1000, name="Url to Checkout", description=
		"Specify the repository url to checkout, for example:" +
		"<em>svn://scm-host/home/svn/repository/myproject/trunk</em>, " +
		"<em>http://scm-host/repository/myproject/branches/2.0</em>.<br>" +
		"<strong>NOTE:</strong> If you are using <em>https://</em> or <em>svn+ssh://</em> schema, " +
		"you should make sure that svn server certificate has been accepted permanently by " +
		"your build machine for the same user account the QuickBuild process is running as")
	@NotEmpty
	@ScriptApi("Get Subversion URL to checkout from.")
	@Scriptable
	public String getUrl() {
		return url;
	}

	public void setUrl(String url) {
		this.url = url;
	}

	@Editable(order=1002, description=
		"Optionally specify the user who has permission to access the above url"
		)
	@ScriptApi("Get user name to access the repository.")
	@Scriptable
	public String getUserName() {
		return userName;
	}

	public void setUserName(String userName) {
		this.userName = userName;
	}

	@Editable(order=1004, description="Specify password of the above user")
	@Password
	@ScriptApi("Get password to access the repository. Null if not specified.")
	@Scriptable
	public String getPassword() {
		return password;
	}

	public void setPassword(String password) {
		this.password = password;
	}

	@Editable(order=1006, name="Revision to Build", description=
		"Optionally specify the revision to build against. You may also specify date here as long as it "
		+ "is surrounded with { and }. Refer to Subversion revision spec for more details. If not "
		+ "specified, the head revision of the above url will be checked out."
		)
	@ScriptApi("Get specified revision to build against. Null if build against head revision.")
	@Scriptable
	public String getBuildRevision() {
		return buildRevision;
	}
	
	public void setBuildRevision(String buildRevision) {
		this.buildRevision = buildRevision;
	}
	
	@Editable(order=1008, description=
		"Whether or not to checkout specified url recursively."
		)
	@ScriptApi("Whether or not to checkout specified url recursively.")
	@Advanced
	public boolean isRecursive() {
		return recursive;
	}

	public void setRecursive(boolean recursive) {
		this.recursive = recursive;
	}

	@Editable(order=1010, name="Destination Path", description=
		"Specify the destination path to which contents in the above url will be retrieved into. " +
		"This path is relative to the running configuration's workspace. If left empty, the " +
		"workspace directory itself will be used to put checked out contents."
		)
	@ScriptApi("Get destination path to put checked out files. Null if workspace directory " +
			"is used.")
	@Scriptable
	public String getDestPath() {
		return destPath;
	}

	public void setDestPath(String destPath) {
		this.destPath = destPath;
	}

	@Editable(order=1030, name="Use Export Mode?", description=
		"Specifies whether or not to retrieve code from Subversion repository using export mode. " +
		"When use export mode, the full source tree can be retrieved very fast, however, it can " +
		"not incremently update the working directory. The full source tree need to be retrieved " +
		"for each build even if a single source file has been changed since last build.")
	@ScriptApi("Whether or not the export mode is used for code checkout.")
	@Advanced
	public boolean isExportMode() {
		return exportMode;
	}

	public void setExportMode(boolean exportMode) {
		this.exportMode = exportMode;
	}

	@Editable(order=1040, name="Ignore Externals?", description=
		"Specifies whether or not to ignore external definitions when checkout from " +
		"the repository."
		)
	@ScriptApi("Whether or not to ignore externals when checkout.")
	@Advanced
	public boolean isIgnoreExternals() {
		return ignoreExternals;
	}

	public void setIgnoreExternals(boolean ignoreExternals) {
		this.ignoreExternals = ignoreExternals;
	}

	@Editable(order=1050, name="Url to Label as", description=
		"Optionally specify the destination url if this repository is to be labeled. " +
		"The checkout url will be copied to this url when the label step is executed " +
		"against this repository. For example:" +
		"<em>svn://buildmachine/home/svn/repository/myproject/tags/{0}</em>, " +
		"<em>http://buildmachine/repository/myproject/tags/{0}</em>. " +
		"Here <em>{0}</em> will be replaced by the actual label.")
	@Scriptable
	@ScriptApi("Get url to label as.")
	public String getLabelUrl() {
		return labelUrl;
	}

	public void setLabelUrl(String labelUrl) {
		this.labelUrl = labelUrl;
	}

	@Editable(order=1055, description="Check this to enable proof build for this repository")
	@Override
	@ScriptApi("Get proof build support object. Null if proof build support is not enabled.")
	@Advanced
	public SvnProofBuildSupport getProofBuildSupport() {
		return proofBuildSupport;
	}

	public void setProofBuildSupport(SvnProofBuildSupport proofBuildSupport) {
		this.proofBuildSupport = proofBuildSupport;
	}

	@ScriptApi("Get revision of this repository.")
	@Override
	public SvnRevision getRevision() {
		return super.getRevision();
	}

	Pair<String, String> getNormalizedUrls() {
		if (normalizedUrls == null) {
			normalizedUrls = new Pair<String, String>();
	        Commandline cmdline = buildSvnCmd().addArgValue("info").addArgValue(getUrl());

	        addAuthenticationSwitches(cmdline, getUserName(), getPassword());
	        cmdline.addArgValue("--non-interactive");
    		if (isSupportTrustServerCert())
    			cmdline.addArgValue("--trust-server-cert");
	        
	        cmdline.addArgValue("--xml");
	        final StringBuffer buffer = new StringBuffer();

	        // this command specify "p\n" (or "p\r\n" for windows) as input in order to accept 
	        // certificate permanently in case certificate acceptance option is presented.
	        cmdline.execute(new LineConsumer(Constants.UTF8) {

				@Override
				public void consume(String line) {
	            	Context.getLogger().debug(line);
	                buffer.append(line);
	                buffer.append("\n");
				}
	        	
	        }, new LineConsumer.WarnLogger(), 
	        "p" + Constants.LINE_SEPARATOR + "yes" + Constants.LINE_SEPARATOR).checkReturnCode();
	        
	        SAXReader reader = new SAXReader();
	        try {
	            Document doc = reader.read(new StringReader(buffer.toString()));
	            Element entryElement = doc.getRootElement().element("entry");
	            if (entryElement == null) {
	                throw new QuickbuildException("Can not find entry element for " +
	                		"information of url '" + getUrl() + "'.");
	            }
	            normalizedUrls.setFirst(decodeUrl(entryElement.element("repository").element("root").getText()));
	            normalizedUrls.setSecond(decodeUrl(entryElement.element("url").getText()));
	        } catch (DocumentException e) {
	            throw new RuntimeException(e);
	        }
		}
		return normalizedUrls;
	}

	/**
	 * Get svn url of specified directory.
	 * @param dir
	 */
	private String getDirUrl(File dir) {
		Commandline cmdline = buildSvnCmd().addArgValue("info")
				.addArgValue(dir.getAbsolutePath());

		addAuthenticationSwitches(cmdline, getUserName(), getPassword());
        cmdline.addArgValue("--non-interactive");
		if (isSupportTrustServerCert())
			cmdline.addArgValue("--trust-server-cert");
		
		cmdline.addArgValue("--xml");
        final StringBuffer buffer = new StringBuffer();

        // this command specify "p\n" (or "p\r\n" for windows) as input in order to accept 
        // certificate permanently in case certificate acceptance option is presented.
        cmdline.execute(new LineConsumer(Constants.UTF8) {

			@Override
			public void consume(String line) {
            	Context.getLogger().debug(line);
                buffer.append(line);
                buffer.append("\n");
			}
        	
        }, new LineConsumer.WarnLogger(), 
        "p" + Constants.LINE_SEPARATOR + "yes" + Constants.LINE_SEPARATOR).checkReturnCode();

		SAXReader reader = new SAXReader();
		try {
			Document doc = reader.read(new StringReader(buffer.toString()));
			Element entryElement = doc.getRootElement().element("entry");
			if (entryElement == null) {
				throw new QuickbuildException("Can not find entry element for " +
						"information of url '" + getUrl() + "'.");
			}
			return decodeUrl(entryElement.element("url").getText());
		} catch (DocumentException e) {
			throw new RuntimeException(e);
		}
	}

	Commandline buildSvnCmd() {
        Commandline cmdLine = new Commandline();
        String svnExe = ((SvnSetting)getPlugin().getSetting(true)).getSvnExecutablePath();
        if (svnExe == null)
        	svnExe = "svn";
        cmdLine.setExecutable(svnExe);
        return cmdLine;
    }
	
	void addLogLevelSwitches(Commandline cmdLine) {
        if (!Context.getLogger().isInfoEnabled())
            cmdLine.createArgument().setValue("--quiet");
    }
	
    void addAuthenticationSwitches(Commandline cmdline, String userName, final String password) {
        if (userName != null) {
            cmdline.addArgValue("--username").addArgValue(userName);
            if (password != null) {
                cmdline.addArgValue("--password").addArgValue(password);
                cmdline.setSecretMasker(new SecretMasker() {
					
					public String mask(String message) {
						return StringUtils.replace(message, 
								"--password " + quoteIfNecessary(password), "--password ******");
					}
				});
            } else {
                cmdline.addArgValue("--password");
                cmdline.addArgValue("\"\"");
            }
        }
    }
	
	@SuppressWarnings("unchecked")
	@Override
	protected void checkoutByRevision(SvnRevision revision) {
    	File workingDir = Context.getConfiguration().getWorkspaceDir();
		File destDir = getDestDir();
		
		Context.getLogger().info("Checking out (url: {}, to: {}, revision: {})", 
				new Object[]{getUrl(), destDir.getAbsolutePath(), revision.toString()});
		
		String url = getUrl();
		String svnCommand = "checkout";
		if(new File(destDir, ".svn").exists()) {
			String destDirUrl = getDirUrl(destDir);
			if(!StringUtils.stripEnd(url, "/").equals(destDirUrl)) {
				svnCommand = "switch";
				Context.getLogger().info("Detected change in URL (from: {}, to: {}), using switch command.", 
						destDirUrl, url);
			}
		}
        if (!isExportMode()) {
    		Commandline cmdline = buildSvnCmd().addArgValue(svnCommand).addArgValue(url)
    				.addArgValue(destDir.getAbsolutePath()).addArgValue("--non-interactive");
    		if (isSupportTrustServerCert())
    			cmdline.addArgValue("--trust-server-cert");
            addLogLevelSwitches(cmdline);
            addAuthenticationSwitches(cmdline, getUserName(), getPassword());
            if (!isRecursive())
            	cmdline.addArgValue("-N");
        	if (isIgnoreExternals())
        		cmdline.addArgValue("--ignore-externals");
        	cmdline.addArgLine("-r " + revision);
        	
            final boolean conflictsFound[] = new boolean[]{false};
            ExecuteResult result = cmdline.execute(workingDir, new LineConsumer() {

				@Override
				public void consume(String line) {
					if (line.contains("Summary of conflicts:") || line.contains("Tree conflicts:") ||
							line.contains("Text conflicts:")) {
						Context.getLogger().error(line);
						conflictsFound[0] = true;
					} else {
						Context.getLogger().debug(line);
					}
				}
            	
            }, new LineConsumer.WarnLogger());
            
            if (conflictsFound[0])
            	throw new QuickbuildException("Conflicts found.");
            else if (result.getReturnCode() != 0)
            	throw result.buildException();
            
            if (!isIgnoreExternals()) {
	    		final List<String> untrackedPaths = new ArrayList<String>();
	    		final List<String> externalPaths = new ArrayList<String>();
	
	    		cmdline.clearArgs();
	    		cmdline.addArgValue("status");
	    		
	    		cmdline.execute(destDir, new LineConsumer() {
	
	    			@Override
	    			public void consume(String line) {
	                	Context.getLogger().debug(line);
	                	if (line.startsWith("Performing status on external item at")) {
	                		String externalPath = StringUtils.replace(StringUtils.substringBeforeLast(
	                				StringUtils.substringAfter(line, "'"), "'"), "\\", "/");
	                		if (!externalPath.startsWith("/"))
	                			externalPath = "/" + externalPath;
	                		externalPaths.add(externalPath);
	                	} else if (line.startsWith("?")) {            	
	                		String untrackedPath = StringUtils.replace(line.substring(1).trim(), "\\", "/");
	                		if (!untrackedPath.startsWith("/"))
	                			untrackedPath = "/" + untrackedPath;
	                		untrackedPaths.add(untrackedPath);
	                	}
	    			}
	    			
	    		}, new LineConsumer.WarnLogger()).checkReturnCode();
	            
	    		List<ExternalInfo> externals = new ArrayList<ExternalInfo>();
	    		for (String externalPath: externalPaths) {
	    			final ExternalInfo external = new ExternalInfo();
	    			external.setCheckoutPath(externalPath);
	    			File externalDir = new File(destDir, externalPath);
	    			cmdline.clearArgs();
	    			cmdline.addArgValue("info");
	    			cmdline.execute(externalDir, new LineConsumer() {
	
						@Override
						public void consume(String line) {
							Context.getLogger().debug(line);
							if (line.startsWith("URL: ")) {
								external.setUrl(line.substring("URL: ".length()));
							} else if (line.startsWith("Last Changed Rev: ")) {
								external.setRevision(line.substring("Last Changed Rev: ".length()));
							}
						}
	    				
	    			}, new LineConsumer.WarnLogger()).checkReturnCode();
	    			
	    			externals.add(external);
	    		}
	    		
	    		for (ExternalInfo external: externals) {
					if (external.getUrl().contains("/tags/")) {
						String before = StringUtils.substringBefore(external.getUrl(), "/tags/");
						String after = StringUtils.substringAfter(external.getUrl(), "/tags/");
						after = StringUtils.substringAfter(after, "/");
						String revisedUrl = before + "/trunk/" + after;
						// now check if revised url exists
						cmdline.clearArgs();
				        addAuthenticationSwitches(cmdline, getUserName(), getPassword());
						cmdline.addArgValue("info");
						cmdline.addArgValue("--non-interactive");
			    		if (isSupportTrustServerCert())
			    			cmdline.addArgValue("--trust-server-cert");
						cmdline.addArgValue(revisedUrl);
						result = cmdline.execute(new LineConsumer.DebugLogger(), new LineConsumer.WarnLogger());
						if (result.getReturnCode() == 0) {
							external.setUrl(revisedUrl);
						}
					}
	    		}

	    		List<ExternalInfo> previousExternals = (List<ExternalInfo>) Quickbuild.getServerService()
	    				.getPreviousRepositoryRuntimeCustomData(getBuild().getId(), getName());

	    		if (previousExternals != null) {
	    			if (Context.getConfiguration().isRecordSCMChanges())	    			
	    				amendChanges(getChanges(), externals, previousExternals);
	    			
		    		for (String untrackedPath: untrackedPaths) {
	    				for (ExternalInfo previousExternal: previousExternals) {
	    					if (untrackedPath.equals(previousExternal.getCheckoutPath())) {
	    						File untrackedFile = new File(destDir, untrackedPath);
	    						if (untrackedFile.isFile())
	    							FileUtils.deleteFile(untrackedFile);
	    						else
	    							FileUtils.deleteDir(untrackedFile);
	    					}
	    				}
		    		}
	    		}
	    		
	    		getRuntime().setCustomData(externals);
	    		Quickbuild.getServerService().saveRepositoryRuntimeCustomData(
	    				getBuild().getId(), getName(), getRuntime().getCustomData());
            }
        } else {
    		Commandline cmdline = buildSvnCmd().addArgValue("export").addArgValue(getUrl())
    				.addArgValue(destDir.getAbsolutePath()).addArgValue("--non-interactive");
    		if (isSupportTrustServerCert())
    			cmdline.addArgValue("--trust-server-cert");
            addLogLevelSwitches(cmdline);
            addAuthenticationSwitches(cmdline, getUserName(), getPassword());
            if (!isRecursive())
            	cmdline.addArgValue("-N");
        	if (isIgnoreExternals())
        		cmdline.addArgValue("--ignore-externals");
            cmdline.addArgValue("--force").addArgLine("-r " + revision);
        	
            cmdline.execute(workingDir, new LineConsumer.DebugLogger(), new LineConsumer.WarnLogger())
            		.checkReturnCode();
        }				
	}
	
	@SuppressWarnings("unchecked")
	@Override
	protected void amendChanges(List<Changeset> changes, Object customData, Object sinceCustomData) {
		List<ExternalInfo> externals = (List<ExternalInfo>) customData;
		List<ExternalInfo> sinceExternals = (List<ExternalInfo>) sinceCustomData;
		
		boolean hasExternalChanges = false;
		for (Changeset each: changes) {
			if (each.getId().contains(":")) {
				hasExternalChanges = true;
				break;
			}
		}
		if (!hasExternalChanges) {
			for (ExternalInfo external: externals) {
				for (ExternalInfo previousExternal: sinceExternals) {
					if (previousExternal.getUrl().equals(external.getUrl())) {
						SvnRepository externalRepo = new SvnRepository();
						externalRepo.setUrl(external.getUrl());
						externalRepo.setUserName(getUserName());
						externalRepo.setPassword(getPassword());
						externalRepo.setIgnoreExternals(true);
						List<Changeset> externalChanges = externalRepo.getChangesBetween(new SvnRevision(previousExternal.getRevision()), 
								new SvnRevision(external.getRevision()));
				        String checkoutPath = externalRepo.getNormalizedUrls().getSecond().substring(
				        		externalRepo.getNormalizedUrls().getFirst().length());
				        for (Changeset change: externalChanges) {
				        	for (Modification modification: change.getModifications()) {
				        		String relativePath = modification.getPath().substring(checkoutPath.length());
				        		if (relativePath.length() > 0 && relativePath.charAt(0) != '/')
				        			relativePath = "/" + relativePath;
				        		relativePath = external.getCheckoutPath() + relativePath;
				        		relativePath = getNormalizedUrls().getSecond().substring(getNormalizedUrls().getFirst().length()) + relativePath;
				        		modification.setPath(relativePath);
				        	}
				        	change.setId(external.getCheckoutPath() + ":" + change.getId());
				        }
						
						changes.addAll(externalChanges);
					}
				}
			}
		}
	}
	
	File getDestDir() {
		return FileUtils.resolvePath(Context.getConfiguration().getWorkspaceDir(), getDestPath());
	}

	@Override
	protected RevisionInfo getRevisionInfo(SvnRevision revision) {
        Commandline cmdline = buildSvnCmd().addArgValue("log")
        		.addArgValue(getNormalizedUrls().getFirst()).addArgValue("--non-interactive");
		if (isSupportTrustServerCert())
			cmdline.addArgValue("--trust-server-cert");
        
        addAuthenticationSwitches(cmdline, getUserName(), getPassword());
        cmdline.addArgLine("--xml -r");
        cmdline.addArgValue(revision.toString());
        final StringBuffer buffer = new StringBuffer();
        
        cmdline.execute(new LineConsumer(Constants.UTF8) {

			@Override
			public void consume(String line) {
        		Context.getLogger().debug(line);
                buffer.append(line);   
                buffer.append("\n");
			}
        	
        }, new LineConsumer.WarnLogger()).checkReturnCode();
        
        RevisionInfo revInfo = new RevisionInfo();
        
        SAXReader reader = new SAXReader();
        try {
            Document doc = reader.read(new StringReader(buffer.toString()));
            Element logEntry = doc.getRootElement().element("logentry");
            Element authorElement = logEntry.element("author");
            if (authorElement != null) 
            	revInfo.setCommitter(authorElement.getText());
            String comment = logEntry.element("msg").getText();
            if (StringUtils.isNotBlank(comment))
                revInfo.setComment(comment);
        } catch (DocumentException e) {
            throw new RuntimeException(e);
        }
        
        return revInfo;
	}
	
	@Override
	protected SvnRevision getHeadRevision() {
		String buildRevision = getBuildRevision();
		if (buildRevision != null && (!buildRevision.startsWith("{") || !buildRevision.endsWith("}"))) {
			return new SvnRevision(buildRevision);
		}
		
        Commandline cmdline = buildSvnCmd().addArgValue("info").addArgValue(getUrl());

        addAuthenticationSwitches(cmdline, getUserName(), getPassword());
        cmdline.addArgValue("--non-interactive");
		if (isSupportTrustServerCert())
			cmdline.addArgValue("--trust-server-cert");
		
		if (buildRevision != null)
			cmdline.addArgValue("-r").addArgValue(buildRevision);
        
        cmdline.addArgValue("--xml");
        final StringBuffer buffer = new StringBuffer();

        // this command specify "p\n" (or "p\r\n" for windows) as input in order to accept 
        // certificate permanently in case certificate acceptance option is presented.
        cmdline.execute(new LineConsumer(Constants.UTF8) {

			@Override
			public void consume(String line) {
            	Context.getLogger().debug(line);
                buffer.append(line);
                buffer.append("\n");
			}
        	
        }, new LineConsumer.WarnLogger(), 
        "p" + Constants.LINE_SEPARATOR + "yes" + Constants.LINE_SEPARATOR).checkReturnCode();

        SAXReader reader = new SAXReader();
        try {
            Document doc = reader.read(new StringReader(buffer.toString()));
            Element entryElement = doc.getRootElement().element("entry");
            if (entryElement == null) {
                throw new QuickbuildException("Can not find entry element for " +
                		"information of url '" + getUrl() + "'.");
            }
            normalizedUrls = new Pair<String, String>();
            normalizedUrls.setFirst(decodeUrl(entryElement.element("repository").element("root").getText()));
            normalizedUrls.setSecond(decodeUrl(entryElement.element("url").getText()));
            Attribute revisionAttrib = entryElement.attribute("revision");
            if (revisionAttrib == null)
                throw new QuickbuildException("Can not find revision attribute inside the " +
                		"entry element.");
            return new SvnRevision(revisionAttrib.getValue());
        } catch (DocumentException e) {
            throw new RuntimeException(e);
        }
    }

	@Override
	protected void labelOnRevision(SvnRevision revision, String label, String comment) {
        Context.getLogger().info("Creating label (url: {}, revision: {}, label: {})", 
        		new Object[]{getUrl(), revision.toString(), label});
        
		File workingDir = Context.getConfiguration().getWorkspaceDir();

        if (getLabelUrl() == null) {
        	throw new QuickbuildException("The property 'Url to Label as' needs to " +
        			"be defined in order to create label on this repository");
        }
        
        String destUrl = StringUtils.stripEnd(StringUtils.replace(getLabelUrl(), "{0}", label), "/");
        Commandline cmdline = buildSvnCmd();

        // Is destination path already exists?
        cmdline.addArgValue("list").addArgValue(destUrl).addArgValue("--non-interactive");
		if (isSupportTrustServerCert())
			cmdline.addArgValue("--trust-server-cert");
        
        addAuthenticationSwitches(cmdline, getUserName(), getPassword());
        
        Commandline.ExecuteResult result = cmdline.execute(workingDir, new LineConsumer.DebugLogger(), 
        		new LineConsumer() {

					@Override
					public void consume(String line) {
						if (!line.endsWith("non-existent in that revision"))
							Context.getLogger().warn(line);
					}
        	
        });

        if (result.getReturnCode() == 0) {
            Context.getLogger().warn("Destination url '" + destUrl + "' already exists, deleting...");
            cmdline.clearArgs().addArgValue("delete").addArgValue(destUrl)
            		.addArgValue("-m").addArgValue(comment)
            		.addArgValue("--non-interactive");
    		if (isSupportTrustServerCert())
    			cmdline.addArgValue("--trust-server-cert");
            
            addLogLevelSwitches(cmdline);
            addAuthenticationSwitches(cmdline, getUserName(), getPassword());
            
            cmdline.execute(workingDir, new LineConsumer.NoOp(), new LineConsumer.WarnLogger())
            		.checkReturnCode();
        }

        // TODO: We can make use of --parents to create intermediate missing directories
        // automatically for Subversion 1.5 or higher.
        // make sure existence of parent paths
        List<String> missingParentUrls = new ArrayList<String>();
        
        Context.getLogger().debug("Checking missing path elements...");
        String parentUrl = StringUtils.substringBeforeLast(destUrl, "/");
        while (StringUtils.isNotBlank(parentUrl)) {
            cmdline.clearArgs().addArgValue("list").addArgValue(parentUrl)
            		.addArgValue("--non-interactive");
    		if (isSupportTrustServerCert())
    			cmdline.addArgValue("--trust-server-cert");
            
            addAuthenticationSwitches(cmdline, getUserName(), getPassword());
            
            result = cmdline.execute(workingDir, new LineConsumer.NoOp(), 
            		new LineConsumer() {

						@Override
						public void consume(String line) {
							if (!line.endsWith("non-existent in that revision"))
								Context.getLogger().warn(line);
						}
            	
            });

            if (result.getReturnCode() != 0) {
            	missingParentUrls.add(parentUrl);
            	parentUrl = StringUtils.substringBeforeLast(parentUrl, "/");
            } else {
            	break;
            }
        }

        Collections.reverse(missingParentUrls);
        for (String missingParentUrl: missingParentUrls) {
        	Context.getLogger().debug("Creating missing parent: " + missingParentUrl);
            cmdline.clearArgs().addArgValue("mkdir").addArgValue(missingParentUrl)
            		.addArgValue("-m").addArgValue(comment).addArgValue("--non-interactive");
    		if (isSupportTrustServerCert())
    			cmdline.addArgValue("--trust-server-cert");
            addLogLevelSwitches(cmdline);
            addAuthenticationSwitches(cmdline, getUserName(), getPassword());
            cmdline.execute(workingDir, new LineConsumer.InfoLogger(), new LineConsumer.WarnLogger());
        }

        cmdline.clearArgs().addArgValue("copy").addArgValue(getUrl()).addArgValue(destUrl)
        		.addArgValue("-m").addArgValue(comment).addArgValue("--non-interactive");
		if (isSupportTrustServerCert())
			cmdline.addArgValue("--trust-server-cert");
        
        addLogLevelSwitches(cmdline);
        addAuthenticationSwitches(cmdline, getUserName(), getPassword());
        cmdline.addArgLine("-r " + revision);
        
        cmdline.execute(workingDir, new LineConsumer.DebugLogger(), new LineConsumer.WarnLogger())
        		.checkReturnCode();
	}

	@SuppressWarnings("unchecked")
	@Override
	protected List<Changeset> getChangesBetween(SvnRevision startRevision, SvnRevision endRevision) {
		List<Changeset> changes = new ArrayList<Changeset>();
		
		String url = getUrl();
        Commandline cmdline = buildSvnCmd().addArgValue("log")
        		.addArgValue(url).addArgValue("--non-interactive");
		if (isSupportTrustServerCert())
			cmdline.addArgValue("--trust-server-cert");
        
        addAuthenticationSwitches(cmdline, getUserName(), getPassword());
        cmdline.addArgLine("-v --xml -r");
        cmdline.addArgValue(endRevision.toString() + ":" + startRevision.toString());
        final StringBuffer buffer = new StringBuffer();
        
        cmdline.execute(new LineConsumer(Constants.UTF8) {

			@Override
			public void consume(String line) {
        		Context.getLogger().debug(line);
                buffer.append(line);   
                buffer.append("\n");
			}
        	
        }, new LineConsumer.WarnLogger()).checkReturnCode();
        
        if (buffer.length() > Constants.MAX_XML_BUFFER) {
        	Context.getLogger().warn("Changesets can not be collected as there are too many of them.");
        	return changes;
        }
        
        String checkoutPath = getNormalizedUrls().getSecond().substring(getNormalizedUrls().getFirst().length());
        
        SAXReader reader = new SAXReader();
        try {
            Document doc = reader.read(new StringReader(buffer.toString()));
            for (Element logEntry: (List<Element>)doc.getRootElement().elements("logentry")) {
                long revision = Long.parseLong(logEntry.attribute("revision").getText());
                if (revision <= Long.parseLong(startRevision.getValue()) || 
                		revision > Long.parseLong(endRevision.getValue())) {
                    continue;
                }
                Changeset change = new Changeset();
                String dateString = logEntry.element("date").getText();
                Date revisionDate = OUTPUT_DATETIME_FORMATTER.parseDateTime(
                		dateString.substring(0, dateString.indexOf('Z') - 3)).toDate();
                change.setDate(revisionDate);
                change.setId(String.valueOf(revision));

                Element authorElement = logEntry.element("author");
                if (authorElement != null) 
                    change.setUser(authorElement.getText());
                String comment = logEntry.element("msg").getText();
                if (StringUtils.isNotBlank(comment))
                    change.setComment(comment);
                for (Element path: (List<Element>)logEntry.element("paths").elements("path")) {
                	String pathText = path.getText().trim();
                	if (pathText.startsWith(checkoutPath)) {
                		String relativePath = pathText.substring(checkoutPath.length());
                		String[] parts = StringUtils.split(relativePath, "/");
                		if (isRecursive() || parts.length == 1) {
                            Modification modification = new Modification();
                            String actionText = path.attribute("action").getText();
                            if (actionText.equals("A"))
                            	modification.setAction(Modification.Action.ADD);
                            else if (actionText.equals("M") || actionText.equals("R"))
                            	modification.setAction(Modification.Action.MODIFY);
                            else if (actionText.equals("D"))
                            	modification.setAction(Modification.Action.DELETE);
                            else
                            	Context.getLogger().warn("Unrecognized action: " + actionText);
                            modification.setPath(pathText);
                            modification.setEdition(change.getId());
                            long previousRevision = Long.parseLong(change.getId()) - 1;
                            if (previousRevision != 0)
                            	modification.setPreviousEdition(String.valueOf(previousRevision));
                            if (modification.getAction() != null)
                            	change.getModifications().add(modification);
                		}
                	}
                }
                if (!change.getModifications().isEmpty())
                	changes.add(change);
            }
        } catch (DocumentException e) {
            throw new RuntimeException(e);
        }
        
        return changes;
    }
	
	String decodeUrl(String url) {
		return StringUtils.replace(url, "%20", " ");
	}

	@Override
	@ScriptApi
	public SourceViewSupport<SvnRevision> getSourceViewSupport() {
		return new SvnSourceViewSupport(this);
	}

	@SuppressWarnings("unchecked")
	@Override
	protected boolean isQuietSince(Date date) {
		String url = getUrl();
        Commandline cmdline = buildSvnCmd().addArgValue("log")
        		.addArgValue(url).addArgValue("--non-interactive");
		if (isSupportTrustServerCert())
			cmdline.addArgValue("--trust-server-cert");
        
        addAuthenticationSwitches(cmdline, getUserName(), getPassword());
        cmdline.addArgLine("--xml -r").addArgValue("{" + 
        		INPUT_DATETIME_FORMATTER.print(new DateTime(date)) + "}:{" + 
        		INPUT_DATETIME_FORMATTER.print(new DateTime()) + "}");
        final StringBuffer buffer = new StringBuffer();
    	
        cmdline.execute(new LineConsumer(Constants.UTF8) {

			@Override
			public void consume(String line) {
                buffer.append(line);   
                buffer.append("\n");
			}
        	
        }, new LineConsumer.WarnLogger()).checkReturnCode();
        
        SAXReader reader = new SAXReader();
        try {
            Document doc = reader.read(new StringReader(buffer.toString()));
            for (Element logEntry: (List<Element>)doc.getRootElement().elements("logentry")) {
                String dateString = logEntry.element("date").getText();
                Date revisionDate = OUTPUT_DATETIME_FORMATTER.parseDateTime(
                		dateString.substring(0, dateString.indexOf('Z') - 3)).toDate();
                if (revisionDate.after(date))
                	return false;
            }
        } catch (DocumentException e) {
            throw new RuntimeException(e);
        }
        return true;
    }

	@SuppressWarnings("unused")
	private void migrate1(VersionedDocument dom, Stack<Integer> versions) {
		Element rootUrlElement = dom.getRootElement().element("rootUrl");
		if (rootUrlElement != null)
			rootUrlElement.detach();
		dom.getRootElement().addElement("recursive").setText("true");
	}

	@SuppressWarnings("unused")
	private void migrate2(VersionedDocument dom, Stack<Integer> versions) {
		Element revisionElement = dom.getRootElement().element("revision");
		if (revisionElement != null)
			revisionElement.setName("buildRevision");
	}

	@SuppressWarnings("unused")
	private void migrate3(VersionedDocument dom, Stack<Integer> versions) {
		if (versions.empty()) {
			versions.push(0);
			versions.push(0);
		}
	}
	
	@SuppressWarnings("unused")
	private void migrate4(VersionedDocument dom, Stack<Integer> versions) {
		Element proofBuildSupportElement = dom.getRootElement().element("proofBuildSupport");
		if (proofBuildSupportElement != null) {
			proofBuildSupportElement.addElement("proofCondition").addAttribute("class", 
					"com.pmease.quickbuild.setting.repository.proofcondition.AlwaysProof");		
		}
	}

	public boolean isSupportTrustServerCert() {
		if (supportTrustServerCert == null) {
			Commandline cmdline = buildSvnCmd();
			cmdline.addArgLine("help co");
			
			cmdline.execute(new LineConsumer() {

				@Override
				public void consume(String line) {
					if (line.contains("--trust-server-cert"))
						supportTrustServerCert = true;
				}
				
			}, new LineConsumer.WarnLogger()).checkReturnCode();
			
			if (supportTrustServerCert == null)
				supportTrustServerCert = false;
		}
		return supportTrustServerCert;
	}
}
