MODULE SVNWebDAV; (** AUTHOR "rstoll"; *)

IMPORT
	WebHTTP, Files, Strings, Streams, Dates,
	XML, XMLObjects,KernelLog,
	SVNAdmin, SVNUtil, SVNOutput,
	OdSvn, OdXml;


	PROCEDURE Checkout* (svn : OdSvn.OdSvn; CONST pathName : ARRAY OF CHAR; CONST workName : ARRAY OF CHAR; VAR res : LONGINT );
	VAR
		name, err, vcc, repoUUID, bln, bc, version, workUrl : ARRAY 256 OF CHAR;
		props: WebHTTP.AdditionalField;
		ver, pos : LONGINT;
	BEGIN
		svn.checkout := TRUE;
		res := SVNOutput.ResOK;
		Files.SplitPath ( pathName, workUrl, name );

		svn.repositoryPathLength := 999; (* we don't want a update-target in the update request *)

		IF Files.Old ( workName ) = NIL THEN
			COPY ( pathName, workUrl );
			svn.FileUpdate ( FALSE );
		ELSE
			svn.FileUpdate ( TRUE );
		END;

		svn.Propfind ( workUrl, "D:version-controlled-configuration.D:resourcetype.D2:baseline-relative-path.D2:repository-uuid", props, err );

		IF (svn.pfStatus >= 400) OR (svn.pfStatus = 0) THEN
			PrintError ( svn, res );
			RETURN;
		END;

		IF ~WebHTTP.GetAdditionalFieldValue(props, "DAV:version-controlled-configuration", vcc) THEN END;
		IF ~WebHTTP.GetAdditionalFieldValue(props, "http://subversion.tigris.org/xmlns/dav/repository-uuid", repoUUID) THEN END;

		svn.Propfind ( vcc, "D:checked-in", props, err );
		IF ~WebHTTP.GetAdditionalFieldValue ( props, "DAV:checked-in", bln ) THEN END;

		svn.Propfind ( bln, "D:baseline-collection.D:version-name", props, err );
		IF ~WebHTTP.GetAdditionalFieldValue ( props, "DAV:baseline-collection", bc ) THEN END;
		IF ~WebHTTP.GetAdditionalFieldValue ( props, "DAV:version-name", version ) THEN END;

		IF err # "" THEN
			svn.context.out.String ( err ); svn.context.out.Ln;
			RETURN;
		END;

		Strings.StrToIntPos(version, ver, pos);

		svn.UpdateReport ( pathName, vcc, workUrl, -1, ver, workName, res );
		svn.checkout := FALSE;
	END Checkout;





	(* SVN update using .svn directories *)
	PROCEDURE Update* (svn : OdSvn.OdSvn; CONST pathName : ARRAY OF CHAR; pathNameVersion : LONGINT;  CONST workName : ARRAY OF CHAR; VAR res : LONGINT );
	VAR
		name, err, vcc, repoUUID, bln, bc, version, workUrl : ARRAY 256 OF CHAR;
		props: WebHTTP.AdditionalField;
		ver, pos : LONGINT;
	BEGIN
		res := SVNOutput.ResOK;
		Files.SplitPath ( pathName, workUrl, name );

		IF Files.Old ( workName ) = NIL THEN
			COPY ( pathName, workUrl );
			svn.FileUpdate ( FALSE );
		ELSE
			svn.FileUpdate ( TRUE );
		END;

		svn.traverseDummy := TRUE; (* first call *)
		SVNAdmin.Traverse ( workName, UpdateHandler, svn, FALSE, res ); (* don't read the version url from wcprops *)

		svn.Propfind ( workUrl, "D:version-controlled-configuration.D:resourcetype.D2:baseline-relative-path.D2:repository-uuid", props, err );

		IF (svn.pfStatus >= 400) OR (svn.pfStatus = 0) THEN
			PrintError ( svn, res );
			RETURN;
		END;

		IF ~WebHTTP.GetAdditionalFieldValue(props, "DAV:version-controlled-configuration", vcc) THEN END;
		IF ~WebHTTP.GetAdditionalFieldValue(props, "http://subversion.tigris.org/xmlns/dav/repository-uuid", repoUUID) THEN END;

		svn.Propfind ( vcc, "D:checked-in", props, err );
		IF ~WebHTTP.GetAdditionalFieldValue ( props, "DAV:checked-in", bln ) THEN END;

		svn.Propfind ( bln, "D:baseline-collection.D:version-name", props, err );
		IF ~WebHTTP.GetAdditionalFieldValue ( props, "DAV:baseline-collection", bc ) THEN END;
		IF ~WebHTTP.GetAdditionalFieldValue ( props, "DAV:version-name", version ) THEN END;

		IF err # "" THEN
			svn.context.out.String ( err ); svn.context.out.Ln;
			RETURN;
		END;

		Strings.StrToIntPos(version, ver, pos);

		svn.UpdateReport ( pathName, vcc, workUrl, pathNameVersion, ver, workName, res );
	END Update;

	PROCEDURE Commit* ( svn : OdSvn.OdSvn; CONST pathName, workName, message : ARRAY OF CHAR; VAR res : LONGINT );
	VAR
		act: OdSvn.Activity;
		uuid : Strings.String;
		name, err, vcc, bln, workUrl, wbl : ARRAY 256 OF CHAR;
		props, patch : WebHTTP.AdditionalField;
		resHeader: WebHTTP.ResponseHeader;
	BEGIN
		uuid := SVNUtil.GetUUID();
		NEW ( act, svn.client, uuid^ );
		ASSERT ( act # NIL );
		act.make;

		IF act.getUrl() = NIL THEN
			svn.pfStatus := 0;
			PrintError ( svn, res );
			RETURN;
		END;

		Files.SplitPath ( pathName, workUrl, name );

		IF Files.Old ( workName ) = NIL THEN
			COPY ( pathName, workUrl );
		END;

		svn.Propfind ( workUrl, "D:version-controlled-configuration", props, err );
		IF ~WebHTTP.GetAdditionalFieldValue(props, "DAV:version-controlled-configuration", vcc) THEN END;

		IF (svn.pfStatus >= 400) OR (svn.pfStatus = 0) THEN
			PrintError ( svn, res );
			RETURN;
		END;

		svn.Propfind ( vcc, "D:checked-in", props, err );
		IF ~WebHTTP.GetAdditionalFieldValue ( props, "DAV:checked-in", bln ) THEN END;

		svn.Checkout ( bln, resHeader, err );
		COPY ( resHeader.location, wbl );

		WebHTTP.SetAdditionalFieldValue ( patch, "log xmlns=http://subversion.tigris.org/xmlns/svn/", message );
		svn.Proppatch ( wbl, patch, err );

		svn.Propfind ( workUrl, "D:checked-in", props, err );
		IF ~WebHTTP.GetAdditionalFieldValue(props, "DAV:checked-in", svn.ver) THEN END;

		svn.Checkout ( svn.ver, resHeader, err );
		COPY ( resHeader.location, svn.wrk );

		svn.removeDir := FALSE;
		svn.countChanges := 0;
		SVNAdmin.Traverse ( workName, CommitHandler, svn, TRUE, res );

		(* don't merge if there are no modified files *)
		IF svn.countChanges > 0 THEN
			svn.Merge ( workUrl, act.getUrl(), resHeader, err );
			IF resHeader.statuscode = 200 THEN
				ParseMergeContent ( svn, workUrl, workName );
			END;
		END;

		act.delete;
	END Commit;


	PROCEDURE ParseMergeContent ( svn : OdSvn.OdSvn; CONST baseUrl, basePath : ARRAY OF CHAR );
	VAR
		root, e, e2 : XML.Element;
		enum : XMLObjects.Enumerator;
		str, md5 : Strings.String;
		p : ANY;
		xml : OdXml.OdXml;
		vcc, vurl, ver, tmp, tmp2, path, name, date : ARRAY 256 OF CHAR;
		creationdate, creator, status : ARRAY 33 OF CHAR;
		version : ARRAY 10 OF CHAR;
		len, res, ft,fd : LONGINT;
		adminDir : SVNAdmin.Entry;
		f : Files.File;
	BEGIN
		NEW ( xml );
		NEW ( adminDir, NIL );

		root := svn.resultDoc.GetRoot();
		str := root.GetName();

		IF str^ # "D:merge-response" THEN RETURN END;

		enum := root.GetContents();
		IF ~enum.HasMoreElements() THEN RETURN END;

		p := enum.GetNext();
		e := p ( XML.Element );
		str := e.GetName();
		IF str^ # "D:updated-set" THEN RETURN END;

		enum := e.GetContents();
		IF ~enum.HasMoreElements() THEN RETURN END;

		p := enum.GetNext();
		e := p ( XML.Element );
		str := e.GetName();
		IF str^ # "D:response" THEN RETURN END;

		e2 := xml.SplitElement ( e, "DAV:href" );
		ASSERT ( e2 # NIL );

		OdXml.GetCharData ( e2, vcc );

		e2 := xml.SplitElement ( e, "DAV:propstat.DAV:prop.DAV:creationdate" ); ASSERT ( e2 # NIL );
		OdXml.GetCharData ( e2, creationdate );

		e2 := xml.SplitElement ( e, "DAV:propstat.DAV:prop.DAV:version-name" ); ASSERT ( e2 # NIL );
		OdXml.GetCharData ( e2, version );

		e2 := xml.SplitElement ( e, "DAV:propstat.DAV:prop.DAV:creator-displayname" ); ASSERT ( e2 # NIL );
		OdXml.GetCharData ( e2, creator );

		len := Strings.Length ( baseUrl );
		ASSERT ( ~Strings.EndsWith ( "/", basePath ) );

		WHILE enum.HasMoreElements() DO
			p := enum.GetNext();
			IF p IS XML.Element THEN
				e := p ( XML.Element );
				str := e.GetName();

				e2 := xml.SplitElement ( e, "DAV:href" ); ASSERT ( e2 # NIL );
				OdXml.GetCharData ( e2, vurl );

				(*e2 := xml.SplitElement ( e, "DAV:propstat.DAV:prop.DAV:resourcetype.DAV:collection" );
				IF e2 = NIL THEN
					(*KernelLog.String ( "NIL collection" );*)
				ELSE
					(*KernelLog.String ( "NOT NIL collection" );*)
				END;*)

				e2 := xml.SplitElement ( e, "DAV:propstat.DAV:prop.DAV:checked-in.DAV:href" ); ASSERT ( e2 # NIL );
				OdXml.GetCharData ( e2, ver );

				e2 := xml.SplitElement ( e, "DAV:propstat.DAV:status" ); ASSERT ( e2 # NIL );
				OdXml.GetCharData ( e2, status );

				IF Strings.Match ( "HTTP/1.? 200 OK", status ) THEN
					str := Strings.Substring2 ( len, vurl );
					SVNUtil.UrlDecode ( str^, tmp2 );
					Strings.Concat ( basePath, tmp2, tmp );

					adminDir.SetPath ( tmp, res ); ASSERT ( res = SVNOutput.ResOK );
					adminDir.CreateTempfile;

					f := Files.Old ( tmp );
					IF f # NIL THEN
						Files.SplitPath ( tmp, path, name );
						IF adminDir.IsItemVersioned ( name ) THEN
							adminDir.ReadWriteLines ( 1 );
							adminDir.ReadWriteString ( version );
							adminDir.ReadWriteLines ( 2 );
							adminDir.ReadWriteString ( "" ); (* remove scheduled stuff *)
							adminDir.ReadWriteString ( creationdate );
							md5 := SVNUtil.GetChecksum ( tmp );
							adminDir.ReadWriteString ( md5^ );
							f.GetDate ( ft, fd );
							Strings.FormatDateTime ( SVNOutput.DateFormat, Dates.OberonToDateTime ( fd, ft ), date );
							adminDir.ReadWriteString ( date );
							adminDir.ReadWriteString ( version );
							adminDir.ReadWriteString ( creator );
							adminDir.ReadWriteRest;
							adminDir.WriteUpdate;

							SVNAdmin.CopyToBaseFile ( tmp );

							SVNAdmin.WriteWCPROPS ( path, name, ver );
						ELSE
							svn.context.out.String ( "ERROR: received merge request, but item is not versioned" );
							svn.context.out.Ln;
						END;
					ELSE
						adminDir.ReadWriteLines ( 3 );
						adminDir.ReadWriteString ( version );
						adminDir.ReadWriteLines ( 2 );
						adminDir.ReadWriteString ( "" ); (* remove scheduled stuff *)
						adminDir.ReadWriteLines ( 2 );
						adminDir.ReadWriteString ( creationdate ); (* correct? *)
						adminDir.ReadWriteString ( version );
						adminDir.ReadWriteString ( creator );

						(* remove schedule entries *)
						adminDir.ReadWriteToEOE;
						WHILE ~adminDir.IsEOF () DO
							adminDir.ReadWriteLines ( 1 );
							adminDir.ReadWriteLine ( tmp2 );
							IF tmp2 = "dir" THEN
								adminDir.ReadWriteEOE;
							ELSE
								adminDir.ReadWriteToEOE;
							END;
						END;
						adminDir.WriteUpdate;

						SVNAdmin.WriteWCPROPS ( tmp, "", ver );
					END;
				END;
			END;
		END;
	END ParseMergeContent;

	PROCEDURE UpdateHandler* ( CONST path : ARRAY OF CHAR; entry : SVNAdmin.EntryEntity; data : ANY ) : BOOLEAN;
	VAR
		svn : OdSvn.OdSvn;
	BEGIN
		(* TODO:
			- search for files/directories which are on a separate revision
			- search for missing resources..and restore them:
				- dirs: neuer request!
				- files: restore von base copy
		*)
(*		KernelLog.String ( "path: " );
		KernelLog.String ( path );
		KernelLog.String ( "//" );
		KernelLog.String ( entry.Name );
		KernelLog.Ln;*)

		svn := data ( OdSvn.OdSvn );

		IF svn.traverseDummy THEN
			svn.traverseDummy := FALSE;
			svn.repositoryPathLength := Strings.Length ( entry.RepositoryRoot ) - Strings.IndexOfByte ( '/', 7, entry.RepositoryRoot );
		END;

		RETURN TRUE;
	END UpdateHandler;

	PROCEDURE CommitHandler* ( CONST path : ARRAY OF CHAR; entry : SVNAdmin.EntryEntity; data : ANY ) : BOOLEAN;
	VAR
		str : Strings.String;
		err, wrk, tmp2 : ARRAY 256 OF CHAR;
		props: WebHTTP.AdditionalField;
		svn : OdSvn.OdSvn;
		res : LONGINT;
		resHeader: WebHTTP.ResponseHeader;
		m : SVNOutput.Message;
	BEGIN
		svn := data ( OdSvn.OdSvn );

		NEW ( str, 256 );
		str := Strings.Substring2 ( Strings.Length ( entry.RepositoryRoot ), entry.Url );
		IF str^[0] = Files.PathDelimiter THEN str := Strings.Substring2 ( 1, str^ ) END;

		IF ~svn.removeDir THEN
			svn.removeDir := TRUE;
			Strings.Truncate ( svn.wrk, Strings.Length(svn.wrk) - Strings.Length(str^) );
		END;

		Strings.Concat ( svn.wrk, str^, tmp2 );
		SVNUtil.UrlEncode ( tmp2, wrk );

		IF entry.NodeKind = "file" THEN
			Files.JoinPath ( path, entry.Name, tmp2 );

			IF entry.Schedule = "add" THEN (* DONE *)
				svn.Propfind ( wrk, "D:version-controlled-configuration.D2:repository-uuid", props, err );
				IF svn.pfStatus = 404 THEN (* 404 = not found *)
					svn.context.out.String ( "Adding " ); svn.context.out.String ( tmp2 ); svn.context.out.Ln;

					Put ( wrk, tmp2, svn, res );
					ExpectedResult ( 201, svn, wrk, tmp2, "add file" ); (* 201 = created *)
				ELSE
					svn.context.out.String ( " ERROR: " ); svn.context.out.String ( wrk ); svn.context.out.String ( " is already on the server!" ); svn.context.out.Ln;
					svn.countChanges := 0; (* abort commit *)
					RETURN FALSE;
				END;
			ELSIF entry.Schedule = "delete" THEN
				(* TODO: remove entry in entries file and all-wcprops file: there is no report packet !!!  *)
				(*IF ~entry.GlobalRemoval THEN (* file will be deleted anyway if the top directory is removed *)
					svn.context.out.String ( "Deleting " ); svn.context.out.String ( tmp2 ); svn.context.out.Ln;

					Delete ( wrk, svn, res );
					ExpectedResult ( 204, svn, wrk, tmp2, "delete file" ); (* 204 = no content *)
				END;*)
			ELSE (* DONE *)
				IF ~SVNUtil.CheckChecksum ( tmp2, entry.Checksum ) THEN
					svn.context.out.String ( "Sending " );
					svn.context.out.String ( tmp2 );
					svn.context.out.Ln;

					svn.Checkout ( entry.VersionUrl, resHeader, err );
					IF resHeader.statuscode >= 400 THEN
						NEW ( m, svn.context );
						IF resHeader.statuscode = 409 THEN
							m.Print ( SVNOutput.ResCOMMITOUTOFDATE, tmp2 );
						ELSE
							svn.context.out.String ( "HTTP error! Statuscode: " ); svn.context.out.Int ( resHeader.statuscode, 0 );
							svn.context.out.Ln;
							m.Print ( SVNOutput.ResCOMMITUNSPECIFIED, tmp2 );
						END;

						svn.countChanges := 0; (* abort commit *)
						RETURN FALSE;
					ELSE
						Put ( wrk, tmp2, svn, res );
						ExpectedResult ( 204, svn, wrk, tmp2, "add file" ); (* 204 = no content *)
					END;
				END;
			END;
		ELSE
			IF entry.Schedule = "add" THEN (* DONE *)
				svn.context.out.String ( "Adding " ); svn.context.out.String ( path ); svn.context.out.Ln;

				Mkcol ( wrk, svn, res );
				ExpectedResult ( 201, svn, wrk, tmp2, "add directory" );
			ELSIF entry.Schedule = "delete" THEN
				(* TODO: remove entry in entries file *)
				(*svn.context.out.String ( "Deleting " ); svn.context.out.String ( tmp2 ); svn.context.out.Ln;

				Delete ( wrk, svn, res );
				ExpectedResult ( 204, svn, wrk, tmp2, "delete file" ); (* 204 = no content *)
				*)
			ELSE
				(* do nothing *)
			END;
		END;

		RETURN TRUE;
	END CommitHandler;

	PROCEDURE ExpectedResult ( status : LONGINT; svn : OdSvn.OdSvn; CONST wrk, lcl, message : ARRAY OF CHAR );
	BEGIN
		IF svn.pfStatus # status THEN
			svn.context.out.String ( "ERROR: failed to " );
			svn.context.out.String ( message );
			svn.context.out.String ( ": " );
			svn.context.out.String ( lcl );
			svn.context.out.Ln;
			svn.context.out.String ( "url: " );
			svn.context.out.String ( wrk );
			svn.context.out.Ln;
		ELSE
			INC ( svn.countChanges );
		END;
	END ExpectedResult;

	PROCEDURE Mkcol* ( CONST url : ARRAY OF CHAR; svn : OdSvn.OdSvn; VAR res : LONGINT );
	VAR
		resHeader: WebHTTP.ResponseHeader;
		out : Streams.Reader;
	BEGIN
		svn.client.Mkcol ( url, resHeader, out, res );
		svn.pfStatus := resHeader.statuscode;
	END Mkcol;

	PROCEDURE Delete* ( CONST url : ARRAY OF CHAR; svn : OdSvn.OdSvn; VAR res : LONGINT );
	VAR
		resHeader: WebHTTP.ResponseHeader;
		out : Streams.Reader;
	BEGIN
		svn.client.Delete ( url, resHeader, out, res );
		svn.pfStatus := resHeader.statuscode;
	END Delete;

	(* sends 'workName' to the svn.wrk address *)
	PROCEDURE Put* ( CONST workUrl, workName: ARRAY OF CHAR; svn : OdSvn.OdSvn; VAR res : LONGINT );
	VAR
		f : Files.File;
		resHeader: WebHTTP.ResponseHeader;
		reqHeader: WebHTTP.RequestHeader;
		in : Files.Reader;
		out : Streams.Reader;
		lenStr: ARRAY 10 OF CHAR;
		m : SVNOutput.Message;
	BEGIN
		f := Files.Old(workName);
		IF f # NIL THEN
			Files.OpenReader ( in, f, 0 );

			WebHTTP.SetAdditionalFieldValue ( reqHeader.additionalFields, "Content-Type", "application/octet-stream" );
			Strings.IntToStr ( f.Length(), lenStr );
			WebHTTP.SetAdditionalFieldValue ( reqHeader.additionalFields, "Content-Length", lenStr );

			svn.client.Put ( workUrl, reqHeader, resHeader, out, in, res );
			svn.pfStatus := resHeader.statuscode;
		ELSE
			svn.context.out.String ( " ERROR: PUT " );

			NEW ( m, svn.context );
			m.Print ( SVNOutput.ResFILENOTFOUND, workName );
		END;
	END Put;

	PROCEDURE PrintError ( svn : OdSvn.OdSvn; VAR res : LONGINT );
	BEGIN
		svn.context.out.String ( "Server response: " ); svn.context.out.Int ( svn.pfStatus, 0 ); svn.context.out.Ln;
		IF svn.pfStatus = 401 THEN
			res := SVNOutput.ResNOTAUTHORIZED;
		ELSE
			res := SVNOutput.ResUNEXPECTEDSERVERRESPONSE;
		END;
	END PrintError;

END SVNWebDAV.