/*
 * Copyright (C) 2011 eXo Platform SAS.
 *
 * This is free software; you can redistribute it and/or modify it
 * under the terms of the GNU Lesser General Public License as
 * published by the Free Software Foundation; either version 2.1 of
 * the License, or (at your option) any later version.
 *
 * This software is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this software; if not, write to the Free
 * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
 * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
 */
package org.exoplatform.services.jcr.impl.storage.jdbc;

import org.exoplatform.commons.utils.SecurityHelper;
import org.exoplatform.services.database.utils.JDBCUtils;
import org.exoplatform.services.jcr.config.RepositoryConfigurationException;
import org.exoplatform.services.jcr.config.WorkspaceEntry;
import org.exoplatform.services.jcr.core.security.JCRRuntimePermissions;
import org.exoplatform.services.jcr.datamodel.ItemType;
import org.exoplatform.services.jcr.datamodel.NodeData;
import org.exoplatform.services.jcr.datamodel.PropertyData;
import org.exoplatform.services.jcr.datamodel.QPathEntry;
import org.exoplatform.services.jcr.impl.Constants;
import org.exoplatform.services.jcr.impl.checker.DummyRepair;
import org.exoplatform.services.jcr.impl.checker.EarlierVersionsRemover;
import org.exoplatform.services.jcr.impl.checker.InspectionQuery;
import org.exoplatform.services.jcr.impl.checker.InspectionReport;
import org.exoplatform.services.jcr.impl.checker.NodeRemover;
import org.exoplatform.services.jcr.impl.checker.PropertyRemover;
import org.exoplatform.services.jcr.impl.checker.RootAsParentAssigner;
import org.exoplatform.services.jcr.impl.checker.ValueRecordsRemover;
import org.exoplatform.services.jcr.impl.core.lock.LockTableHandler;
import org.exoplatform.services.jcr.impl.core.lock.cacheable.AbstractCacheableLockManager;
import org.exoplatform.services.jcr.impl.core.nodetype.NodeTypeDataManagerImpl;
import org.exoplatform.services.jcr.impl.dataflow.persistent.SimpleChangedSizeHandler;
import org.exoplatform.services.jcr.impl.storage.value.ValueDataNotFoundException;
import org.exoplatform.services.jcr.impl.storage.value.ValueStorageNotFoundException;
import org.exoplatform.services.jcr.impl.util.jdbc.DBInitializerHelper;
import org.exoplatform.services.jcr.storage.WorkspaceStorageConnection;
import org.exoplatform.services.jcr.storage.value.ValueIOChannel;
import org.exoplatform.services.jcr.storage.value.ValueStoragePluginProvider;
import org.exoplatform.services.log.ExoLogger;
import org.exoplatform.services.log.Log;

import java.io.IOException;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

import javax.jcr.RepositoryException;
import javax.naming.NamingException;

/**
 * @author <a href="mailto:skarpenko@exoplatform.com">Sergiy Karpenko</a>
 * @version $Id: exo-jboss-codetemplates.xml 34360 14 жовт. 2011 skarpenko $
 *
 */
public class JDBCWorkspaceDataContainerChecker
{
   protected static final Log LOG = ExoLogger.getLogger("exo.jcr.component.core.JDBCWorkspaceDataContainerChecker");

   protected final JDBCWorkspaceDataContainer jdbcDataContainer;

   protected final ValueStoragePluginProvider vsPlugin;

   protected final WorkspaceEntry workspaceEntry;

   protected final InspectionReport report;

   private InspectionQuery vsInspectionQuery;

   private InspectionQuery lockInspectionQuery;

   private List<InspectionQuery> itemsInspectionQuery = new ArrayList<InspectionQuery>();;

   private LockTableHandler lockHandler;

   private NodeTypeDataManagerImpl nodeTypeManager;

   /**
    * JDBCWorkspaceDataContainerChecker constructor.
    */
   public JDBCWorkspaceDataContainerChecker(JDBCWorkspaceDataContainer jdbcDataContainer,
      AbstractCacheableLockManager lockManager, ValueStoragePluginProvider vsPlugin, WorkspaceEntry workspaceEntry,
      NodeTypeDataManagerImpl nodeTypeManager, InspectionReport report)
   {
      this.jdbcDataContainer = jdbcDataContainer;
      this.vsPlugin = vsPlugin;
      this.workspaceEntry = workspaceEntry;
      this.report = report;
      this.lockHandler = lockManager.getLockTableHandler();
      this.nodeTypeManager = nodeTypeManager;

      initInspectionQueries();
   }

   /**
    * Checks jcr locks for consistency. Defines if there is a node with lockIsDeep or lockOwner property 
    * (basically means that the node is to be locked)
    * and has no corresponding record in LockManager persistent layer (db table); 
    * or the opposite.
    */
   public void checkLocksInDataBase(boolean autoRepair)
   {
      SecurityHelper.validateSecurityPermission(JCRRuntimePermissions.MANAGE_REPOSITORY_PERMISSION);

      ResultSet resultSet = null;
      PreparedStatement preparedStatement = null;
      Connection jdbcConnection = null;
      try
      {
         jdbcConnection = jdbcDataContainer.getConnectionFactory().getJdbcConnection();
         preparedStatement = jdbcConnection.prepareStatement(lockInspectionQuery.getStatement());
         resultSet = preparedStatement.executeQuery();

         Set<String> lockedInJCRITEM = new HashSet<String>();
         while (resultSet.next())
         {
            lockedInJCRITEM.add(removeWorkspacePrefix(resultSet.getString(DBConstants.COLUMN_PARENTID)));
         }

         Set<String> lockedInJCRLOCK = lockHandler.getLockedNodesIds();

         checkConsistencyInJCRITEM(lockedInJCRITEM, lockedInJCRLOCK, autoRepair);
         checkConsistencyInJCRLOCK(lockedInJCRITEM, lockedInJCRLOCK, autoRepair);
      }
      catch (SQLException e)
      {
         logExceptionAndSetInconsistency("Unexpected exception during LOCK DB checking.", e);
      }
      catch (NamingException e)
      {
         logExceptionAndSetInconsistency("Unexpected exception during LOCK DB checking.", e);
      }
      catch (RepositoryConfigurationException e)
      {
         logExceptionAndSetInconsistency("Unexpected exception during LOCK DB checking.", e);
      }
      catch (RepositoryException e)
      {
         logExceptionAndSetInconsistency("Unexpected exception during LOCK DB checking.", e);
      }
      finally
      {
         JDBCUtils.freeResources(resultSet, preparedStatement, jdbcConnection);
      }
   }

   private void checkConsistencyInJCRITEM(Set<String> lockedInJCRITEM, Set<String> lockedInJCRLOCK, boolean autoRepair)
      throws RepositoryException, SQLException
   {
      for (String nodeId : lockedInJCRITEM)
      {
         if (!lockedInJCRLOCK.contains(nodeId))
         {
            logBrokenObjectAndSetInconsistency("Lock exists in ITEM table but not in LOCK table. Node UUID: " + nodeId);

            if (autoRepair)
            {
               WorkspaceStorageConnection conn = jdbcDataContainer.openConnection();
               try
               {
                  NodeData parent = (NodeData)conn.getItemData(nodeId);
                  PropertyData prop =
                     (PropertyData)conn.getItemData(parent, new QPathEntry(Constants.JCR_LOCKISDEEP, 0),
                        ItemType.PROPERTY);
                  conn.delete(prop, new SimpleChangedSizeHandler());

                  prop =
                     (PropertyData)conn.getItemData(parent, new QPathEntry(Constants.JCR_LOCKOWNER, 0),
                        ItemType.PROPERTY);
                  conn.delete(prop, new SimpleChangedSizeHandler());

                  conn.commit();

                  logComment("Lock has been removed form ITEM table. Node UUID: " + nodeId);
               }
               catch (RepositoryException e)
               {
                  conn.rollback();
                  throw e;
               }
            }
         }
      }
   }

   private void checkConsistencyInJCRLOCK(Set<String> lockedInJCRITEM, Set<String> lockedInJCRLOCK, boolean autoRepair)
      throws NamingException, RepositoryConfigurationException, SQLException
   {
      for (String nodeId : lockedInJCRLOCK)
      {
         if (!lockedInJCRITEM.contains(nodeId))
         {
            logBrokenObjectAndSetInconsistency("Lock exists in LOCK table but not in ITEM table. Node UUID: " + nodeId);

            if (autoRepair)
            {
               lockHandler.removeLockedNode(nodeId);
               logComment("Lock has been removed form LOCK table. Node UUID: " + nodeId);
            }
         }
      }
   }

   /**
    * Check database.
    * <p>
    * Check that database is not broken, and all base relation between jcr-items are not corrupted.
    * </p>
    */
   public void checkDataBase(boolean autoRepair)
   {
      SecurityHelper.validateSecurityPermission(JCRRuntimePermissions.MANAGE_REPOSITORY_PERMISSION);

      Connection jdbcConn = null;
      try
      {
         jdbcConn = jdbcDataContainer.getConnectionFactory().getJdbcConnection();

         for (InspectionQuery query : itemsInspectionQuery)
         {
            PreparedStatement st = null;
            ResultSet resultSet = null;
            try
            {
               st = jdbcConn.prepareStatement(query.getStatement());

               resultSet = st.executeQuery();
               if (resultSet.next())
               {
                  logDescription(query.getDescription());

                  do
                  {
                     logBrokenObjectAndSetInconsistency(getBrokenObject(resultSet, query.getFieldNames()));
                     if (autoRepair)
                     {
                        try
                        {
                           query.getRepair().doRepair(resultSet);
                           logComment("Inconsistency has been fixed");
                        }
                        catch (SQLException e)
                        {
                           logExceptionAndSetInconsistency("Inconsistency can not been fixed", e);
                        }
                     }
                  }
                  while (resultSet.next());
               }
            }
            finally
            {
               JDBCUtils.freeResources(resultSet, st, null);
            }
         }
      }
      catch (SQLException e)
      {
         logExceptionAndSetInconsistency("Unexpected exception during DB checking.", e);
      }
      catch (RepositoryException e)
      {
         logExceptionAndSetInconsistency("Unexpected exception during DB checking.", e);
      }
      finally
      {
         JDBCUtils.freeResources(null, null, jdbcConn);
      }
   }

   /**
    * Inspect ValueStorage.
    * <p>
    * All ValueDatas that have storage description (that means, value data stored in value storage) will be inspected:
    * <ul>
    * <li> does value exists in value storage;</li>
    * <ul>
    */
   public void checkValueStorage(boolean autoRepair)
   {
      SecurityHelper.validateSecurityPermission(JCRRuntimePermissions.MANAGE_REPOSITORY_PERMISSION);

      Connection connection = null;
      PreparedStatement st = null;
      ResultSet resultSet = null;
      try
      {
         connection = jdbcDataContainer.getConnectionFactory().getJdbcConnection();
         st = connection.prepareStatement(vsInspectionQuery.getStatement());
         resultSet = st.executeQuery();

         if (resultSet.next())
         {
            logDescription("ValueData not found inconsistency");

            do
            {
               String storageDesc = resultSet.getString(DBConstants.COLUMN_VSTORAGE_DESC);

               ValueIOChannel channel = null;
               try
               {
                  channel = getIOChannel(storageDesc);
                  doCheckAndRepairValueData(channel, resultSet, autoRepair);
               }
               catch (IOException e)
               {
                  logExceptionAndSetInconsistency("Unexpected exception during checking.", e);
               }
               catch (SQLException e)
               {
                  logExceptionAndSetInconsistency("Unexpected exception during checking.", e);
               }
               finally
               {
                  if (channel != null)
                  {
                     channel.close();
                  }
               }
            }
            while (resultSet.next());
         }
      }
      catch (SQLException e)
      {
         logExceptionAndSetInconsistency("Unexpected exception during checking.", e);
      }
      catch (RepositoryException e)
      {
         logExceptionAndSetInconsistency("Unexpected exception during checking.", e);
      }
      finally
      {
         JDBCUtils.freeResources(resultSet, st, connection);
      }
   }

   private ValueIOChannel getIOChannel(String storageDesc) throws IOException
   {
      ValueIOChannel channel = null;
      try
      {
         channel = vsPlugin.getChannel(storageDesc);
      }
      catch (ValueStorageNotFoundException e)
      {
         logExceptionAndSetInconsistency("ValueStorage " + storageDesc + " not found: " + e.getMessage(), e);
      }

      return channel;
   }

   private void doCheckAndRepairValueData(ValueIOChannel channel, ResultSet resultSet, boolean autoRepair)
      throws IOException, SQLException
   {
      String propertyId = removeWorkspacePrefix(resultSet.getString(DBConstants.COLUMN_VPROPERTY_ID));
      int orderNumber = resultSet.getInt(DBConstants.COLUMN_VORDERNUM);

      try
      {
         channel.checkValueData(propertyId, orderNumber);
      }
      catch (ValueDataNotFoundException e)
      {
         logBrokenObjectAndSetInconsistency(getBrokenObject(resultSet, vsInspectionQuery.getFieldNames()));

         if (autoRepair)
         {
            try
            {
               channel.repairValueData(propertyId, orderNumber);
               logComment("ValueData has been repaired. New empty file is created.");
            }
            catch (IOException e2)
            {
               logExceptionAndSetInconsistency("Can not repair value data: " + e.getMessage(), e);
            }
         }
      }
   }

   private String removeWorkspacePrefix(String str)
   {
      return jdbcDataContainer.containerConfig.dbStructureType.isMultiDatabase() ? str : str
         .substring(jdbcDataContainer.containerConfig.containerName.length());
   }

   private String getBrokenObject(ResultSet resultSet, String[] fieldNames)
   {
      StringBuilder record = new StringBuilder();
      for (String fieldName : fieldNames)
      {
         record.append(fieldName);
         record.append('=');

         try
         {
            record.append(resultSet.getString(fieldName));
         }
         catch (SQLException e)
         {
            LOG.error(e.getMessage(), e);
         }
         record.append(' ');
      }

      return record.toString();
   }

   private void logBrokenObjectAndSetInconsistency(String brokenObject)
   {
      try
      {
         report.logBrokenObjectAndSetInconsistency(brokenObject);
      }
      catch (IOException e)
      {
         LOG.error(e.getMessage(), e);
      }
   }

   private void logComment(String message)
   {
      try
      {
         report.logComment(message);
      }
      catch (IOException e1)
      {
         LOG.error(e1.getMessage(), e1);
      }
   }

   private void logDescription(String description)
   {
      try
      {
         report.logDescription(description);
      }
      catch (IOException e1)
      {
         LOG.error(e1.getMessage(), e1);
      }
   }

   private void logExceptionAndSetInconsistency(String message, Throwable e)
   {
      try
      {
         report.logExceptionAndSetInconsistency(message, e);
      }
      catch (IOException e1)
      {
         LOG.error(e1.getMessage(), e1);
      }
   }

   private void initInspectionQueries()
   {
      String itemTable = DBInitializerHelper.getItemTableName(jdbcDataContainer.containerConfig);
      String valueTable = DBInitializerHelper.getValueTableName(jdbcDataContainer.containerConfig);
      String refTable = DBInitializerHelper.getRefTableName(jdbcDataContainer.containerConfig);

      boolean singleDatabase =
         jdbcDataContainer.containerConfig.dbStructureType == JDBCDataContainerConfig.DatabaseStructureType.SINGLE;

      vsInspectionQuery =
         new InspectionQuery(singleDatabase ? "select V.PROPERTY_ID, V.ORDER_NUM, V.STORAGE_DESC from " + valueTable
            + " V, " + itemTable + " I" + " where I.CONTAINER_NAME='" + jdbcDataContainer.containerConfig.containerName
            + "' and V.PROPERTY_ID = I.ID and STORAGE_DESC is not null"
            : "select PROPERTY_ID, ORDER_NUM, STORAGE_DESC from " + valueTable + " where STORAGE_DESC is not null",
            new String[]{DBConstants.COLUMN_VPROPERTY_ID, DBConstants.COLUMN_VORDERNUM,
               DBConstants.COLUMN_VSTORAGE_DESC}, "Items with value data stored in value storage", new DummyRepair());

      lockInspectionQuery =
         new InspectionQuery(singleDatabase ? "select distinct PARENT_ID from " + itemTable + " WHERE CONTAINER_NAME='"
            + jdbcDataContainer.containerConfig.containerName + "'"
            + " AND I_CLASS=2 and (NAME='[http://www.jcp.org/jcr/1.0]lockOwner'"
            + " OR NAME='[http://www.jcp.org/jcr/1.0]lockIsDeep')" : "select distinct PARENT_ID from " + itemTable
            + " where I_CLASS=2 AND"
            + " (NAME='[http://www.jcp.org/jcr/1.0]lockOwner' OR NAME='[http://www.jcp.org/jcr/1.0]lockIsDeep')",
            new String[]{DBConstants.COLUMN_PARENTID}, "Items which have jcr:lockOwner and jcr:lockIsDeep properties",
            new DummyRepair());

      // ITEM tables
      itemsInspectionQuery.add(new InspectionQuery(singleDatabase ? "select * from " + itemTable
         + " I where I.CONTAINER_NAME='" + jdbcDataContainer.containerConfig.containerName
         + "' and NOT EXISTS(select * from " + itemTable + " P where P.ID = I.PARENT_ID)" : "select * from "
         + itemTable + " I where NOT EXISTS(select * from " + itemTable + " P where P.ID = I.PARENT_ID)", new String[]{
         DBConstants.COLUMN_ID, DBConstants.COLUMN_PARENTID, DBConstants.COLUMN_NAME, DBConstants.COLUMN_CLASS},
         "Items that do not have parent nodes", new RootAsParentAssigner(jdbcDataContainer.getConnectionFactory(),
            jdbcDataContainer.containerConfig)));

      String statement =
         singleDatabase ? "select * from " + itemTable + " P where P.CONTAINER_NAME='"
            + jdbcDataContainer.containerConfig.containerName + "' and P.I_CLASS=2"
            + " and P.P_MULTIVALUED=? and NOT EXISTS( select * from " + valueTable + " V where V.PROPERTY_ID=P.ID)"
            : "select * from " + itemTable
               + " P where P.I_CLASS=2 and P.P_MULTIVALUED=? and NOT EXISTS( select * from " + valueTable + " V "
               + "where V.PROPERTY_ID=P.ID)";

      if (jdbcDataContainer.containerConfig.dbDialect.startsWith(DBConstants.DB_DIALECT_PGSQL))
      {
         statement = statement.replace("?", "'f'");
      }
      else if (jdbcDataContainer.containerConfig.dbDialect.startsWith(DBConstants.DB_DIALECT_HSQLDB))
      {
         statement = statement.replace("?", "FALSE");
      }
      else
      {
         statement = statement.replace("?", "0");
      }

      itemsInspectionQuery.add(new InspectionQuery(statement, new String[]{DBConstants.COLUMN_ID,
         DBConstants.COLUMN_PARENTID, DBConstants.COLUMN_NAME},
         "A node that has a single valued properties with nothing declared in the VALUE table.", new PropertyRemover(
            jdbcDataContainer.getConnectionFactory(), jdbcDataContainer.containerConfig, nodeTypeManager)));

      itemsInspectionQuery.add(new InspectionQuery(singleDatabase ? "select * from " + itemTable
         + " N where N.CONTAINER_NAME='" + jdbcDataContainer.containerConfig.containerName
         + "' and N.I_CLASS=1 and NOT EXISTS (select * from " + itemTable + " P "
         + "where P.I_CLASS=2 and P.PARENT_ID=N.ID and P.NAME='[http://www.jcp.org/jcr/1.0]primaryType' "
         + "and P.CONTAINER_NAME='" + jdbcDataContainer.containerConfig.containerName + "')" : "select * from "
         + itemTable + " N where N.I_CLASS=1 and NOT EXISTS " + "(select * from " + itemTable
         + " P where P.I_CLASS=2 and P.PARENT_ID=N.ID " + "and P.NAME='[http://www.jcp.org/jcr/1.0]primaryType')",
         new String[]{DBConstants.COLUMN_ID, DBConstants.COLUMN_PARENTID, DBConstants.COLUMN_NAME},
         "A node that doesn't have primary type property", new NodeRemover(jdbcDataContainer.getConnectionFactory(),
            jdbcDataContainer.containerConfig, nodeTypeManager)));

      itemsInspectionQuery.add(new InspectionQuery(singleDatabase ? "select * from " + valueTable
         + " V where NOT EXISTS(select * from " + itemTable + " P " + "where V.PROPERTY_ID = P.ID and P.I_CLASS=2)"
         : "select * from " + valueTable + " V where NOT EXISTS(select * from " + itemTable + " P "
            + "where V.PROPERTY_ID = P.ID and P.I_CLASS=2)", new String[]{DBConstants.COLUMN_ID,
         DBConstants.COLUMN_VPROPERTY_ID}, "All value records that has not related property record",
         new ValueRecordsRemover(jdbcDataContainer.getConnectionFactory(), jdbcDataContainer.containerConfig)));

      // The differences in the queries by DB dialect.
      if (jdbcDataContainer.containerConfig.dbDialect.startsWith(DBConstants.DB_DIALECT_SYBASE))
      {
         statement =
            singleDatabase ? "select V.* from " + valueTable + " V, " + itemTable
               + " I where V.PROPERTY_ID = I.ID and I.CONTAINER_NAME='"
               + jdbcDataContainer.containerConfig.containerName
               + "' AND ((STORAGE_DESC is not null and not DATA like null))" : "select * from " + valueTable
               + " where (STORAGE_DESC is not null and not DATA like null)";
      }
      else if (jdbcDataContainer.containerConfig.dbDialect.startsWith(DBConstants.DB_DIALECT_ORACLE))
      {
         statement =
            singleDatabase ? "select V.* from " + valueTable + " V, " + itemTable
               + " I where V.PROPERTY_ID = I.ID and I.CONTAINER_NAME='"
               + jdbcDataContainer.containerConfig.containerName
               + "' AND (STORAGE_DESC is not null and DATA is not null)" : "select * from " + valueTable
               + " where (STORAGE_DESC is not null and DATA is not null)";
      }
      else
      {
         statement =
            singleDatabase ? "select V.* from " + valueTable + " V, " + itemTable
               + " I where V.PROPERTY_ID = I.ID and I.CONTAINER_NAME='"
               + jdbcDataContainer.containerConfig.containerName
               + "' AND ((STORAGE_DESC is not null and DATA is not null))" : "select * from " + valueTable
               + " where (STORAGE_DESC is not null and DATA is not null)";
      }
      itemsInspectionQuery.add(new InspectionQuery(statement, new String[]{DBConstants.COLUMN_ID},
         "Incorrect VALUE records. Both fields STORAGE_DESC and DATA contain not null value.", new DummyRepair()));

      itemsInspectionQuery.add(new InspectionQuery(singleDatabase ? "select * from " + itemTable
         + " I where I.ID = I.PARENT_ID and I.CONTAINER_NAME='" + jdbcDataContainer.containerConfig.containerName
         + "' and I.NAME <> '" + Constants.ROOT_PARENT_NAME + "'" : "select * from " + itemTable
         + " I where I.ID = I.PARENT_ID and I.NAME <> '" + Constants.ROOT_PARENT_NAME + "'", new String[]{
         DBConstants.COLUMN_ID, DBConstants.COLUMN_PARENTID, DBConstants.COLUMN_NAME}, "An item is its own parent.",
         new RootAsParentAssigner(jdbcDataContainer.getConnectionFactory(), jdbcDataContainer.containerConfig)));

      itemsInspectionQuery.add(new InspectionQuery(singleDatabase ? "select * from " + itemTable
         + " I where I.CONTAINER_NAME='" + jdbcDataContainer.containerConfig.containerName
         + "' and EXISTS (select * from " + itemTable + " J WHERE I.CONTAINER_NAME = J.CONTAINER_NAME and"
         + " I.PARENT_ID = J.PARENT_ID AND I.NAME = J.NAME and I.I_INDEX = J.I_INDEX and I.I_CLASS = J.I_CLASS"
         + " and I.VERSION != J.VERSION and I.I_CLASS = 2)" : "select * from " + itemTable
         + " I where EXISTS (select * from " + itemTable + " J"
         + " WHERE I.PARENT_ID = J.PARENT_ID AND I.NAME = J.NAME and I.I_INDEX = J.I_INDEX and I.I_CLASS = J.I_CLASS"
         + " and I.VERSION != J.VERSION and I.I_CLASS = 2)", new String[]{DBConstants.COLUMN_ID,
         DBConstants.COLUMN_PARENTID, DBConstants.COLUMN_NAME, DBConstants.COLUMN_VERSION, DBConstants.COLUMN_CLASS,
         DBConstants.COLUMN_INDEX}, "Several versions of same item.", new EarlierVersionsRemover(jdbcDataContainer
         .getConnectionFactory(), jdbcDataContainer.containerConfig)));

      itemsInspectionQuery.add(new InspectionQuery(singleDatabase ? "select * from " + itemTable + " P, " + valueTable
         + " V where P.ID=V.PROPERTY_ID and P.CONTAINER_NAME='" + jdbcDataContainer.containerConfig.containerName
         + "' and P.P_TYPE=9 and NOT EXISTS (select * from " + refTable + " R where P.ID=R.PROPERTY_ID)"
         : "select * from " + itemTable + " P, " + valueTable
            + " V where P.ID=V.PROPERTY_ID and P.P_TYPE=9 and NOT EXISTS " + "(select * from " + refTable
            + " R where P.ID=R.PROPERTY_ID)", new String[]{DBConstants.COLUMN_ID, DBConstants.COLUMN_PARENTID,
         DBConstants.COLUMN_NAME}, "Reference properties without reference records", new PropertyRemover(
         jdbcDataContainer.getConnectionFactory(), jdbcDataContainer.containerConfig, nodeTypeManager)));
   }
}
