/* The contents of this file are subject to the license and copyright terms * detailed in the license directory at the root of the source tree (also * available online at http://fedora-commons.org/license/). */ package fedora.server.search; import java.io.IOException; import java.io.InputStream; import java.lang.reflect.Field; import java.sql.Connection; import java.sql.DriverManager; import java.sql.ResultSet; import java.sql.SQLException; import java.sql.Statement; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Date; import java.util.List; import java.util.Properties; import java.util.regex.Matcher; import java.util.regex.Pattern; import org.junit.After; import org.junit.AfterClass; import org.junit.Before; import org.junit.BeforeClass; import org.junit.Test; import junit.framework.JUnit4TestAdapter; import mock.sql.MockConnection; import mock.sql.MockDriver; import mock.sql.MockStatement; import fedora.server.Context; import fedora.server.config.DatastoreConfiguration; import fedora.server.errors.InconsistentTableSpecException; import fedora.server.errors.ServerException; import fedora.server.storage.ConnectionPool; import fedora.server.storage.MockDOReader; import fedora.server.storage.MockRepositoryReader; import fedora.server.storage.MockServiceDeploymentReader; import fedora.server.storage.ServiceDeploymentReader; import fedora.server.storage.types.BasicDigitalObject; import fedora.server.storage.types.DatastreamXMLMetadata; import fedora.server.storage.types.DeploymentDSBindSpec; import fedora.server.utilities.SQLUtility; import fedora.server.utilities.TableCreatingConnection; import fedora.server.utilities.TableSpec; import static junit.framework.Assert.assertEquals; import static junit.framework.Assert.fail; public class TestFieldSearchSQLImpl { private static final String[] SHORT_FIELDS = FieldSearchSQLImpl.DB_COLUMN_NAMES_NODC; private static final String[] LONG_FIELDS = FieldSearchSQLImpl.DB_COLUMN_NAMES; private static final String DC_PAYLOAD_NO_DATES = "<oai_dc:dc " + " xmlns:oai_dc=\"http://www.openarchives.org/OAI/2.0/oai_dc/\" " + " xmlns:dc=\"http://purl.org/dc/elements/1.1/\">\n" + " <dc:title>Sandy's Reference Object</dc:title>\n" + " <dc:creator>Sandy Payette</dc:creator>\n" + " <dc:subject>FOXML Testing</dc:subject>\n" + " <dc:description>Object depicts all types of datastreams</dc:description>\n" + " <dc:publisher>Cornell CIS</dc:publisher>\n" + " <dc:identifier>test:100</dc:identifier>\n" + "</oai_dc:dc>\n"; private static final String DC_PAYLOAD_WITH_DATES = "<oai_dc:dc " + " xmlns:oai_dc=\"http://www.openarchives.org/OAI/2.0/oai_dc/\" " + " xmlns:dc=\"http://purl.org/dc/elements/1.1/\">\n" + " <dc:title>Sandy's Reference Object</dc:title>\n" + " <dc:creator>Sandy Payette</dc:creator>\n" + " <dc:subject>FOXML Testing</dc:subject>\n" + " <dc:description>Object depicts all types of datastreams</dc:description>\n" + " <dc:publisher>Cornell CIS</dc:publisher>\n" + " <dc:identifier>test:100</dc:identifier>\n" + " <dc:date>2006-10-15</dc:date>\n" + "</oai_dc:dc>\n"; public static junit.framework.Test suite() { return new JUnit4TestAdapter(TestFieldSearchSQLImpl.class); } private static final ObjectData OBJECT_WITH_NO_DC = new ObjectData( "somePid", "myLabel", "A", "theOwner", new Date( 12345), new Date(67890), new Date(0), null); private static final ObjectData OBJECT_WITH_DC = new ObjectData("somePid", "myLabel", "A", "theOwner", new Date(12345), new Date(67890), new Date(10000), DC_PAYLOAD_NO_DATES); private static final ObjectData OBJECT_WITH_DC_AND_DATES = new ObjectData( "somePid", "myLabel", "A", "theOwner", new Date( 12345), new Date(67890), new Date(10000), DC_PAYLOAD_WITH_DATES); private static SQLUtility saveSqlUtility; @BeforeClass public static void saveSqlUtilityImpl() { saveSqlUtility = getSqlUtilityInstance(); } @AfterClass public static void restoreSqlUtilityImpl() { setSqlUtilityInstance(saveSqlUtility); } private MockConnection mockConnection; private MockRepositoryReader mockRepositoryReader; private ConnectionPool connectionPool; private final MyMockDriver mockDriver = new MyMockDriver(); private int expectedDateInserts; private int expectedDateDeletes; @Before public void registerMockDriver() { try { DriverManager.registerDriver(mockDriver); } catch (SQLException e) { fail("Failed to register mock JDBC driver: " + e); } } @After public void deregisterMockDriver() { try { DriverManager.deregisterDriver(mockDriver); } catch (SQLException e) { fail("Failed to deregister mock JDBC driver: " + e); } } @Before public void createConnectionPool() throws SQLException { // Create a connection pool that uses the Mock Driver and some other // plausible values. this.connectionPool = new ConnectionPool(MockDriver.class.getName(), "mock://bogus.url", "bogusUsername", "bogusPassword", 5, 5, 5, 0, 0, 2, 300, null, false, false, false, (byte) 0); } @Before public void clearExpectedValues() { this.expectedDateInserts = 0; this.expectedDateDeletes = 0; } @Test public void noDC() throws ServerException { setSqlUtilityInstance(new UpdatingMockSqlUtility(SHORT_FIELDS, OBJECT_WITH_NO_DC.getShortFieldValueList())); this.mockConnection = new UnusedMockConnection(); this.mockRepositoryReader = new UnusedMockRepositoryReader(); updateRecord(OBJECT_WITH_NO_DC, false); checkExpectations(); } @Test public void dcNoDatesShortFields() throws ServerException { setSqlUtilityInstance(new UpdatingMockSqlUtility(SHORT_FIELDS, OBJECT_WITH_DC.getShortFieldValueList())); this.mockConnection = new UnusedMockConnection(); this.mockRepositoryReader = new UnusedMockRepositoryReader(); updateRecord(OBJECT_WITH_DC, false); checkExpectations(); } @Test public void dcNoDatesLongFields() throws ServerException { setSqlUtilityInstance(new UpdatingMockSqlUtility(LONG_FIELDS, OBJECT_WITH_DC.getLongFieldValueList())); this.mockConnection = new UpdatingMockConnection(); this.expectedDateDeletes = 1; this.expectedDateInserts = 0; this.mockRepositoryReader = new UnusedMockRepositoryReader(); updateRecord(OBJECT_WITH_DC, true); checkExpectations(); } @Test public void dcDatesShortFields() throws ServerException { setSqlUtilityInstance(new UpdatingMockSqlUtility(SHORT_FIELDS, OBJECT_WITH_DC_AND_DATES.getShortFieldValueList())); this.mockConnection = new UnusedMockConnection(); this.mockRepositoryReader = new UnusedMockRepositoryReader(); updateRecord(OBJECT_WITH_DC_AND_DATES, false); checkExpectations(); } @Test public void dcDatesLongFields() throws ServerException { setSqlUtilityInstance(new UpdatingMockSqlUtility(LONG_FIELDS, OBJECT_WITH_DC_AND_DATES.getLongFieldValueList())); this.mockConnection = new UpdatingMockConnection(); this.expectedDateDeletes = 1; this.expectedDateInserts = 1; this.mockRepositoryReader = new UnusedMockRepositoryReader(); updateRecord(OBJECT_WITH_DC_AND_DATES, true); checkExpectations(); } private void updateRecord(ObjectData objectData, boolean longFields) throws ServerException { // Create a DC datastream if appropriate. DatastreamXMLMetadata dcmd = null; if (objectData.getDcPayload() != null) { dcmd = new DatastreamXMLMetadata(); dcmd.DatastreamID = "DC"; dcmd.DSCreateDT = objectData.getDcModifiedDate(); dcmd.xmlContent = objectData.getDcPayload().getBytes(); } // Create the object and populate it. BasicDigitalObject theObject = new BasicDigitalObject(); theObject.setPid(objectData.getPid()); theObject.setLabel(objectData.getLabel()); theObject.setState(objectData.getState()); theObject.setOwnerId(objectData.getOwnerId()); theObject.setCreateDate(objectData.getCreateDate()); theObject.setLastModDate(objectData.getLastModDate()); if (dcmd != null) { theObject.addDatastreamVersion(dcmd, false); } // Create the test instance. FieldSearchSQLImpl fssi = new FieldSearchSQLImpl(this.connectionPool, this.mockRepositoryReader, 50, 50, longFields); // And do the update. fssi.update(new MockDOReader(theObject)); } private void checkExpectations() { ((MockSqlUtility) getSqlUtilityInstance()).checkExpectations(); if (mockConnection instanceof UpdatingMockConnection) { ((UpdatingMockConnection) mockConnection).checkExpectations( expectedDateDeletes, expectedDateInserts); } if (mockRepositoryReader instanceof SDepMockRepositoryReader) { ((SDepMockRepositoryReader) mockRepositoryReader) .checkExpectations(); } } private void assertEqualArrays(String label, Object[] expected, Object[] actual) { if (!Arrays.equals(expected, actual)) { fail(label + ", expected: " + Arrays.deepToString(expected) + ", actual: " + Arrays.deepToString(actual)); } } private void assertEqualValues(String[] columns, Object[] expected, Object[] actual) { if (Arrays.equals(expected, actual)) { return; } String noValue = "_NO_VALUE_"; String message = ""; List<String> badColumns = new ArrayList<String>(); for (int i = 0; i < columns.length; i++) { Object expectedValue = (i < expected.length) ? expected[i] : noValue; Object actualValue = (i < actual.length) ? actual[i] : noValue; if (!equivalent(expectedValue, actualValue)) { badColumns.add(columns[i]); } String expectedString = (expectedValue == noValue) ? noValue : (expectedValue == null) ? "null" : (expected[i] .getClass().getName() + "[" + expected[i] + "]"); String actualString = (actualValue == noValue) ? noValue : (actualValue == null) ? "null" : (actual[i].getClass() .getName() + "[" + actual[i] + "]"); message += String.format("column '%s', expected=%s, actual=%s\n", columns[i], expectedString, actualString); } if (!badColumns.isEmpty()) { message = "bad columns: " + badColumns + "\n" + message; } fail(message); } private boolean equivalent(Object o1, Object o2) { return (o1 == null) ? (o2 == null) : (o1.equals(o2)); } /** * Reach into the {@link SQLUtility} class and get the instance that is * handling the JDBC-based methods. */ private static SQLUtility getSqlUtilityInstance() { try { Field instanceField = SQLUtility.class.getDeclaredField("instance"); instanceField.setAccessible(true); return (SQLUtility) instanceField.get(null); } catch (SecurityException e) { fail("Failed to set SqlUtility instance: " + e); } catch (NoSuchFieldException e) { fail("Failed to set SqlUtility instance: " + e); } catch (IllegalArgumentException e) { fail("Failed to set SqlUtility instance: " + e); } catch (IllegalAccessException e) { fail("Failed to set SqlUtility instance: " + e); } return null; } /** * Reach into the {@link SQLUtility} class and set an instance to handle the * JDBC-based methods. */ private static void setSqlUtilityInstance(SQLUtility instance) { try { Field instanceField = SQLUtility.class.getDeclaredField("instance"); instanceField.setAccessible(true); instanceField.set(null, instance); } catch (SecurityException e) { fail("Failed to set SqlUtility instance: " + e); } catch (NoSuchFieldException e) { fail("Failed to set SqlUtility instance: " + e); } catch (IllegalArgumentException e) { e.printStackTrace(); fail("Failed to set SqlUtility instance: " + e); } catch (IllegalAccessException e) { fail("Failed to set SqlUtility instance: " + e); } } /** * Base class for Mock {@link SQLUtility} implementations. Every method * causes the test to fail, unless overridden by the subclass. */ public abstract static class MockSqlUtility extends SQLUtility { public abstract void checkExpectations(); @Override protected void i_addRow(Connection conn, String table, String[] columns, String[] values, boolean[] numeric) throws SQLException { fail("Unexpected call to MockSqlUtility.i_addRow"); } @Override protected void i_createNonExistingTables(ConnectionPool pool, InputStream dbSpec) throws IOException, InconsistentTableSpecException, SQLException { fail("Unexpected call to MockSqlUtility.i_addRow"); } @Override protected void i_createTables(TableCreatingConnection tcConn, List<TableSpec> specs) throws SQLException { fail("Unexpected call to MockSqlUtility.i_addRow"); } @Override protected ConnectionPool i_getConnectionPool(DatastoreConfiguration cpDC) throws SQLException { fail("Unexpected call to MockSqlUtility.i_addRow"); return null; } @Override protected String i_getLongString(ResultSet rs, int pos) throws SQLException { fail("Unexpected call to MockSqlUtility.i_addRow"); return null; } @Override protected List<TableSpec> i_getNonExistingTables(Connection conn, List<TableSpec> specs) throws SQLException { fail("Unexpected call to MockSqlUtility.i_addRow"); return null; } @Override protected void i_replaceInto(Connection conn, String table, String[] columns, String[] values, String uniqueColumn, boolean[] numeric) throws SQLException { fail("Unexpected call to MockSqlUtility.i_addRow"); } @Override protected boolean i_updateRow(Connection conn, String table, String[] columns, String[] values, String uniqueColumn, boolean[] numeric) throws SQLException { fail("Unexpected call to MockSqlUtility.i_addRow"); return false; } } public static class UnusedMockSqlUtility extends MockSqlUtility { @Override public void checkExpectations() { // Nothing to check. } } private class UpdatingMockSqlUtility extends MockSqlUtility { private final String[] expectedColumns; private final String[] expectedValues; private String[] actualColumns; private String[] actualValues; /** * Write down some of what we expect to have happen. * * @param expectedColumns * @param expectedValues */ public UpdatingMockSqlUtility(String[] expectedColumns, List<String> expectedValues) { this.expectedColumns = expectedColumns; this.expectedValues = expectedValues .toArray(new String[expectedValues.size()]); } /** * If we get a replace call, store the columns and values for testing * later. (If we get more then one call, only the last will be * retained.) */ @Override protected void i_replaceInto(Connection conn, String table, String[] columns, String[] values, String uniqueColumn, boolean[] numeric) throws SQLException { this.actualColumns = columns; this.actualValues = values; } @Override public void checkExpectations() { assertEqualArrays("column names", expectedColumns, actualColumns); assertEqualValues(expectedColumns, expectedValues, actualValues); } } private static class UnusedMockConnection extends MockConnection { @Override public Statement createStatement() throws SQLException { fail("Unexpected call to UnusedMockConnection.createStatement"); return null; } } private static class UpdatingMockConnection extends MockConnection { private int deleteCalls = 0; private int insertCalls = 0; @Override public Statement createStatement() throws SQLException { return new MockStatement() { @Override public int executeUpdate(String sql) throws SQLException { if (sql.trim().toLowerCase().startsWith("insert")) { insertCalls++; } if (sql.trim().toLowerCase().startsWith("delete")) { deleteCalls++; } return 1; } }; } public void checkExpectations(int expectedDeletes, int expectedInserts) { assertEquals("delete calls", expectedDeletes, deleteCalls); assertEquals("insert calls", expectedInserts, insertCalls); } } private static class UnusedMockRepositoryReader extends MockRepositoryReader { @Override public synchronized ServiceDeploymentReader getServiceDeploymentReader( boolean cachedObjectRequired, Context context, String pid) throws ServerException { fail("Unexpected call to UnusedMockRepositoryReader.getServiceDeploymentReader"); return null; } } private static class SDepMockRepositoryReader extends MockRepositoryReader { private int calls; public SDepMockRepositoryReader() { } public void checkExpectations() { assertEquals("sDep reader calls", 1, calls); } @Override public synchronized ServiceDeploymentReader getServiceDeploymentReader( boolean cachedObjectRequired, Context context, String pid) throws ServerException { calls++; return new MockServiceDeploymentReader(null) { @Override public DeploymentDSBindSpec getServiceDSInputSpec(Date versDateTime) throws ServerException { DeploymentDSBindSpec spec = new DeploymentDSBindSpec(); return spec; } }; } } private class MyMockDriver extends MockDriver { @Override public Connection connect(String url, Properties info) throws SQLException { return mockConnection; } } private static class ObjectData { private final String pid; private final String label; private final String state; private final String ownerId; private final Date createDate; private final Date lastModDate; private final Date dcModifiedDate; private final String dcPayload; public ObjectData(String pid, String label, String state, String ownerId, Date createDate, Date lastModDate, Date dcModifiedDate, String dcPayload) { this.pid = pid; this.label = label; this.state = state; this.ownerId = ownerId; this.createDate = createDate; this.lastModDate = lastModDate; this.dcModifiedDate = dcModifiedDate; this.dcPayload = dcPayload; } public List<String> getShortFieldValueList() { List<String> result = new ArrayList<String>(); result.add(pid); result.add(lowerCase(label)); result.add(lowerCase(state)); result.add(lowerCase(ownerId)); result.add(dateStamp(createDate)); result.add(dateStamp(lastModDate)); result.add(dateStamp(dcModifiedDate)); return result; } public List<String> getLongFieldValueList() { List<String> result = new ArrayList<String>(); result.addAll(getShortFieldValueList()); result.add(lowerCase(getDcFields("dc:title"))); result.add(lowerCase(getDcFields("dc:creator"))); result.add(lowerCase(getDcFields("dc:subject"))); result.add(lowerCase(getDcFields("dc:description"))); result.add(lowerCase(getDcFields("dc:publisher"))); result.add(lowerCase(getDcFields("dc:contributor"))); result.add(lowerCase(getDcFields("dc:date"))); result.add(lowerCase(getDcFields("dc:type"))); result.add(lowerCase(getDcFields("dc:format"))); result.add(lowerCase(getDcFields("dc:identifier"))); result.add(lowerCase(getDcFields("dc:source"))); result.add(lowerCase(getDcFields("dc:language"))); result.add(lowerCase(getDcFields("dc:relation"))); result.add(lowerCase(getDcFields("dc:coverage"))); result.add(lowerCase(getDcFields("dc:rights"))); return result; } public String getPid() { return pid; } public String getLabel() { return label; } public String getState() { return state; } public String getOwnerId() { return ownerId; } public Date getCreateDate() { return createDate; } public Date getLastModDate() { return lastModDate; } public Date getDcModifiedDate() { return dcModifiedDate; } public String getDcPayload() { return dcPayload; } private String lowerCase(String raw) { return (raw == null) ? null : raw.toLowerCase(); } private String dateStamp(Date date) { return (date == null) ? null : String.valueOf(date.getTime()); } private String getDcFields(String fieldName) { String pString = String.format("<%1$s>\\s*([^<]*)\\s*</%1$s>", fieldName); Pattern p = Pattern.compile(pString); Matcher m = p.matcher(dcPayload); List<String> values = new ArrayList<String>(); int start = 0; while (m.find(start)) { values.add(m.group(1)); start = m.end(); } return joinStrings(values); } private String joinStrings(Collection<String> strings) { if ((strings == null) || (strings.isEmpty())) { return null; } StringBuffer result = new StringBuffer(); for (String string : strings) { result.append(" ").append(string).append(" ."); } return result.toString(); } } }