View Javadoc

1   /*
2    * Copyright 2013 University of Glasgow.
3    *
4    * Licensed under the Apache License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    *
8    *      http://www.apache.org/licenses/LICENSE-2.0
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
15   */
16  package broadwick.data;
17  
18  import broadwick.data.readers.BatchedMovementsFileReader;
19  import broadwick.data.readers.DirectedMovementsFileReader;
20  import broadwick.data.readers.FullMovementsFileReader;
21  import broadwick.data.readers.LocationsFileReader;
22  import broadwick.data.readers.PopulationsFileReader;
23  import broadwick.data.readers.TestsFileReader;
24  import com.google.common.base.Throwables;
25  import com.google.common.cache.Cache;
26  import com.google.common.cache.CacheBuilder;
27  import java.io.Serializable;
28  import java.sql.Connection;
29  import java.sql.SQLException;
30  import java.util.ArrayList;
31  import java.util.Collection;
32  import java.util.Comparator;
33  import java.util.HashMap;
34  import java.util.HashSet;
35  import java.util.Map;
36  import lombok.Getter;
37  import lombok.extern.slf4j.Slf4j;
38  import org.apache.commons.lang3.time.StopWatch;
39  import org.jooq.DSLContext;
40  import org.jooq.Record;
41  import org.jooq.Record1;
42  import org.jooq.Record2;
43  import org.jooq.Result;
44  import org.jooq.conf.Settings;
45  import org.jooq.exception.DataAccessException;
46  import org.jooq.impl.DSL;
47  
48  /**
49   * This class works as an interface to the databases holding the movements, locations and animal data read from the
50   * configuration file. When the methods to retrieve all the data in the DB (e.g. getMovements()) are called the results
51   * are stored in a cache for speedier retrieval. This cache is not permanent however, freeing memory when required.
52   */
53  @Slf4j
54  public final class Lookup {
55  
56      /**
57       * Create the lookup object for accessing data in the internal databases.
58       * @param dbFacade the object that is responsible for accessing the internal databases.
59       */
60      public Lookup(final DatabaseImpl dbFacade) {
61          try {
62              connection = dbFacade.getConnection();
63              final Settings settings = new Settings();
64              settings.setExecuteLogging(Boolean.FALSE);
65              jooq = DSL.using(dbFacade.getConnection(), dbFacade.getDialect(), settings);
66          } catch (SQLException e) {
67              log.error("Could not create database lookup object. {}", Throwables.getStackTraceAsString(e));
68          }
69      }
70  
71      /**
72       * Get the number of tests stored in the internal database.
73       * @return the number of tests in the database.
74       */
75      public int getNumTests() {
76          int numTests = 0;
77          try {
78              final Result<Record1<Integer>> fetch = jooq.selectCount().from(TestsFileReader.getTABLE_NAME()).fetch();
79              numTests = fetch.get(0).value1();
80          } catch (org.jooq.exception.DataAccessException e) {
81              log.trace("Could not get number of tests - perhaps the table hasn't been created.");
82          }
83          return numTests;
84      }
85  
86      /**
87       * Get the number of animals stored in the internal database.
88       * @return the number of animals in the database.
89       */
90      public int getNumAnimals() {
91          int numAnimals = 0;
92          try {
93              final Result<Record1<Integer>> fetch = jooq.selectCount().from(PopulationsFileReader.getLIFE_HISTORIES_TABLE_NAME()).fetch();
94              numAnimals = fetch.get(0).value1();
95          } catch (org.jooq.exception.DataAccessException e) {
96              log.trace("Could not get number of animals - perhaps the table hasn't been created.");
97          }
98          return numAnimals;
99      }
100 
101     /**
102      * Get the number of locations stored in the internal database.
103      * @return the number of locations in the database.
104      */
105     public int getNumLocations() {
106         int numLocations = 0;
107         try {
108             final Result<Record1<Integer>> fetch = jooq.selectCount().from(LocationsFileReader.getTABLE_NAME()).fetch();
109             numLocations = fetch.get(0).value1();
110         } catch (org.jooq.exception.DataAccessException e) {
111             log.trace("Could not get number of locations - perhaps the table hasn't been created.");
112         }
113         return numLocations;
114     }
115 
116     /**
117      * Get the number of movements stored in the internal database.
118      * @return the number of movements in the database.
119      */
120     public int getNumMovements() {
121         int numMovements = 0;
122 
123         try {
124             numMovements = jooq.selectCount().from(BatchedMovementsFileReader.getTABLE_NAME()).fetch().get(0).value1();
125         } catch (org.jooq.exception.DataAccessException e) {
126             log.trace("Could not get number of movements from {} - perhaps the table hasn't been created.",
127                       BatchedMovementsFileReader.getTABLE_NAME());
128         }
129         try {
130             numMovements += jooq.selectCount().from(FullMovementsFileReader.getTABLE_NAME()).fetch().get(0).value1();
131         } catch (org.jooq.exception.DataAccessException e) {
132             log.trace("Could not get number of movements from {} - perhaps the table hasn't been created.",
133                       FullMovementsFileReader.getTABLE_NAME());
134         }
135         try {
136             numMovements += jooq.selectCount().from(DirectedMovementsFileReader.getTABLE_NAME()).fetch().get(0).value1();
137         } catch (org.jooq.exception.DataAccessException e) {
138             log.trace("Could not get number of movements from {} - perhaps the table hasn't been created.",
139                       DirectedMovementsFileReader.getTABLE_NAME());
140         }
141         return numMovements;
142     }
143 
144     /**
145      * Get the number of movements stored in the internal database filtered on a date range.
146      * @param startDate the first date in the range with which we will filter the movements
147      * @param endDate   the final date in the range with which we will filter the movements
148      * @return a collection of movement events that have been recorded.
149      */
150     public int getNumMovements(final int startDate, final int endDate) {
151         int numMovements = 0;
152 
153         try {
154             numMovements = jooq.selectCount().from(BatchedMovementsFileReader.getTABLE_NAME())
155                     .where(String.format("%s >= %d and %s <= %d",
156                                          BatchedMovementsFileReader.getDEPARTURE_DATE(), startDate,
157                                          BatchedMovementsFileReader.getDESTINATION_DATE(), endDate)).fetch().get(0).value1();
158         } catch (org.jooq.exception.DataAccessException e) {
159             log.trace("Could not get number of movements from {} - perhaps the table hasn't been created.",
160                       BatchedMovementsFileReader.getTABLE_NAME());
161         }
162         try {
163             numMovements += jooq.selectCount().from(FullMovementsFileReader.getTABLE_NAME())
164                     .where(String.format("%s >= %d and %s <= %d",
165                                          FullMovementsFileReader.getDEPARTURE_DATE(), startDate,
166                                          FullMovementsFileReader.getDESTINATION_DATE(), endDate))
167                     .fetch().get(0).value1();
168         } catch (org.jooq.exception.DataAccessException e) {
169             log.trace("Could not get number of movements from {} - perhaps the table hasn't been created.",
170                       FullMovementsFileReader.getTABLE_NAME());
171         }
172         try {
173             numMovements += jooq.selectCount().from(DirectedMovementsFileReader.getTABLE_NAME())
174                     .where(String.format("%s >= %d and %s <= %d",
175                                          DirectedMovementsFileReader.getMOVEMENT_DATE(), startDate,
176                                          DirectedMovementsFileReader.getMOVEMENT_DATE(), endDate))
177                     .fetch().get(0).value1();
178         } catch (org.jooq.exception.DataAccessException e) {
179             log.trace("Could not get number of movements from {} - perhaps the table hasn't been created.",
180                       DirectedMovementsFileReader.getTABLE_NAME());
181         }
182         return numMovements;
183     }
184 
185     /**
186      * Get all the movements that have been read from the file(s) specified in the configuration file.
187      * @return a collection of movement events that have been recorded.
188      */
189     public Collection<Movement> getMovements() {
190         final Collection<Movement> movements = new HashSet<>();
191         final StopWatch sw = new StopWatch();
192         sw.start();
193 
194         Result<Record> records;
195         try {
196             records = jooq.select().from(BatchedMovementsFileReader.getTABLE_NAME()).fetch();
197             for (Record r : records) {
198                 final Movement movement = createMovement(r);
199                 if (movement != null) {
200                     movements.add(movement);
201                 }
202             }
203         } catch (org.jooq.exception.DataAccessException e) {
204             log.trace("Could not get movements from {} - this is not an error; the project might be configutred with one.",
205                       BatchedMovementsFileReader.getTABLE_NAME());
206         }
207 
208         try {
209             records = jooq.select().from(FullMovementsFileReader.getTABLE_NAME()).fetch();
210             for (Record r : records) {
211                 final Movement movement = createMovement(r);
212                 if (movement != null) {
213                     movements.add(movement);
214                 }
215             }
216         } catch (org.jooq.exception.DataAccessException e) {
217             log.trace("Could not get movements from {} - this is not an error; the project might be configutred with one.",
218                       FullMovementsFileReader.getTABLE_NAME());
219         }
220 
221         try {
222             records = jooq.select().from(DirectedMovementsFileReader.getTABLE_NAME()).fetch();
223             for (Record r : records) {
224                 final Movement movement = createMovement(r);
225                 if (movement != null) {
226                     movements.add(movement);
227                 }
228             }
229         } catch (org.jooq.exception.DataAccessException e) {
230             log.trace("Could not get movements from {} - this is not an error; the project might be configutred with one.",
231                       DirectedMovementsFileReader.getTABLE_NAME());
232         }
233 
234         sw.stop();
235         log.debug("Found {} movements in {}.", movements.size(), sw.toString());
236         return movements;
237     }
238 
239     /**
240      * Get all the movements that have been read from the file(s) specified in the configuration file filtered on a date
241      * range.
242      * @param startDate the first date in the range with which we will filter the movements
243      * @param endDate   the final date in the range with which we will filter the movements
244      * @return a collection of movement events that have been recorded.
245      */
246     public Collection<Movement> getMovements(final int startDate, final int endDate) {
247         log.trace("Getting all movements between {} and {}", startDate, endDate);
248         final Collection<Movement> movements = new HashSet<>();
249         final StopWatch sw = new StopWatch();
250         sw.start();
251 
252         try {
253             // try directed movements
254             Result<Record> records;
255 
256             try {
257                 records = jooq.select().from(DirectedMovementsFileReader.getTABLE_NAME())
258                         .where(String.format("%s >= %d and %s <= %d",
259                                              DirectedMovementsFileReader.getMOVEMENT_DATE(), startDate,
260                                              DirectedMovementsFileReader.getMOVEMENT_DATE(), endDate))
261                         .fetch();
262                 for (Record r : records) {
263                     final Movement movement = createMovement(r);
264                     if (movement != null) {
265                         movements.add(movement);
266                     }
267                 }
268             } catch (org.jooq.exception.DataAccessException e) {
269                 log.trace("Could not get movements from {} - this is not an error; the project might be configutred with one.",
270                           DirectedMovementsFileReader.getTABLE_NAME());
271             }
272 
273             try {
274                 // try full movements
275                 records = jooq.select().from(FullMovementsFileReader.getTABLE_NAME())
276                         .where(String.format("%s >= %d and %s <= %d",
277                                              FullMovementsFileReader.getDEPARTURE_DATE(), startDate,
278                                              FullMovementsFileReader.getDESTINATION_DATE(), endDate))
279                         .fetch();
280                 for (Record r : records) {
281                     final Movement movement = createMovement(r);
282                     if (movement != null) {
283                         movements.add(movement);
284                     }
285                 }
286             } catch (org.jooq.exception.DataAccessException e) {
287                 log.trace("Could not get movements from {} - this is not an error; the project might be configutred with one.",
288                           FullMovementsFileReader.getTABLE_NAME());
289             }
290 
291             try {
292                 // try batched movements
293                 records = jooq.select().from(BatchedMovementsFileReader.getTABLE_NAME())
294                         .where(String.format("%s >= %d and %s <= %d",
295                                              BatchedMovementsFileReader.getDEPARTURE_DATE(), startDate,
296                                              BatchedMovementsFileReader.getDESTINATION_DATE(), endDate))
297                         .fetch();
298                 for (Record r : records) {
299                     final Movement movement = createMovement(r);
300                     if (movement != null) {
301                         movements.add(movement);
302                     }
303                 }
304             } catch (org.jooq.exception.DataAccessException e) {
305                 log.trace("Could not get movements from {} - this is not an error; the project might be configutred with one.",
306                           BatchedMovementsFileReader.getTABLE_NAME());
307             }
308 
309         } catch (org.jooq.exception.DataAccessException e) {
310             // We WILL get an error here - we don't know the type of table here so we try directed, batched and full
311             // movements. We can ignore the errors.
312         }
313 
314         sw.stop();
315         log.debug("Found {} movements in {}.", movements.size(), sw.toString());
316         return movements;
317     }
318 
319     /**
320      * Get all the OFF movements that have been read from the file(s) specified in the configuration file filtered on a
321      * date range.
322      * @param startDate the first date in the range with which we will filter the movements
323      * @param endDate   the final date in the range with which we will filter the movements
324      * @return a collection of movement events that have been recorded.
325      */
326     public Collection<Movement> getOffMovements(final int startDate, final int endDate) {
327         log.trace("Getting off movements between {} and {}", startDate, endDate);
328         final Collection<Movement> movements = new HashSet<>();
329         final StopWatch sw = new StopWatch();
330         sw.start();
331 
332         try {
333             // try directed movements
334             Result<Record> records;
335 
336             try {
337                 records = jooq.select().from(DirectedMovementsFileReader.getTABLE_NAME())
338                         .where(String.format("%s >= %d AND %s <= %d AND %s ='OFF'",
339                                              DirectedMovementsFileReader.getMOVEMENT_DATE(), startDate,
340                                              DirectedMovementsFileReader.getMOVEMENT_DATE(), endDate,
341                                              DirectedMovementsFileReader.getMOVEMENT_DIRECTION()))
342                         .fetch();
343                 for (Record r : records) {
344                     final Movement movement = createMovement(r);
345                     if (movement != null) {
346                         movements.add(movement);
347                     }
348                 }
349             } catch (org.jooq.exception.DataAccessException e) {
350                 log.trace("Could not get movements from {} - this is not an error; the project might be configutred with one.",
351                           DirectedMovementsFileReader.getTABLE_NAME());
352             }
353 
354             try {
355                 // try full movements
356                 records = jooq.select().from(FullMovementsFileReader.getTABLE_NAME())
357                         .where(String.format("%s >= %d and %s <= %d",
358                                              FullMovementsFileReader.getDEPARTURE_DATE(), startDate,
359                                              FullMovementsFileReader.getDEPARTURE_DATE(), endDate))
360                         .fetch();
361                 for (Record r : records) {
362                     final Movement movement = createMovement(r);
363                     if (movement != null) {
364                         movements.add(movement);
365                     }
366                 }
367             } catch (org.jooq.exception.DataAccessException e) {
368                 log.trace("Could not get movements from {} - this is not an error; the project might be configutred with one.",
369                           FullMovementsFileReader.getTABLE_NAME());
370             }
371 
372             try {
373                 // try batched movements
374                 records = jooq.select().from(BatchedMovementsFileReader.getTABLE_NAME())
375                         .where(String.format("%s >= %d and %s <= %d",
376                                              BatchedMovementsFileReader.getDEPARTURE_DATE(), startDate,
377                                              BatchedMovementsFileReader.getDEPARTURE_DATE(), endDate))
378                         .fetch();
379                 for (Record r : records) {
380                     final Movement movement = createMovement(r);
381                     if (movement != null) {
382                         movements.add(movement);
383                     }
384                 }
385             } catch (org.jooq.exception.DataAccessException e) {
386                 log.trace("Could not get movements from {} - this is not an error; the project might be configutred with one.",
387                           BatchedMovementsFileReader.getTABLE_NAME());
388             }
389 
390         } catch (org.jooq.exception.DataAccessException e) {
391             // We WILL get an error here - we don't know the type of table here so we try directed, batched and full
392             // movements. We can ignore the errors.
393         }
394 
395         sw.stop();
396         log.debug("Found {} off movements in {}.", movements.size(), sw.toString());
397         return movements;
398     }
399 
400     /**
401      * Get all the ON movements that have been read from the file(s) specified in the configuration file filtered on a
402      * date range.
403      * @param startDate the first date in the range with which we will filter the movements
404      * @param endDate   the final date in the range with which we will filter the movements
405      * @return a collection of movement events that have been recorded.
406      */
407     public Collection<Movement> getOnMovements(final int startDate, final int endDate) {
408         log.trace("Getting on movements between {} and {}", startDate, endDate);
409         final Collection<Movement> movements = new HashSet<>();
410         final StopWatch sw = new StopWatch();
411         sw.start();
412 
413         try {
414             // try directed movements
415             Result<Record> records;
416 
417             try {
418                 records = jooq.select().from(DirectedMovementsFileReader.getTABLE_NAME())
419                         .where(String.format("%s >= %d AND %s <= %d AND %s ='ON'",
420                                              DirectedMovementsFileReader.getMOVEMENT_DATE(), startDate,
421                                              DirectedMovementsFileReader.getMOVEMENT_DATE(), endDate,
422                                              DirectedMovementsFileReader.getMOVEMENT_DIRECTION()))
423                         .fetch();
424                 for (Record r : records) {
425                     final Movement movement = createMovement(r);
426                     if (movement != null) {
427                         movements.add(movement);
428                     }
429                 }
430             } catch (org.jooq.exception.DataAccessException e) {
431                 log.trace("Could not get movements from {} - this is not an error; the project might be configutred with one.",
432                           DirectedMovementsFileReader.getTABLE_NAME());
433             }
434 
435             try {
436                 // try full movements
437                 records = jooq.select().from(FullMovementsFileReader.getTABLE_NAME())
438                         .where(String.format("%s >= %d and %s <= %d",
439                                              FullMovementsFileReader.getDESTINATION_DATE(), startDate,
440                                              FullMovementsFileReader.getDESTINATION_DATE(), endDate))
441                         .fetch();
442                 for (Record r : records) {
443                     final Movement movement = createMovement(r);
444                     if (movement != null) {
445                         movements.add(movement);
446                     }
447                 }
448             } catch (org.jooq.exception.DataAccessException e) {
449                 log.trace("Could not get movements from {} - this is not an error; the project might be configutred with one.",
450                           FullMovementsFileReader.getTABLE_NAME());
451             }
452 
453             try {
454                 // try batched movements
455                 records = jooq.select().from(BatchedMovementsFileReader.getTABLE_NAME())
456                         .where(String.format("%s >= %d and %s <= %d",
457                                              BatchedMovementsFileReader.getDESTINATION_DATE(), startDate,
458                                              BatchedMovementsFileReader.getDESTINATION_DATE(), endDate))
459                         .fetch();
460                 for (Record r : records) {
461                     final Movement movement = createMovement(r);
462                     if (movement != null) {
463                         movements.add(movement);
464                     }
465                 }
466             } catch (org.jooq.exception.DataAccessException e) {
467                 log.trace("Could not get movements from {} - this is not an error; the project might be configutred with one.",
468                           BatchedMovementsFileReader.getTABLE_NAME());
469             }
470 
471         } catch (org.jooq.exception.DataAccessException e) {
472             // We WILL get an error here - we don't know the type of table here so we try directed, batched and full
473             // movements. We can ignore the errors.
474         }
475 
476         sw.stop();
477         log.debug("Found {} on movements in {}.", movements.size(), sw.toString());
478         return movements;
479     }
480 
481     /**
482      * Get all the tests that have been read from the file(s) specified in the configuration file.
483      * @return a collection of movement events that have been recorded.
484      */
485     public Collection<Test> getTests() {
486         final Collection<Test> tests = new ArrayList<>();
487         final StopWatch sw = new StopWatch();
488         sw.start();
489 
490         final Result<Record> records = jooq.select().from(TestsFileReader.getTABLE_NAME()).fetch();
491         for (Record r : records) {
492             final Test test = createTest(r);
493             if (test != null) {
494                 tests.add(test);
495             }
496         }
497 
498         sw.stop();
499         log.debug("Found {} tests in {}.", tests.size(), sw.toString());
500         return tests;
501     }
502 
503     /**
504      * Get all the tests that have been read from the file(s) specified in the configuration file.
505      * @param startDate the first date in the range with which we will filter the tests.
506      * @param endDate   the final date in the range with which we will filter the tests.
507      * @return a collection of movement events that have been recorded.
508      */
509     public Collection<Test> getTests(final int startDate, final int endDate) {
510         final Collection<Test> tests = new ArrayList<>();
511         final StopWatch sw = new StopWatch();
512         sw.start();
513 
514         final Result<Record> records = jooq.select().from(TestsFileReader.getTABLE_NAME())
515                 .where(String.format("%s >= %d and %s <= %d",
516                                      TestsFileReader.getTEST_DATE(), startDate,
517                                      TestsFileReader.getTEST_DATE(), endDate))
518                 .fetch();
519         for (Record r : records) {
520             final Test test = createTest(r);
521             if (test != null) {
522                 tests.add(test);
523             }
524         }
525 
526         sw.stop();
527         log.debug("Found {} tests in {}.", tests.size(), sw.toString());
528         return tests;
529     }
530 
531     /**
532      * Get all the animals that have been read from the file(s) specified in the configuration file.
533      * @return a collection of animal events that have been recorded.
534      */
535     public Collection<Animal> getAnimals() {
536         final Collection<Animal> animals = new HashSet<>();
537         final StopWatch sw = new StopWatch();
538         sw.start();
539 
540         final Result<Record> records = jooq.select().from(PopulationsFileReader.getLIFE_HISTORIES_TABLE_NAME()).fetch();
541         for (Record r : records) {
542             final Animal animal = createAnimal(r);
543             if (animal != null) {
544                 animals.add(animal);
545             }
546         }
547 
548         sw.stop();
549         log.debug("Found {} animals in {}.", animals.size(), sw.toString());
550         return animals;
551     }
552 
553     /**
554      * Get all the animals that have been read from the file(s) specified in the configuration file whose date of birth
555      * is before or on a given date and whose date of death (it there is any) is on or after the same date.
556      * @param date the date for which we reuqire the animals in the system.
557      * @return a collection of animals that have been recorded whose DoB &ge; date and DoD &ge; date
558      */
559     public Collection<Animal> getAnimals(final int date) {
560         final Collection<Animal> animals = new HashSet<>();
561         final String whereClause = String.format("%s <= %d and (%s IS NULL or %s >= %d)",
562                                                  PopulationsFileReader.getDATE_OF_BIRTH(), date,
563                                                  PopulationsFileReader.getDATE_OF_DEATH(),
564                                                  PopulationsFileReader.getDATE_OF_DEATH(), date);
565 
566         final StopWatch sw = new StopWatch();
567         sw.start();
568 
569         final Result<Record> records = jooq.select().from(PopulationsFileReader.getLIFE_HISTORIES_TABLE_NAME()).where(whereClause)
570                 .fetch();
571         for (Record r : records) {
572             final Animal animal = createAnimal(r);
573             if (animal != null) {
574                 animals.add(animal);
575             }
576         }
577 
578         sw.stop();
579         log.debug("Found {} animals in {}.", animals.size(), sw.toString());
580         return animals;
581     }
582 
583     /**
584      * Get all the movements that have been read from the file(s) specified in the configuration file.
585      * @return a collection of movement events that have been recorded.
586      */
587     public Collection<Location> getLocations() {
588         final Collection<Location> locations = new ArrayList<>();
589         final StopWatch sw = new StopWatch();
590         sw.start();
591 
592         final Result<Record> records = jooq.select().from(LocationsFileReader.getTABLE_NAME()).fetch();
593         for (Record r : records) {
594             final Location location = createLocation(r);
595             if (location != null) {
596                 locations.add(location);
597             }
598         }
599 
600         sw.stop();
601         log.debug("Found {} locations in {}.", locations.size(), sw.toString());
602         return locations;
603     }
604 
605     /**
606      * Get a location from the list of locations in the system. If there is no location matching the id a
607      * BroadwickException is thrown because we should only be looking for valid locations. Note, this method returns a
608      * live view of the movements so changes to one affect the other and in a worst case scenario can cause a
609      * ConcurrentModificationException. The returned collection isn't threadsafe or serializable, even if unfiltered is.
610      * @param locationId the id of the location we are looking for.
611      * @return the Location object with the required id.
612      */
613     public Location getLocation(final String locationId) {
614         Location location = locationsCache.getIfPresent(locationId);
615         if (location == null) {
616             final Result<Record> records = jooq.select().from(LocationsFileReader.getTABLE_NAME())
617                     .where(String.format("%s = '%s'", LocationsFileReader.getID(), locationId)).fetch();
618             for (Record r : records) {
619                 location = createLocation(r);
620                 locationsCache.put(location.getId(), location);
621             }
622         }
623         return location;
624     }
625 
626     /**
627      * Get an animal from the list of animals in the system. If there is no animal matching the id a BroadwickException
628      * is thrown because we should only be looking for valid animals. Note, this method returns a live view of the
629      * movements so changes to one affect the other and in a worst case scenario can cause a
630      * ConcurrentModificationException. The returned collection isn't threadsafe or serializable, even if unfiltered is.
631      * @param animalId the id of the animal we are looking for.
632      * @return the Animal object with the required id.
633      */
634     public Animal getAnimal(final String animalId) {
635         Animal animal = animalsCache.getIfPresent(animalId);
636         if (animal == null) {
637             final Result<Record> records = jooq.select().from(PopulationsFileReader.getLIFE_HISTORIES_TABLE_NAME())
638                     .where(String.format("ID = '%s'", animalId)).fetch();
639             for (Record r : records) {
640                 animal = createAnimal(r);
641                 animalsCache.put(animal.getId(), animal);
642             }
643         }
644         return animal;
645     }
646 
647     /**
648      * Get all the recorded movements for a given animal. Note, this method returns a live view of the movements so
649      * changes to one affect the other and in a worst case scenario can cause a ConcurrentModificationException. The
650      * returned collection isn't threadsafe or serializable, even if unfiltered is.
651      * @param animalId the id of the animal whose movements are to be returned.
652      * @return a collection of movement events that have been recorded for the animal with the given id.
653      */
654     public Collection<Movement> getMovementsForAnimal(final String animalId) {
655         final Collection<Movement> movements = new HashSet<>();
656         final StopWatch sw = new StopWatch();
657         sw.start();
658 
659         Result<Record> records;
660         try {
661             records = jooq.select().from(FullMovementsFileReader.getTABLE_NAME())
662                     .where(String.format("%s = '%s'", FullMovementsFileReader.getID(), animalId))
663                     .orderBy(DSL.fieldByName(FullMovementsFileReader.getDEPARTURE_DATE()).asc())
664                     .fetch();
665             for (Record r : records) {
666                 movements.add(createMovement(r));
667             }
668         } catch (org.jooq.exception.DataAccessException e) {
669             log.trace("Could not get movements from {} - this is not an error; the project might be configutred with one.",
670                       FullMovementsFileReader.getTABLE_NAME());
671         }
672 
673         try {
674             records = jooq.select().from(DirectedMovementsFileReader.getTABLE_NAME())
675                     .where(String.format("%s = '%s'", DirectedMovementsFileReader.getID(), animalId))
676                     .fetch();
677             for (Record r : records) {
678                 movements.add(createMovement(r));
679             }
680         } catch (org.jooq.exception.DataAccessException e) {
681             log.trace("Could not get movements from {} - this is not an error; the project might be configutred with one.",
682                       DirectedMovementsFileReader.getTABLE_NAME());
683         }
684 
685         sw.stop();
686         log.debug("Found {} movements in {}.", movements.size(), sw.toString());
687         return movements;
688     }
689 
690     /**
691      * Get an animals location at a specified date. If the animal does not have a specified location, e.g. if we are
692      * asking for its location before it's born or in the middle of a movement where the departure and destination dates
693      * span several days then a null location will be returned.
694      * @param animalId the id of the animal.
695      * @param date     the date for which we want the animals location.
696      * @return the location of the animal on date or Location.getNullLocation if there isn't a valid location.
697      */
698     public String getAnimalLocationIdAtDate(final String animalId, final int date) {
699 
700         String locationId = "";
701         int locationDate = Integer.MIN_VALUE;
702         Result<Record2<Object, Object>> records;
703         try {
704             // get the destination id of the last movement BEFORE the given date.
705             records = jooq.select(DSL.fieldByName(FullMovementsFileReader.getDESTINATION_ID()),
706                                   DSL.fieldByName(FullMovementsFileReader.getDESTINATION_DATE()))
707                     .from(FullMovementsFileReader.getTABLE_NAME())
708                     .where(String.format("%s = '%s' and (%s <= %d or %s <= %d)",
709                                          FullMovementsFileReader.getID(), animalId,
710                                          FullMovementsFileReader.getDEPARTURE_DATE(), date,
711                                          FullMovementsFileReader.getDESTINATION_DATE(), date))
712                     .orderBy(DSL.fieldByName(FullMovementsFileReader.getDESTINATION_DATE()).desc())
713                     .limit(1)
714                     .fetch();
715             if (records.isNotEmpty()) {
716                 final int thisDate = (int) records.getValue(0, DSL.fieldByName(FullMovementsFileReader.getDESTINATION_DATE()));
717                 if (thisDate > locationDate) {
718                     locationDate = (int) records.getValue(0, DSL.fieldByName(FullMovementsFileReader.getDESTINATION_DATE()));
719                     locationId = (String) records.getValue(0, DSL.fieldByName(FullMovementsFileReader.getDESTINATION_ID()));
720                 }
721             }
722         } catch (org.jooq.exception.DataAccessException e) {
723             log.trace("Could not get movements from {} - this is not an error; the project might be configutred with one.",
724                       FullMovementsFileReader.getTABLE_NAME());
725         }
726 
727         try {
728             records = jooq.select(DSL.fieldByName(DirectedMovementsFileReader.getLOCATION_ID()),
729                                   DSL.fieldByName(DirectedMovementsFileReader.getMOVEMENT_DATE()))
730                     .from(DirectedMovementsFileReader.getTABLE_NAME())
731                     .where(String.format("%s = '%s' and %s <= %d",
732                                          DirectedMovementsFileReader.getID(), animalId,
733                                          DirectedMovementsFileReader.getMOVEMENT_DATE(), date))
734                     .orderBy(DSL.fieldByName(DirectedMovementsFileReader.getMOVEMENT_DATE()).desc())
735                     .limit(1)
736                     .fetch();
737             if (records.isNotEmpty()) {
738                 final int thisDate = (int) records.getValue(0, DSL.fieldByName(DirectedMovementsFileReader.getMOVEMENT_DATE()));
739                 if (thisDate > locationDate) {
740                     locationDate = (int) records.getValue(0, DSL.fieldByName(DirectedMovementsFileReader.getMOVEMENT_DATE()));
741                     locationId = (String) records.getValue(0, DSL.fieldByName(DirectedMovementsFileReader.getLOCATION_ID()));
742                 }
743             }
744         } catch (org.jooq.exception.DataAccessException e) {
745             log.trace("Could not get movements from {} - this is not an error; the project might be configutred with one.",
746                       DirectedMovementsFileReader.getTABLE_NAME());
747         }
748 
749         if (locationDate > Integer.MIN_VALUE) {
750             return locationId;
751         } else {
752             // else no movement => it is still on it's location of birth.
753             final Animal animal = getAnimal(animalId);
754             if (animal != null) {
755                 return animal.getLocationOfBirth();
756             } else {
757                 log.error("Could not find location for {} at {}", animalId, date);
758             }
759         }
760         return null;
761     }
762 
763     /**
764      * Run a custom query against the database. This method is not intended to be used in general situations as it
765      * exposed the underlying jooq data structures but in some situations it may be used as a last resort.
766      * @param query the SQL query to be run.
767      * @return a Result set of records that were returned by the database.
768      */
769     public Result<Record> runCustomQuery(final String query) {
770         Result<Record> records = null;
771         try {
772             records = jooq.fetch(query);
773         } catch (DataAccessException e) {
774             log.error("Could not execute SQL {}. {}", query, e.getLocalizedMessage());
775         }
776         return records;
777     }
778 
779     /**
780      * Create a location object from the node object defining it in the graph database.
781      * @param locationRecord the record object from the database defining the location.
782      * @return the created location object.
783      */
784     private Location createLocation(final Record locationRecord) {
785         String id = "";
786         Double easting = null;
787         Double northing = null;
788 
789         try {
790             id = (String) locationRecord.getValue(LocationsFileReader.getID());
791         } catch (IllegalArgumentException | ArrayIndexOutOfBoundsException e) {
792             // ignore - the field was not in the record.
793         }
794         try {
795             easting = (Double) locationRecord.getValue(LocationsFileReader.getEASTING());
796         } catch (IllegalArgumentException | ArrayIndexOutOfBoundsException e) {
797             // ignore - the field was not in the record.
798         }
799         try {
800             northing = (Double) locationRecord.getValue(LocationsFileReader.getNORTHING());
801         } catch (IllegalArgumentException | ArrayIndexOutOfBoundsException e) {
802             // ignore - the field was not in the record.
803         }
804 
805         // TODO add custom tags
806 //        for (int i = 6; i < locationRecord.size(); i++) {
807 //            locationRecord.getValue(i);
808 //        }
809         final Map<String, Integer> populations = new HashMap<>();
810         log.trace("Creating location object for {}",
811                   String.format("%s %f,%f %s", id, easting, northing, populations));
812 
813         return new Location(id, easting, northing, populations);
814     }
815 
816     /**
817      * Create an animal object from the node object defining it in the graph database.
818      * @param animalRecord the record object from the database defining the animal.
819      * @return the created animal object.
820      */
821     private Animal createAnimal(final Record animalRecord) {
822 
823         String id = "";
824         Integer dob = null;
825         String lob = "";
826         Integer dod = null;
827         String lod = "";
828         String species = "";
829 
830         try {
831             id = (String) animalRecord.getValue(PopulationsFileReader.getID());
832         } catch (IllegalArgumentException | ArrayIndexOutOfBoundsException e) {
833             log.trace("error {}", e.getLocalizedMessage());
834             // ignore - the field was not in the record.
835         }
836         try {
837             dob = (Integer) animalRecord.getValue(PopulationsFileReader.getDATE_OF_BIRTH());
838         } catch (IllegalArgumentException | ArrayIndexOutOfBoundsException e) {
839             // ignore - the field was not in the record.
840         }
841         try {
842             lob = (String) animalRecord.getValue(PopulationsFileReader.getLOCATION_OF_BIRTH());
843         } catch (IllegalArgumentException | ArrayIndexOutOfBoundsException e) {
844             // ignore - the field was not in the record.
845         }
846         try {
847             dod = (Integer) animalRecord.getValue(PopulationsFileReader.getDATE_OF_DEATH());
848         } catch (IllegalArgumentException | ArrayIndexOutOfBoundsException e) {
849             // ignore - the field was not in the record.
850         }
851         try {
852             lod = (String) animalRecord.getValue(PopulationsFileReader.getLOCATION_OF_DEATH());
853         } catch (IllegalArgumentException | ArrayIndexOutOfBoundsException e) {
854             // ignore - the field was not in the record.
855         }
856         try {
857             species = (String) animalRecord.getValue(PopulationsFileReader.getSPECIES());
858         } catch (IllegalArgumentException | ArrayIndexOutOfBoundsException e) {
859             // ignore - the field was not in the record.
860         }
861 
862         // TODO add custom tags
863 //        for (int i = 6; i < animalRecord.size(); i++) {
864 //            animalRecord.getValue(i);
865 //        }
866         log.trace("Creating animal object for {}",
867                   String.format("%s (%s) dob:%d[%s] dod:%d[%s]", id, species, dob, lob, dod, lod));
868 
869         return new Animal(id, species, dob, lob, dod, lod);
870 
871     }
872 
873     /**
874      * Create a Test object from the node object defining it in the graph database.
875      * @param testRecord the record object from the database defining the test.
876      * @return the created test object.
877      */
878     private Test createTest(final Record testRecord) {
879 
880         String id = "";
881         String group = "";
882         String location = "";
883         Integer testDate = null;
884         Boolean positiveResult = null;
885         Boolean negativeResult = null;
886 
887         try {
888             id = (String) testRecord.getValue(TestsFileReader.getID());
889         } catch (IllegalArgumentException | ArrayIndexOutOfBoundsException e) {
890             // ignore - the field was not in the record.
891         }
892         try {
893             group = (String) testRecord.getValue(TestsFileReader.getGROUP_ID());
894         } catch (IllegalArgumentException | ArrayIndexOutOfBoundsException e) {
895             // ignore - the field was not in the record.
896         }
897         try {
898             location = (String) testRecord.getValue(TestsFileReader.getLOCATION_ID());
899         } catch (IllegalArgumentException | ArrayIndexOutOfBoundsException e) {
900             // ignore - the field was not in the record.
901         }
902         try {
903             testDate = (Integer) testRecord.getValue(TestsFileReader.getTEST_DATE());
904         } catch (IllegalArgumentException | ArrayIndexOutOfBoundsException e) {
905             // ignore - the field was not in the record.
906         }
907         try {
908             final Integer val = (Integer) testRecord.getValue(TestsFileReader.getPOSITIVE_RESULT());
909             if (val == 0) {
910                 positiveResult = Boolean.FALSE;
911             } else {
912                 positiveResult = Boolean.TRUE;
913             }
914         } catch (IllegalArgumentException | ArrayIndexOutOfBoundsException e) {
915             // ignore - the field was not in the record.
916         }
917         try {
918             final Integer val = (Integer) testRecord.getValue(TestsFileReader.getNEGATIVE_RESULT());
919             if (val == 0) {
920                 negativeResult = Boolean.FALSE;
921             } else {
922                 negativeResult = Boolean.TRUE;
923             }
924         } catch (IllegalArgumentException | ArrayIndexOutOfBoundsException e) {
925             // ignore - the field was not in the record.
926         }
927 
928         // TODO add custom tags
929 //        for (int i = 6; i < testRecord.size(); i++) {
930 //            testRecord.getValue(i);
931 //        }
932         log.trace("Creating test object for {}",
933                   String.format("%s group:%s location:%s date:%d pos:%s neg:%s", id, group, location, testDate, positiveResult, negativeResult));
934 
935         return new Test(id, group, location, testDate, positiveResult, negativeResult);
936     }
937 
938     /**
939      * Create a movement object from the relationship object defining it in the graph database.
940      * @param movementRecord the record object from the database defining the movement.
941      * @return the created movement object.
942      */
943     private Movement createMovement(final Record movementRecord) {
944 
945         String id = "";
946         Integer batchSize = null;
947         Integer departureDate = null;
948         String departureId = "";
949         Integer destinationDate = null;
950         String destinationId = "";
951         Integer marketDate = null;
952         String marketId = "";
953         String species = "";
954 
955         try {
956             id = (String) movementRecord.getValue(FullMovementsFileReader.getID());
957         } catch (IllegalArgumentException | ArrayIndexOutOfBoundsException e) {
958             // ignore - the field was not in the record.
959         }
960         try {
961             batchSize = (Integer) movementRecord.getValue(BatchedMovementsFileReader.getBATCH_SIZE());
962         } catch (IllegalArgumentException | ArrayIndexOutOfBoundsException e) {
963             // ignore - the field was not in the record.
964         }
965         try {
966             departureDate = (Integer) movementRecord.getValue(FullMovementsFileReader.getDEPARTURE_DATE());
967         } catch (IllegalArgumentException | ArrayIndexOutOfBoundsException e) {
968             // ignore - the field was not in the record.
969         }
970         try {
971             departureId = (String) movementRecord.getValue(FullMovementsFileReader.getDEPARTURE_ID());
972         } catch (IllegalArgumentException | ArrayIndexOutOfBoundsException e) {
973             // ignore - the field was not in the record.
974         }
975         try {
976             destinationDate = (Integer) movementRecord.getValue(FullMovementsFileReader.getDESTINATION_DATE());
977         } catch (IllegalArgumentException | ArrayIndexOutOfBoundsException e) {
978             // ignore - the field was not in the record.
979         }
980         try {
981             destinationId = (String) movementRecord.getValue(FullMovementsFileReader.getDESTINATION_ID());
982         } catch (IllegalArgumentException | ArrayIndexOutOfBoundsException e) {
983             // ignore - the field was not in the record.
984         }
985         try {
986             marketDate = (Integer) movementRecord.getValue(BatchedMovementsFileReader.getMARKET_DATE());
987         } catch (IllegalArgumentException | ArrayIndexOutOfBoundsException e) {
988             // ignore - the field was not in the record.
989         }
990         try {
991             marketId = (String) movementRecord.getValue(BatchedMovementsFileReader.getMARKET_ID());
992         } catch (IllegalArgumentException | ArrayIndexOutOfBoundsException e) {
993             // ignore - the field was not in the record.
994         }
995         try {
996             species = (String) movementRecord.getValue(DirectedMovementsFileReader.getSPECIES());
997         } catch (IllegalArgumentException | ArrayIndexOutOfBoundsException e) {
998             // ignore - the field was not in the record.
999         }
1000         // For directed movements, set the appropriate destination/departure id and dates.
1001         try {
1002             final String direction = (String) movementRecord.getValue(DirectedMovementsFileReader.getMOVEMENT_DIRECTION());
1003             if ("ON".equalsIgnoreCase(direction)) {
1004                 destinationId = (String) movementRecord.getValue(DirectedMovementsFileReader.getLOCATION_ID());
1005                 destinationDate = (Integer) movementRecord.getValue(DirectedMovementsFileReader.getMOVEMENT_DATE());
1006             } else {
1007                 departureId = (String) movementRecord.getValue(DirectedMovementsFileReader.getLOCATION_ID());
1008                 departureDate = (Integer) movementRecord.getValue(DirectedMovementsFileReader.getMOVEMENT_DATE());
1009             }
1010         } catch (IllegalArgumentException | ArrayIndexOutOfBoundsException e) {
1011             // ignore - the field was not in the record.
1012         }
1013 
1014         // TODO add custom tags
1015 //        for (int i = 6; i < testRecord.size(); i++) {
1016 //            testRecord.getValue(i);
1017 //        }
1018         log.trace("Creating movement object for {}",
1019                   String.format("%s batchSize:%d departureDate:%d departureId:%s destinationDate:%d destinationId:%s marketDate:%s marketId:%s species:%s",
1020                                 id, batchSize, departureDate, departureId, destinationDate, destinationId, marketDate, marketId, species));
1021 
1022         return new Movement(id, batchSize, departureDate, departureId, destinationDate, destinationId, marketDate, marketId, species);
1023     }
1024     Cache<String, Collection<Movement>> movementsCache = CacheBuilder.newBuilder().maximumSize(1000).build();
1025     Cache<String, Location> locationsCache = CacheBuilder.newBuilder().maximumSize(1000).build();
1026     Cache<String, Animal> animalsCache = CacheBuilder.newBuilder().maximumSize(1000).build();
1027     Cache<String, Test> testsCache = CacheBuilder.newBuilder().maximumSize(1000).build();
1028     private DSLContext jooq;
1029     @Getter
1030     @SuppressWarnings("PMD.UnusedPrivateField")
1031     private Connection connection;
1032 }
1033 
1034 /**
1035  * Implement a comparator for movements so that movements can be stored in ascending date order with OFF movements
1036  * appearing before ON movements in the movements cache.
1037  */
1038 class MovementsComparator implements Comparator<Movement>,Serializable {
1039 
1040     @Override
1041     public int compare(final Movement m1, final Movement m2) {
1042         // it's probably easiest to use the natural ordering determined by the toString() methods.
1043         // The departure information appears in the string before the destination information so we, in effect are 
1044         // ordering by departure date.
1045         return m1.toString().compareTo(m2.toString());
1046     }
1047 }