Implementing CoherenceTarget
The last thing we need to do to complete the example is to create a Target interface implementation that will import items read from the CSV file into Coherence.
One of the things we will need to do is to convert the generic item representation from a Map
into an instance of a class that represents the value we want to put
into the cache. The naïve approach is to use Java reflection to create
an instance of a class and set property values, but just as with the CSV
parsing, the devil is in the details.
The property values read from
the CSV files are all strings, but the properties of the target object
might not be. That means that we need to perform type conversion as
appropriate when setting property values on a target object.
Fortunately, we don't need to
reinvent the wheel to do this. If you are familiar with the Spring
Framework, you already know that property values, specified as strings
within the XML configuration file, are automatically converted to
appropriate type before they are injected into your objects. What you
might not know is that this feature is easily accessible outside of
Spring as well, in the form of the BeanWrapper interface and BeanWrapperImpl class.
Another problem we need to solve
is key generation-when we put objects into a Coherence cache, we need
to specify both the key and the value. The simplest option, and the one
we will use for this example, is to extract the key from the target
object itself. This will often be all we need, as most entities already
have a field representing their identity, which is the ideal candidate
for a cache key. In the case of our Country class, we will use the value of the code property as a cache key.
Finally, while we could insert every item into the cache using individual put calls, this is not the most efficient way to perform bulk loading of the data. Achieving Performance, Scalability, and Availability Objectives, each call to the put
method is potentially a network call, and as such introduces some
latency. A significantly better approach from a performance perspective
is to batch multiple items and insert them into the cache all at once by
calling the putAll method.
So, with the design considerations out of the way, let's look at the implementation of the CoherenceTarget class:
public class CoherenceTarget implements Target {
public static final int DEFAULT_BATCH_SIZE = 1000;
private NamedCache cache;
private Class itemClass;
private String idProperty;
private Map batch;
private int batchSize = DEFAULT_BATCH_SIZE;
public CoherenceTarget(String cacheName, Class itemClass,
String idProperty) {
this.cache = CacheFactory.getCache(cacheName);
this.itemClass = itemClass;
this.idProperty = idProperty;
}
public void setBatchSize(int batchSize) {
this.batchSize = batchSize;
}
public void beginImport() {
batch = new HashMap();
}
public void importItem(Map<String, ?> sourceItem) {
BeanWrapper targetItem = new BeanWrapperImpl(itemClass);
for (Map.Entry<String, ?> property : sourceItem.entrySet()) {
targetItem.setPropertyValue(property.getKey(), property.getValue());
}
Object id = targetItem.getPropertyValue(idProperty);
batch.put(id, targetItem.getWrappedInstance());
if (batch.size() % batchSize == 0) {
cache.putAll(batch);
CoherenceTarget classimplementingbatch.clear();
}
}
public void endImport() {
if (!batch.isEmpty()) {
cache.putAll(batch);
}
}
}
The constructor accepts three
arguments: the name of the cache to import objects into, the class of
cache items, and the name of the property of that class that should be
used as a cache key. We initialize batch size to 1000 items by default, but the value can be easily overridden by calling the setBatchSize method.
The beginImport lifecycle method initializes the map representing a batch of items that need to be inserted, while the endImport method ensures that the last, potentially incomplete, batch is also inserted into the cache.
The real meat is in the importItem method, which creates a Spring BeanWrapper instance for the specified item class and sets its properties based on the entries in the sourceItem map. Once the target item is fully initialized, we use BeanWrapper again to extract the cache key from it and add the item to the batch.
Finally, whenever the
batch size reaches the specified limit, we insert all items from the
batch into the cache and clear the batch.
With this last piece in place, we are ready to test the loader.
Cache loader on steroids
It also contains both Source and Target
implementations for CSV files, XML files, and Coherence, allowing you
to import data from the existing CSV and XML files into Coherence, and
to export contents of a Coherence cache into one of these formats.
Testing the Cache loader
In order to test the loader, we will use a countries.csv
file containing a list of most countries in the world (I'd say all, but
being from Serbia I know firsthand that a list of countries in the
world is anything but static ☺).
The file header matches exactly the property names in the Country class defined earlier, which is the requirement imposed by our loader implementation:
code,name,capital,currencySymbol,currencyName
AFG,Afghanistan,Kabul,AFN,Afghani
ALB,Albania,Tirana,ALL,Lek
DZA,Algeria,Algiers,DZD,Dinar
AND,Andorra,Andorra la Vella,EUR,Euro
AGO,Angola,Luanda,AOA,Kwanza
With the test data in place, let's write a JUnit test that will load all of the countries defined in the countries.csv file into the Coherence cache:
public class LoaderTests {
public static final NamedCache countries = CacheFactory.getCache("countries");
@Before
public void clearCache() {
countries.clear();
}
@Test
public void testCsvToCoherenceLoader() {
Source source = new CsvSource("countries.csv");
CoherenceTarget target =
new CoherenceTarget("countries", Country.class, "code");
target.setBatchSize(50);
Loader loader = new Loader(source, target);
loader.load();
assertEquals(193, countries.size());
Country srb = (Country) countries.get("SRB");
assertEquals("Serbia", srb.getName());
assertEquals("Belgrade", srb.getCapital());
assertEquals("RSD", srb.getCurrencySymbol());
assertEquals("Dinar", srb.getCurrencyName());
}
}
As you can see, using the cache loader is quite simple-we initialize the source and target, create a Loader instance for them, and invoke the load
method. We then assert that all the data from the test CSV file has
been loaded, and that the properties of a single object retrieved from
the cache are set correctly.
However, the real purpose
of the previous test is neither to teach you how to use the loader (you
could've easily figured that out yourself) nor to prove that the code in
the preceding sections works (of course it does ☺). It is to lead us
into the very interesting subject that follows.