/*
 * $Header: /home/cvspublic/jakarta-slide/src/stores/org/apache/slide/store/impl/rdbms/DB2RDBMSAdapter.java,v 1.12 2005/05/19 13:01:32 unico Exp $
 * $Revision: 1.12 $
 * $Date: 2005/05/19 13:01:32 $
 *
 * ====================================================================
 *
 * Copyright 2004 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.store.impl.rdbms;

import org.apache.slide.common.*;
import org.apache.slide.content.*;
import org.apache.slide.lock.NodeLock;
import org.apache.slide.security.NodePermission;
import org.apache.slide.store.ConcurrencyConflictError;
import org.apache.slide.structure.LinkNode;
import org.apache.slide.structure.ObjectNode;
import org.apache.slide.structure.ObjectNotFoundException;
import org.apache.slide.util.logger.Logger;

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.Enumeration;
;

/**
 * Adapter for DB2 8.
 * @version $Revision: 1.12 $
 */

public class DB2RDBMSAdapter extends CommonRDBMSAdapter
 {
// The reason This adapter is needed is because DB2 does not like parameter markers in select columns. Ex from StandardRDBMSAdapter:
// "insert into BINDING (URI_ID, NAME, CHILD_UURI_ID) select ?, ?, URI_ID from URI where URI_STRING = ?"

    public DB2RDBMSAdapter(Service service, Logger logger) {
        super(service, logger);
        bcompress = false;
    }


    protected boolean storeObject(Connection connection, Uri uri, ObjectNode object, boolean create)
        throws ServiceAccessException {
        String className = object.getClass().getName();
        long uriid;
        try {
            PreparedStatement statement = null;
            ResultSet res = null;
            try {
                uriid = assureUriId(connection, uri.toString());
                statement =
                    connection.prepareStatement(
                        "select 1 from OBJECT o, URI u where o.URI_ID=u.URI_ID and u.URI_STRING=?");
                statement.setString(1, uri.toString());
                res = statement.executeQuery();
                if (res.next()) {
                    if (create)
                        return false;
                } else {
                    if (!create)
                        return false;
                }
            } finally {
                close(statement, res);
            }

            if (create) {
                // create object in database
                try {
                    statement = connection.prepareStatement("insert into OBJECT (URI_ID,CLASS_NAME) values (?,?)");
                    statement.setLong(1, uriid);
                    statement.setString(2, className);
                    statement.executeUpdate();
                } finally {
                    close(statement);
                }
            }

            // update binding...

            try {
                clearBinding(connection, uri);
            } catch (ObjectNotFoundException e1) {
                // clear only if it existed
            }

            Enumeration bindings = object.enumerateBindings();
            while (bindings.hasMoreElements()) {
                ObjectNode.Binding binding = (ObjectNode.Binding) bindings.nextElement();
                try {
                    long childID = getID(connection,binding.getUuri());
                    /* 
                     * childID can be 0 if we are the root "/" and the child is a mount point for
                     * another store since it won't have been added to the URI table since its is
                     * the root of another store. So when there's no URI for the child just create 
                     * a child URI for the binding table to refer to.
                     */ 
                    if (childID == 0)
                        childID = assureUriId(connection, binding.getUuri());
                        
                    statement =
                        connection.prepareStatement(
                            "insert into BINDING (URI_ID, NAME, CHILD_UURI_ID) values (?,?,?)");
                    statement.setLong(1, uriid);
                    statement.setString(2, binding.getName());
                    statement.setLong(3, childID);
                    statement.executeUpdate();
                } finally {
                    close(statement);
                }
            }

            Enumeration parentBindings = object.enumerateParentBindings();
            while (parentBindings.hasMoreElements()) {
                ObjectNode.ParentBinding parentBinding = (ObjectNode.ParentBinding) parentBindings.nextElement();
                try {
					 long parentID = getID(connection,parentBinding.getUuri());

                    statement =
                        connection.prepareStatement(
                            "insert into PARENT_BINDING (URI_ID, NAME, PARENT_UURI_ID) values (?,?,?)");
                    statement.setLong(1, uriid);
                    statement.setString(2, parentBinding.getName());
                    statement.setLong(3, parentID);
                    statement.executeUpdate();
                } finally {
                    close(statement);
                }
            }

            if (object instanceof LinkNode) {
                // update link target
                try {
                    statement = connection.prepareStatement("delete from LINKS where URI_ID = ?");
                    statement.setLong(1, uriid);
                    statement.executeUpdate();
                } finally {
                    close(statement);
                }
                try {

					// First get the linked uri id
					 long linkedID = getID(connection,((LinkNode) object).getLinkedUri());

					// now insert link data
                    statement =
                        connection.prepareStatement(
                            "insert into LINKS (URI_ID, LINK_TO_ID) values (?,?)");
                    statement.setLong(1, uriid);
                    statement.setLong(2, linkedID);
                    statement.executeUpdate();
                } finally {
                    close(statement);
                }
            }
        } catch (SQLException e) {
            throw createException(e, uri.toString());
        }
        return true;
    }


    public void putLock(Connection connection, Uri uri, NodeLock lock) throws ServiceAccessException {
        PreparedStatement statement = null;
        try {
            int inheritable = lock.isInheritable() ? 1 : 0;
            int exclusive = lock.isExclusive() ? 1 : 0;
            long lockid = assureUriId(connection, lock.getLockId());
            long objectID = getID(connection, lock.getObjectUri());
            long subjectID = getID(connection, lock.getSubjectUri());
            long typeID = getID(connection, lock.getTypeUri());

            statement =
                connection.prepareStatement(
                    "insert into LOCKS (LOCK_ID,OBJECT_ID,SUBJECT_ID,TYPE_ID,EXPIRATION_DATE,IS_INHERITABLE,IS_EXCLUSIVE,OWNER) values (?,?,?,?,?,?,?,?)");
            statement.setLong(1, lockid);
            statement.setLong(2, objectID);
            statement.setLong(3, subjectID);
            statement.setLong(4, typeID);
            statement.setLong(5, lock.getExpirationDate().getTime());
            statement.setInt(6, inheritable);
            statement.setInt(7, exclusive);
            statement.setString(8, lock.getOwnerInfo());

            statement.execute();
        } catch (SQLException e) {
            throw createException(e, uri.toString());
        } finally {
            close(statement);
        }
    }

    public void grantPermission(Connection connection, Uri uri, NodePermission permission)
        throws ServiceAccessException {
        PreparedStatement statement = null;
        ResultSet res = null;
        int succession = 0;

        try {
            // FIXME: This might be useless if insert inserts nothing if insert...select works a i expect
            // FIXME: What happens, if only revision number, inheritable or negative changes?
            // FIXME
            //            revokePermission(connection, uri, permission);

            try {
                statement =
                    connection.prepareStatement(
                        "select max(p.SUCCESSION) from URI ou, PERMISSIONS p where p.OBJECT_ID = ou.URI_ID and ou.URI_STRING = ?");
                statement.setString(1, uri.toString());
                res = statement.executeQuery();
                res.next();
                succession = res.getInt(1) + 1;
            } finally {
                close(statement, res);
            }

            assureUriId(connection,permission.getSubjectUri());
            assureUriId(connection,permission.getActionUri());

            try {

                int inheritable = permission.isInheritable() ? 1 : 0;
                int negative = permission.isNegative() ? 1 : 0;
                long objectID = getID(connection,uri.toString());
                long subjectID = getID(connection,permission.getSubjectUri());
                long actionID = getID(connection,permission.getActionUri());

                statement =
                    connection.prepareStatement(
                        "insert into PERMISSIONS (OBJECT_ID,SUBJECT_ID,ACTION_ID,VERSION_NO, IS_INHERITABLE,IS_NEGATIVE,SUCCESSION) values (?,?,?,?,?,?,?)");
                statement.setLong(1, objectID);
                statement.setLong(2, subjectID);
                statement.setLong(3, actionID);
                statement.setString(4, getRevisionNumberAsString(permission.getRevisionNumber()));
                statement.setInt(5, inheritable);
                statement.setInt(6, negative);
                statement.setInt(7, succession);
                if (statement.executeUpdate() != 1) {
                    String msg = "Failed to insert permission ("
                    + uri.toString()
                    + "," + permission.getSubjectUri()
                    + "," + permission.getActionUri()
                    + ")";
                    getLogger().log(msg,LOG_CHANNEL,Logger.ERROR);
                    throw new ServiceAccessException(service, msg);
                }
            } finally {
                close(statement);
            }
        } catch (SQLException e) {
            throw createException(e, uri.toString());
        }
    }

    public void createRevisionDescriptor(Connection connection, Uri uri, NodeRevisionDescriptor revisionDescriptor)
        throws ServiceAccessException {

        PreparedStatement statement = null;
        try {

            assureVersionInfo(connection, uri, revisionDescriptor);

            for (Enumeration labels = revisionDescriptor.enumerateLabels(); labels.hasMoreElements();) {
                long labelId = assureLabelId(connection, (String) labels.nextElement());
                try {
					long versionID = getVersionID(connection, uri.toString(), revisionDescriptor);
                    statement =
                        connection.prepareStatement(
                            "insert into VERSION_LABELS (VERSION_ID, LABEL_ID) values (?,?)");
                    statement.setLong(1, versionID);
                    statement.setLong(2, labelId);
                    statement.executeUpdate();
                } finally {
                    close(statement);
                }
            }
            for (Enumeration properties = revisionDescriptor.enumerateProperties(); properties.hasMoreElements();) {
                try {
                    NodeProperty property = (NodeProperty) properties.nextElement();
                    long versionID = getVersionID(connection, uri.toString(), revisionDescriptor);
                    statement =
                        connection.prepareStatement(
                            "insert into PROPERTIES (VERSION_ID,PROPERTY_NAMESPACE,PROPERTY_NAME,PROPERTY_VALUE,PROPERTY_TYPE,IS_PROTECTED) values (?,?,?,?,?,?)");
                    int protectedProperty = property.isProtected() ? 1 : 0;
                    statement.setLong(1, versionID);
                    statement.setString(2, property.getNamespace());
                    statement.setString(3, property.getName());
                    statement.setString(4, property.getValue().toString());
                    statement.setString(5, property.getType());
                    statement.setInt(6, protectedProperty);
                    statement.executeUpdate();
                } finally {
                    close(statement);
                }
            }
        } catch (SQLException e) {
            throw createException(e, uri.toString());
        }
    }

    public void createRevisionDescriptors(Connection connection, Uri uri, NodeRevisionDescriptors revisionDescriptors)
    throws ServiceAccessException {

        PreparedStatement statement = null;
        ResultSet res = null;
        try {
            int isVersioned = 0;
            if (revisionDescriptors.isVersioned())
                isVersioned = 1;
            boolean revisionExists;
            try {
                statement =
                    connection.prepareStatement(
                    "SELECT u.URI_ID FROM VERSION v, URI u WHERE v.URI_ID = u.URI_ID and u.URI_STRING = ?");
                statement.setString(1, uri.toString());
                res = statement.executeQuery();
                revisionExists = res.next();

            } finally {
                close(statement, res);
            }
            if (!revisionExists) {
                try {
					long id = getID(connection,uri.toString());
                    statement =
                        connection.prepareStatement(
                        "insert into VERSION (URI_ID, IS_VERSIONED) values (?,?)");
                    statement.setLong(1, id);
                    statement.setInt(2, isVersioned);
                    statement.executeUpdate();
                } finally {
                    close(statement, res);
                }
            }
            boolean versionHistoryExists = false;
            if (revisionDescriptors.getLatestRevision() != null) {
                try {
                    statement =
                        connection.prepareStatement(
                                "SELECT 1 FROM VERSION_HISTORY vh, URI u WHERE vh.URI_ID = u.URI_ID and u.URI_STRING = ? and REVISION_NO = ?");
                    statement.setString(1, uri.toString());
                    statement.setString(2, revisionDescriptors.getLatestRevision().toString());
                    res = statement.executeQuery();
                    versionHistoryExists = res.next();
                } finally {
                    close(statement, res);
                }
            }
            if (!versionHistoryExists && revisionDescriptors.getLatestRevision() != null) {
                try {

					long[] ids = getBranchIdAndUriID(connection,uri.toString());
					long uriID=ids[0];
					long branchID=ids[1];

                    statement =
                        connection.prepareStatement(
                        "insert into VERSION_HISTORY (URI_ID, BRANCH_ID, REVISION_NO) values(?,?,?)");
                    statement.setLong(1, uriID);
                    statement.setLong(2, branchID);
                    statement.setString(3, getRevisionNumberAsString(revisionDescriptors.getLatestRevision()));

                    // FIXME: Create new revisions on the main branch???
                    statement.executeUpdate();
                } finally {
                    close(statement, res);
                }
            }


            // Add revision successors
            Enumeration revisionNumbers = revisionDescriptors.enumerateRevisionNumbers();
            while (revisionNumbers.hasMoreElements()) {
                NodeRevisionNumber nodeRevisionNumber = (NodeRevisionNumber) revisionNumbers.nextElement();

                Enumeration successors = revisionDescriptors.getSuccessors(nodeRevisionNumber);
                while (successors.hasMoreElements())
                {
                    try {
                        NodeRevisionNumber successor = (NodeRevisionNumber) successors.nextElement();

                        statement =
                            connection.prepareStatement(
                                    "insert into VERSION_PREDS (VERSION_ID, PREDECESSOR_ID) " +
                                    " select vr.VERSION_ID, suc.VERSION_ID" +
                                    " FROM URI uri, VERSION_HISTORY  vr, VERSION_HISTORY suc " +
                                    " where vr.URI_ID = uri.URI_ID " +
                                    " and suc.URI_ID = uri.URI_ID " +
                                    " and uri.URI_STRING = ? " +
                                    " and vr.REVISION_NO = ? " +
                                    " and suc.REVISION_NO = ? " );

                        statement.setString(1, uri.toString());
                        statement.setString(2, nodeRevisionNumber.toString());
                        statement.setString(3, successor.toString());
                        statement.executeUpdate();
                    } finally {
                        close(statement);
                    }
                }
            }
            getLogger().log(
                    revisionDescriptors.getOriginalUri() + revisionDescriptors.getInitialRevision(),
                    LOG_CHANNEL,
                    Logger.INFO);


        } catch (SQLException e) {
            throw createException(e, uri.toString());
        }
    }

    protected void assureVersionInfo(Connection connection, Uri uri, NodeRevisionDescriptor revisionDescriptor)
        throws SQLException {
        PreparedStatement statement = null;
        ResultSet res = null;
        boolean revisionExists;
        try {
            statement =
                connection.prepareStatement(
                    "select 1 from VERSION v, URI u where v.URI_ID = u.URI_ID and u.URI_STRING = ?");
            statement.setString(1, uri.toString());
            res = statement.executeQuery();
            revisionExists = res.next();
        } finally {
            close(statement, res);
        }
        // FIXME: Is it true, that the default for IS_VERSIONED is 0 ??
        if (!revisionExists) {
            try {
				long id = getID(connection,uri.toString());
                statement =
                    connection.prepareStatement(
                        "insert into VERSION (URI_ID, IS_VERSIONED) values (?,?)");
                statement.setLong(1,id);
                statement.setInt(2, 0);
                statement.executeUpdate();
            } finally {
                close(statement);
            }
        }
        boolean versionHistoryExists;
        try {
            statement =
                connection.prepareStatement(
                    "select 1 from VERSION_HISTORY vh, URI u where vh.URI_ID = u.URI_ID and u.URI_STRING = ? and REVISION_NO = ?");
            statement.setString(1, uri.toString());
            statement.setString(2, revisionDescriptor.getRevisionNumber().toString());
            res = statement.executeQuery();
            versionHistoryExists = res.next();
        } finally {
            close(statement, res);
        }
        if (!versionHistoryExists) {
            long branchId = assureBranchId(connection, revisionDescriptor.getBranchName());
            try {

				long id = getID(connection,uri.toString());

                statement =
                    connection.prepareStatement(
                        "insert into VERSION_HISTORY (URI_ID, BRANCH_ID, REVISION_NO) values (?,?,?)");
                statement.setLong(1, id);
                statement.setLong(2, branchId);
                statement.setString(3, getRevisionNumberAsString(revisionDescriptor.getRevisionNumber()));
                statement.executeUpdate();
            } finally {
                close(statement);
            }
        }
    }


    private long getID(Connection connection, String uriString) throws  SQLException
    {
		PreparedStatement statement = null;
		ResultSet rs = null;
		long uriID=0l;
		try
		{
			statement =
				connection.prepareStatement(
					"select URI_ID from URI where URI_STRING = ?");

			 statement.setString(1,uriString);
			 rs = statement.executeQuery();
			 if(rs.next())
				 uriID= rs.getLong(1);
		 }
		 finally
		 {
			 close(statement,rs);
		 }

		 return uriID;
	}

  private long[] getBranchIdAndUriID(Connection connection, String uriString) throws SQLException
  {
	PreparedStatement statement = null;
	ResultSet res = null;
	long[] ids = new long[2];

	try
	{
		statement =
			connection.prepareStatement(
				"select u.URI_ID, b.BRANCH_ID from URI u, BRANCH b where u.URI_STRING = ? and b.BRANCH_STRING = ?");

		 statement.setString(1, uriString);
		 statement.setString(2,NodeRevisionDescriptors.MAIN_BRANCH);

		 res = statement.executeQuery();
		 if(res.next())
		 {
			 ids[0]=res.getLong(1);
			 ids[1] = res.getLong(2);
		 }
	 }
	 finally
	 {
		 close(statement,res);
	 }

	 return ids;
  }

  protected String convertRevisionNumberToComparable(String revisioNumber) {
      return revisioNumber;
  }
  
  protected ServiceAccessException createException(SQLException e, String uri) {

      switch (e.getErrorCode()) {
          case 911 : // thread was deadlock victim
              getLogger().log(e.getErrorCode() + ": Deadlock resolved on " + uri, LOG_CHANNEL, Logger.WARNING);
//            return new ServiceAccessException(this, new ConflictException(uri));
              throw new ConcurrencyConflictError(e, uri);

          default :
              return super.createException(e, uri);
      }
  }


}

