/*
 * $Header: /home/cvspublic/jakarta-slide/src/webdav/server/org/apache/slide/webdav/method/LockMethod.java,v 1.75 2005/02/23 12:42:37 luetzkendorf Exp $
 * $Revision: 1.75 $
 * $Date: 2005/02/23 12:42:37 $
 *
 * ====================================================================
 *
 * Copyright 1999-2002 The Apache Software Foundation 
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 */

package org.apache.slide.webdav.method;

import java.io.IOException;
import java.io.StringWriter;
import java.io.Writer;
import java.util.Date;
import java.util.Enumeration;
import java.util.Iterator;
import java.util.List;

import javax.transaction.Transaction;

import org.apache.slide.common.NamespaceAccessToken;
import org.apache.slide.common.NamespaceConfig;
import org.apache.slide.common.ServiceAccessException;
import org.apache.slide.common.SlideException;
import org.apache.slide.content.NodeRevisionContent;
import org.apache.slide.content.NodeRevisionDescriptor;
import org.apache.slide.content.NodeRevisionDescriptors;
import org.apache.slide.content.RevisionAlreadyExistException;
import org.apache.slide.content.RevisionDescriptorNotFoundException;
import org.apache.slide.event.EventDispatcher;
import org.apache.slide.event.VetoException;
import org.apache.slide.lock.NodeLock;
import org.apache.slide.lock.ObjectIsAlreadyLockedException;
import org.apache.slide.lock.ObjectLockedException;
import org.apache.slide.security.AccessDeniedException;
import org.apache.slide.structure.LinkedObjectNotFoundException;
import org.apache.slide.structure.ObjectAlreadyExistsException;
import org.apache.slide.structure.ObjectNotFoundException;
import org.apache.slide.structure.SubjectNode;
import org.apache.slide.transaction.ExternalTransactionContext;
import org.apache.slide.util.XMLValue;
import org.apache.slide.webdav.WebdavException;
import org.apache.slide.webdav.WebdavServletConfig;
import org.apache.slide.webdav.event.WebdavEvent;
import org.apache.slide.webdav.util.PropertyHelper;
import org.apache.slide.webdav.util.WebdavConstants;
import org.apache.slide.webdav.util.WebdavStatus;
import org.jdom.Element;
import org.jdom.JDOMException;
import org.jdom.output.XMLOutputter;

/**
 * LOCK method.
 * 
 */
public class LockMethod extends AbstractMultistatusResponseMethod implements
		WebdavConstants, WriteMethod, FineGrainedLockingMethod {

	// -------------------------------------------------------------- Constants

	/**
	 * Create a new lock.
	 */
	private static final int LOCK_CREATION = 0;

	/**
	 * Refresh lock.
	 */
	private static final int LOCK_REFRESH = 1;

	/**
	 * Maximum and default timeout.
	 */
	private static final int MAX_TIMEOUT = Integer.MAX_VALUE;

	private static final int DEFAULT_TIMEOUT = MAX_TIMEOUT;

	/**
	 * The default owner if not explicitely specified by the request.
	 */
	public static final String DEFAULT_LOCK_OWNER = "";

	// ----------------------------------------------------- Instance Variables

	/**
	 * Depth.
	 */
	private int depth;

	/**
	 * Type of the LOCK method ({@link #LOCK_CREATION}or {@link #LOCK_REFRESH}).
	 */
	private int lockType;

	/**
	 * Lock duration.
	 */
	private int lockDuration = DEFAULT_TIMEOUT;

	/**
	 * Lock scope.
	 */
	private String lockInfo_lockScope;

	/*
	 * Lock type.
	 */
	private String lockInfo_lockType;

	/**
	 * Lock owner.
	 */
	private String lockInfo_lockOwner;

	/**
	 * Lock subject.
	 */
	private String lockInfo_lockSubject;

	/**
	 * The PropertyHelper used by this instance.
	 */
	protected PropertyHelper propertyHelper = null;

	// ----------------------------------------------------------- Constructors

	/**
	 * Constructor.
	 * 
	 * @param token
	 *            the token for accessing the namespace
	 * @param config
	 *            configuration of the WebDAV servlet
	 */
	public LockMethod(NamespaceAccessToken token, WebdavServletConfig config) {
		super(token, config);
	}

  /**
   * @see org.apache.slide.webdav.method.FineGrainedLockingMethod#acquireFineGrainLocks()
   */
  public void acquireFineGrainLocks() {
      acquireStandardLocks(lockInfo_lockSubject);
      // changes this
      acquireLock(lockInfo_lockSubject, WRITE_LOCK);
  }

  // ------------------------------------------------------ Protected Methods

	/**
	 * Parse request.
	 * 
	 * @exception WebdavException
	 *                Does not happen
	 */
	protected void parseRequest() throws WebdavException {

		propertyHelper = PropertyHelper.getPropertyHelper(slideToken, token,
				getConfig());
		//        readRequestContent();

		// Loads the associated object from the store.
		lockInfo_lockSubject = requestUri;
		if (lockInfo_lockSubject == null) {
			lockInfo_lockSubject = "/";
		}

		depth = requestHeaders.getDepth(INFINITY);
		if (depth != 0 && depth != INFINITY) {
			int sc = WebdavStatus.SC_PRECONDITION_FAILED;
			sendError(sc, "Invalid header Depth: " + depth);
			throw new WebdavException(sc);
		}

		lockDuration = requestHeaders.getTimeout(MAX_TIMEOUT);

		if (req.getContentLength() > 0 || isRequestChunked()) {
			parseLockInfo();
		} else {
			lockType = LOCK_REFRESH;
		}

	}

	/**
	 * Parses the <code>&lt;lockinfo&gt;</code> request content document.
	 * 
	 * @throws WebdavException
	 *             if parsing the request failed or if the request is not valid.
	 */
	private void parseLockInfo() throws WebdavException {

		lockType = LOCK_CREATION;

		try {
			Iterator childrenIterator = parseRequestContent(E_LOCKINFO)
					.getChildren().iterator();
			while (childrenIterator.hasNext()) {
				Element currentElement = (Element) childrenIterator.next();
				if (E_LOCKSCOPE.equals(currentElement.getName())) {
					parseLockScope(currentElement);
				} else if (E_LOCKTYPE.equals(currentElement.getName())) {
					parseLockType(currentElement);
				} else if (E_OWNER.equals(currentElement.getName())) {
					parseOwner(currentElement);
				}
			}
		} catch (JDOMException e) {
			int statusCode = WebdavStatus.SC_BAD_REQUEST;
			sendError(statusCode, e);
			throw new WebdavException(statusCode);
		} catch (IOException e) {
			int statusCode = WebdavStatus.SC_INTERNAL_SERVER_ERROR;
			sendError(statusCode, e);
			throw new WebdavException(statusCode);
		}
	}

	/**
	 * Parses the <code>&lt;lockscope&gt;</code> part of the request content
	 * document.
	 * 
	 * @param lockScopeElement
	 *            the <code>&lt;lockscope&gt;</code> to parse.
	 * 
	 * @throws JDOMException
	 *             if parsing the request failed or if the request is not valid.
	 */
	private void parseLockScope(Element lockScopeElement) throws JDOMException {
		if (lockScopeElement == null) {
			throw new JDOMException("Expected <" + E_LOCKSCOPE + "> element");
		}

		List children = lockScopeElement.getChildren();
		if (children.size() != 1) {
			throw new JDOMException("<" + E_LOCKSCOPE
					+ "> must have exactly one child element");
		}

		lockInfo_lockScope = ((Element) children.get(0)).getName();
		if (!(E_EXCLUSIVE.equals(lockInfo_lockScope)
				|| E_SHARED.equals(lockInfo_lockScope) || E_LOCAL
				.equals(lockInfo_lockScope))) {
			throw new JDOMException("<" + E_LOCKSCOPE
					+ "> can only contain one of <" + E_EXCLUSIVE + "> or <"
					+ E_SHARED + "> or <" + E_LOCAL + ">");
		}
	}

	/**
	 * Parses the <code>&lt;locktype&gt;</code> part of the request content
	 * document.
	 * 
	 * @param lockTypeElement
	 *            the <code>&lt;locktype&gt;</code> to parse.
	 * 
	 * @throws JDOMException
	 *             if parsing the request failed or if the request is not valid.
	 */
	private void parseLockType(Element lockTypeElement) throws JDOMException {

		if (lockTypeElement == null) {
			throw new JDOMException("Expected <" + E_LOCKTYPE + "> element");
		}

		List children = lockTypeElement.getChildren();
		if (children.size() != 1) {
			throw new JDOMException("<" + E_LOCKTYPE
					+ "> must have exactly one child element");
		}
		String lockTypeName = ((Element) children.get(0)).getName();
		if (!E_WRITE.equals(lockTypeName)
				&& !E_TRANSACTION.equals(lockTypeName)) {
			throw new JDOMException(
					"Only write and transaction locks are supported");
		}
		lockInfo_lockType = ((Element) children.get(0)).getName();
	}

	/**
	 * Parses the <code>&lt;owner&gt;</code> part of the request content
	 * document.
	 * 
	 * @param ownerElement
	 *            the <code>&lt;owner&gt;</code> to parse.
	 * 
	 * @throws JDOMException
	 *             if parsing the request failed or if the request is not valid.
	 */
	private void parseOwner(Element ownerElement) throws JDOMException {

		if (ownerElement == null) {
			lockInfo_lockOwner = DEFAULT_LOCK_OWNER;
			return;
			//          throw new JDOMException("Expected <"+E_OWNER+"> element");
		}

		StringWriter stringWriter = new StringWriter();
		XMLOutputter xmlOutputter = new XMLOutputter();
		try {
			xmlOutputter.outputElementContent(ownerElement, stringWriter);
		} catch (IOException e) {
			// this should not happen since we do no "real" I/O but
			// only print to a PrintWriter
			e.printStackTrace();
		}
		lockInfo_lockOwner = stringWriter.toString();

		if (lockInfo_lockOwner.length() == 0) {
			lockInfo_lockOwner = DEFAULT_LOCK_OWNER;
			//throw new JDOMException("<"+E_OWNER+"> element must not be
			// empty");
		}
	}

	/**
	 * Execute request.
	 * 
	 * @exception WebdavException
	 *                Unrecoverable error while renewing lock
	 */
	protected void executeRequest() throws WebdavException {

		// Prevent dirty reads
		slideToken.setForceStoreEnlistment(true);

		SubjectNode toLockSubject = null;
		boolean isCollection = isCollection(lockInfo_lockSubject);
		boolean inheritance = false;
		Date lockDate = null;

        
		switch (lockType) {

		case LOCK_CREATION:
			if (lockInfo_lockType.equals(E_TRANSACTION)) {
				try {
					NamespaceConfig namespaceConfig = token
					.getNamespaceConfig();
                    toLockSubject = getToLockSubject();
                    if (lockDate == null)
                        lockDate = new Date((new Date()).getTime()
                                + (lockDuration * 1000L));
					NodeLock lockToken = new NodeLock(toLockSubject.getUri(),((SubjectNode)security.getPrincipal(slideToken)).getUri(),
							namespaceConfig.getCreateObjectAction().getUri(), lockDate,
							inheritance, NodeLock.LOCAL, lockInfo_lockOwner);
                    token.setTransactionTimeout(lockDuration * 1000);
					Transaction transaction = token.getTransactionManager().suspend();
                    //String txId = lockToken.getLockId();
                    String fullTxId = "<" + S_LOCK_TOKEN + lockToken.getLockId() + ">";
                    ExternalTransactionContext.registerContext(fullTxId, transaction);
                    slideToken.setExternalTx();
                    resp.setHeader("Lock-Token", fullTxId);
                    showLockDiscoveryInfo(lockToken);
				} catch (Exception e) {
					int statusCode = getErrorCode(e);
					sendError(statusCode, e);
					throw new WebdavException(statusCode);
				}
			} else if (lockInfo_lockType.equals(E_WRITE)) {
				try {
                    
	                if (!checkIfHeaders()) {
	                    return;
	                }
	                      
					NamespaceConfig namespaceConfig = token.getNamespaceConfig();
                    toLockSubject = getToLockSubject();
					NodeLock lockToken = null;

					inheritance = (depth != 0);
					boolean exclusive = !(lockInfo_lockScope.equals(E_SHARED));

					if (lockDate == null)
						lockDate = new Date((new Date()).getTime()
								+ (lockDuration * 1000L));

					lockToken = new NodeLock(toLockSubject,
							(SubjectNode) security.getPrincipal(slideToken),
							namespaceConfig.getCreateObjectAction(), lockDate,
							inheritance, exclusive, lockInfo_lockOwner);
					
					if (WebdavEvent.LOCK.isEnabled() ) {
					    EventDispatcher.getInstance().fireVetoableEvent(
					            WebdavEvent.LOCK, new WebdavEvent(this));
					}
					
					lock.lock(slideToken, lockToken);

					// Set the lock-token header
					// [RFC 2518, 9.5] " The Lock-Token response header is used
					// with the LOCK method to indicate the lock token created
					// as
					// a result of a successful LOCK request to create a new
					// lock."
					resp.setHeader("Lock-Token", "<" + S_LOCK_TOKEN
							+ lockToken.getLockId() + ">");

					resp.setStatus(WebdavStatus.SC_OK);

					// The lock token on which the DAV module will have the info

					showLockDiscoveryInfo(lockToken);
				} catch (ObjectIsAlreadyLockedException e) {
					if (inheritance
							&& generateMultiStatusResponse(isCollection, e,
									requestUri)) {
						// error is on the resource which we attempted to lock
						String errorMessage = generateErrorMessage(e);
						// Write it on the servlet writer
						resp.setContentType(TEXT_XML_UTF_8);
						resp.setStatus(WebdavStatus.SC_MULTI_STATUS);
						try {
							resp.getWriter().write(errorMessage);
						} catch (IOException ex) {
							// Critical error ... Servlet container is dead or
							// something
							int statusCode = WebdavStatus.SC_INTERNAL_SERVER_ERROR;
							sendError(statusCode, e);
							throw new WebdavException(statusCode);
						}
					} else {
						// Returning 207 on non-collection requests is generally
						// considered bad. So let's not do it, since this way
						// makes clients generally behave better.
						resp.setStatus(WebdavStatus.SC_LOCKED);
					}
					//
					// make sure the transaction is aborted
					// throw any WebDAV exception to indicate the transaction
					// wants to be aborted
					//
					throw new WebdavException(WebdavStatus.SC_ACCEPTED, false);
				} catch (Exception e) {
					int statusCode = getErrorCode(e);
					sendError(statusCode, e);
					throw new WebdavException(statusCode);
				}
			}
			break;

		case LOCK_REFRESH:

			try {

				Enumeration lockTokens = lock.enumerateLocks(slideToken,
						lockInfo_lockSubject, false);

				NodeLock currentLockToken = null;
				Date newExpirationDate = new Date((new Date()).getTime()
						+ (lockDuration * 1000L));
				while (lockTokens.hasMoreElements()) {
					currentLockToken = (NodeLock) lockTokens.nextElement();
					lock.renew(slideToken, currentLockToken, newExpirationDate);
				}

				showLockDiscoveryInfo(currentLockToken);

			} catch (SlideException e) {
				int statusCode = WebdavStatus.SC_PRECONDITION_FAILED;
				sendError(statusCode, e);
				throw new WebdavException(statusCode);
			}

			break;

		}

	}

    protected SubjectNode getToLockSubject() throws ObjectAlreadyExistsException, ObjectNotFoundException,
            AccessDeniedException, RevisionAlreadyExistException, LinkedObjectNotFoundException, ObjectLockedException,
            ServiceAccessException, VetoException {
        
        SubjectNode toLockSubject;
        try {
            toLockSubject = (SubjectNode) structure.retrieve(slideToken, lockInfo_lockSubject);
        } catch (ObjectNotFoundException ex) {

            // Creating a lock null resource
            toLockSubject = new SubjectNode();

            // Creating new subject
            structure.create(slideToken, toLockSubject, lockInfo_lockSubject);

            NodeRevisionDescriptor revisionDescriptor = new NodeRevisionDescriptor(0);

            // Resource type
            XMLValue lockNull = new XMLValue(new Element(E_LOCKNULL, DNSP));
            revisionDescriptor.setResourceType(lockNull.toString());

            NodeRevisionContent nrc = new NodeRevisionContent();
            nrc.setContent(new byte[0]);

            // Creating the revision descriptor
            content.create(slideToken, lockInfo_lockSubject, revisionDescriptor, nrc);
        }
        return toLockSubject;
    }
    
	/**
     * Get return status based on exception type.
     */
	protected int getErrorCode(Exception ex) {
		try {
			throw ex;
		} catch (ObjectNotFoundException e) {
			return WebdavStatus.SC_PRECONDITION_FAILED;
		} catch (Exception e) {
			return super.getErrorCode(e);
		}
	}

	/**
	 * Show lockdiscovery info.
	 * 
	 * @exception WebdavException
	 *                Something is wrong with the servlet container
	 */
	protected void showLockDiscoveryInfo(NodeLock token) throws WebdavException {

		// Generating XML response
		org.jdom.Element prop = new org.jdom.Element(E_PROP, DNSP);
		org.jdom.Element lockdiscovery = new org.jdom.Element(E_LOCKDISCOVERY,
				DNSP);
		prop.addContent(lockdiscovery);
		XMLValue xmlValue = propertyHelper.computeLockDiscovery(token,
				getSlideContextPath());
		Iterator iterator = xmlValue.iterator();
		while (iterator.hasNext()) {
			lockdiscovery.addContent((org.jdom.Element) iterator.next());
		}

		try {
			//System.out.println("Query result");
			//System.out.println(generatedXML.toString());
			resp.setContentType(TEXT_XML_UTF_8);
			Writer writer = resp.getWriter();
			org.jdom.output.Format format = org.jdom.output.Format
					.getPrettyFormat();
			format.setIndent(XML_RESPONSE_INDENT);
			new org.jdom.output.XMLOutputter(format).output(
					new org.jdom.Document(prop), writer);
			writer.flush();
		} catch (Exception e) {
			int statusCode = WebdavStatus.SC_INTERNAL_SERVER_ERROR;
			sendError(statusCode, e);
			throw new WebdavException(statusCode);
		}

	}
    
   private boolean checkIfHeaders() throws AccessDeniedException, LinkedObjectNotFoundException, ServiceAccessException, ObjectLockedException, VetoException, IOException
   {
       try {
           NodeRevisionDescriptors revisionDescriptors =
                      content.retrieve(slideToken, this.requestUri);
           // Retrieve latest revision descriptor
           NodeRevisionDescriptor revisionDescriptor =
                      content.retrieve(slideToken, revisionDescriptors);
           if (revisionDescriptor != null) {
               ResourceInfo resourceInfo =
                     new ResourceInfo(this.requestUri, revisionDescriptor);
               return checkIfHeaders(req, resp, resourceInfo);
           } else {
               return checkIfHeaders(req, resp, new ResourceInfo(this.requestUri));
           }
       } 
       catch (RevisionDescriptorNotFoundException e) {
           return checkIfHeaders(req, resp, new ResourceInfo(this.requestUri));
       }
       catch (ObjectNotFoundException e) {
           return checkIfHeaders(req, resp, new ResourceInfo(this.requestUri));
       }
   }
}

