/*
* 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)));
}
}