// Copyright (C) 2006-2009 Google Inc.
//
// 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 com.google.enterprise.connector.mock;
import com.google.enterprise.connector.mock.MockRepositoryEvent.EventType;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.logging.Logger;
/**
* Mock Document Store for Unit tests.
*
* This class provides a rudimentary memory-based content management system
* (CMS), to enable testing of the connector framework without external
* dependencies.
* <p>
* The CMS has a very simple document-model: see the MockRepositoryDocument
* class.
* <p>
* Documents have ID's: it is an important invariant that document ID's are
* unique.
* <p>
* When constructed, the CMS is empty. Documents can be added, changed, or
* removed by "events" (see MockRepositoryEvent). Thus the CMS is side-effected
* only through the following calls:
* <ul>
* <li> reinit() - throws away the current store, if any, and sets it to an
* empty state
* <li> applyEvent() - takes an event and applies it the store (adds, changes or
* deletes a document)
* </ul>
* <p>
* The CMS can be inspected by the following calls:
* <ul>
* <li> getDocByID() - returns the document with the specified ID, if present.
* Document must have unique IDs
* <li> iterator() - returns an iterator for all documents, in timestamp order.
* (Note: this class implements Iterable<MockRepositoryDocument>
* </ul>
* <p>
* Two integrity constraints are enforced after every side-effecting change:
* <ul>
* <li> checkDocidUniquenessIntegrity() - makes sure that IDs are unique
* <li> checkDateOrderIntegrity() - makes sure that the iterator returns
* documents in timestamp order
* </ul>
* <p>
* TODO(ziff):
* <ul>
* <li>add event call-back
* <li>add textual dump/load so bigger test cases can be built easily
* </ul>
*/
public class MockRepositoryDocumentStore
implements Iterable <MockRepositoryDocument> {
private static final Logger logger =
Logger.getLogger(MockRepositoryDocumentStore.class.getName());
Map<String, MockRepositoryDocument> store = null;
/**
* Makes an empty store
*/
public MockRepositoryDocumentStore() {
reinit();
}
/**
* Returns a store to the empty state
*/
public void reinit() {
store = new HashMap<String, MockRepositoryDocument>();
if (!checkIntegrity()) {
throw new RuntimeException("MockRepositoryStore integrity check failed");
}
}
/**
* Side-effect the store by applying an event. This could add, remove or
* change a document, depending on the event type. Note: the timestamp of the
* event becomes the timestanmp of the created or changed document. The
* implementation does NOT enforce that events are supplied in increasing
* timestamp order. Perhaps it would be a good idea to add this later.
*
* @param event
*/
public void applyEvent(MockRepositoryEvent event) {
if (event.getType() == EventType.SAVE) {
doSave(event);
} else if (event.getType() == EventType.DELETE) {
doDelete(event);
} else if (event.getType() == EventType.METADATA_ONLY_SAVE) {
doSave(event);
} else {
throw new IllegalArgumentException("Unknown event type");
}
if (!checkIntegrity()) {
throw new RuntimeException("Integrity check failed");
}
}
/**
* Performs the DELETE action. Deletes are keyed by docID. The other fields of
* the event are ignored.
*
* @param event Must be a DELETE event
*/
private void doDelete(MockRepositoryEvent event) {
String docID = event.getDocID();
MockRepositoryDocument d = getDocByID(docID);
if (d == null) {
return;
}
store.remove(docID);
}
/**
* Performs the SAVE and METADATA_ONLY_SAVE actions. Looks up the document by
* ID - if not present, it creates it. If present, it applies the changes
* specified in the event.
*
* @param event Must be a SAVE or METADATA_ONLY_SAVE event
*/
private void doSave(MockRepositoryEvent event) {
String docid = event.getDocID();
if (docid == null || docid.length() == 0) {
throw new RuntimeException("document has no id");
}
MockRepositoryDocument d = getDocByID(docid);
if (d == null) {
// this is a new document
d = new MockRepositoryDocument(event.getTimeStamp(), docid, event
.getContent(), event.getPropertyList());
store.put(docid, d);
} else {
// this is a change to an old document
if (event.getPropertyList() != null) {
d.getProplist().merge(event.getPropertyList());
}
String newContent = ((event.getContent() != null) ? event.getContent()
: d.getContent());
MockRepositoryDocument modifiedDoc = new MockRepositoryDocument(event
.getTimeStamp(), docid, newContent, d.getProplist());
store.remove(docid);
store.put(docid, modifiedDoc);
}
}
/**
* Looks up a document in the store by ID
*
* @param docid The ID to look for
* @return If found, the document; otherwise, null
*/
public MockRepositoryDocument getDocByID(String docid) {
return store.get(docid);
}
/**
* Returns an iterator over all documents in the store
*
* @return Iterator
*/
public Iterator<MockRepositoryDocument> iterator() {
List<MockRepositoryDocument> l =
new LinkedList<MockRepositoryDocument>(store.values());
sortDocuments(l);
return l.listIterator();
}
public int size() {
return store.size();
}
/**
* Returns all documents last modified between the two dates: specifically,
* all documents modified at a time greater than or equal to the from
* parameter and strictly less than the to parameter
*
* @param from
* @param to
* @return A List of these results
*/
public List<MockRepositoryDocument> dateRange(
final MockRepositoryDateTime from, final MockRepositoryDateTime to) {
List<MockRepositoryDocument> l = new ArrayList<MockRepositoryDocument>();
for (MockRepositoryDocument d : store.values()) {
int c1 = from.compareTo(d.getTimeStamp());
int c2 = d.getTimeStamp().compareTo(to);
if (c1 <= 0 && c2 < 0) {
l.add(d);
}
}
sortDocuments(l);
return l;
}
/**
* Returns all documents last modified on or after a given date.
*
* @param from
* @return A list of these results
*/
public List<MockRepositoryDocument> dateRange(
final MockRepositoryDateTime from) {
List<MockRepositoryDocument> l = new ArrayList<MockRepositoryDocument>();
for (MockRepositoryDocument d : store.values()) {
int c1 = from.compareTo(d.getTimeStamp());
if (c1 <= 0) {
l.add(d);
}
}
sortDocuments(l);
return l;
}
private void sortDocuments(List<MockRepositoryDocument> l) {
Collections.sort(l, new Comparator<MockRepositoryDocument>() {
public int compare(MockRepositoryDocument d1, MockRepositoryDocument d2) {
int c = d1.getTimeStamp().compareTo(d2.getTimeStamp());
if (c != 0) {
return c;
}
return d1.getDocID().compareTo(d2.getDocID());
}
});
}
/**
* Checks the date-order integrity constraint: iterates through the documents
* and makes sure the timnestamps are in ascending order.
*
* @return True or false, depending on whether the test passes
*/
private boolean checkDateOrderIntegrity() {
boolean result = true;
int lastStamp = -1;
for (MockRepositoryDocument d : this) {
int thisStamp = d.getTimeStamp().getTicks();
if (lastStamp > thisStamp) {
result = false;
logger.info("Docid " + d.getDocID() + " appears out of order");
logger.info("Timestamp: " + thisStamp + " follows stamp: " + lastStamp);
lastStamp = thisStamp;
}
}
return result;
}
/**
* Checks the docid uniqueness constraint: iterates through the documents and
* checks to see whether the ids have ever been seen before
*
* @return True or false, depending on whether the test passes
*/
private boolean checkDocidUniquenessIntegrity() {
boolean result = true;
Set<String> m = new HashSet<String>();
for (MockRepositoryDocument d : this) {
if (!m.add(d.getDocID())) {
// this docid appears more than once
result = false;
logger.info("Docid " + d.getDocID() + " appears more than once!");
}
}
return result;
}
/**
* Runs all integrity checks
*
* @return true or false, depending on whether ALL tests pass
*/
private boolean checkIntegrity() {
boolean result = true;
if (!checkDateOrderIntegrity()) {
result = false;
}
if (!checkDocidUniquenessIntegrity()) {
result = false;
}
return result;
}
}