Coverage Report - de.jollyday.impl.DefaultHolidayManager
 
Classes in this File Line Coverage Branch Coverage Complexity
DefaultHolidayManager
89%
93/104
78%
41/52
3,308
DefaultHolidayManager$HolidayParserRunner
100%
8/8
N/A
3,308
 
 1  
 /**
 2  
  * Copyright 2010 Sven Diedrichsen 
 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 
 12  
  * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 
 13  
  * express or implied. See the License for the specific language 
 14  
  * governing permissions and limitations under the License. 
 15  
  */
 16  
 package de.jollyday.impl;
 17  
 
 18  
 import java.beans.Introspector;
 19  
 import java.beans.PropertyDescriptor;
 20  
 import java.time.LocalDate;
 21  
 import java.util.Collection;
 22  
 import java.util.Collections;
 23  
 import java.util.HashMap;
 24  
 import java.util.HashSet;
 25  
 import java.util.List;
 26  
 import java.util.Map;
 27  
 import java.util.Objects;
 28  
 import java.util.Set;
 29  
 import java.util.logging.Level;
 30  
 import java.util.logging.Logger;
 31  
 
 32  
 import de.jollyday.CalendarHierarchy;
 33  
 import de.jollyday.Holiday;
 34  
 import de.jollyday.HolidayManager;
 35  
 import de.jollyday.config.Configuration;
 36  
 import de.jollyday.config.Holidays;
 37  
 import de.jollyday.parser.HolidayParser;
 38  
 import de.jollyday.util.ClassLoadingUtil;
 39  
 
 40  
 /**
 41  
  * Manager implementation for reading data from the configuration datasource. 
 42  
  * It uses a list a parsers for parsing the different type of XML nodes.
 43  
  * 
 44  
  * @author Sven Diedrichsen
 45  
  */
 46  76
 public class DefaultHolidayManager extends HolidayManager {
 47  
 
 48  1
         private static final Logger LOG = Logger.getLogger(DefaultHolidayManager.class.getName());
 49  
         /**
 50  
          * The configuration prefix for parser implementations.
 51  
          */
 52  
         private static final String PARSER_IMPL_PREFIX = "parser.impl.";
 53  
         /**
 54  
          * Parser cache by XML class name.
 55  
          */
 56  76
         private final Map<String, HolidayParser> parserCache = new HashMap<>();
 57  
         /**
 58  
          * Configuration parsed on initialization.
 59  
          */
 60  
         protected Configuration configuration;
 61  
         /**
 62  
          * Utility class to handle class loading
 63  
          */
 64  76
         private ClassLoadingUtil classLoadingUtil = new ClassLoadingUtil();
 65  
 
 66  
         /**
 67  
          * {@inheritDoc}
 68  
          * 
 69  
          * Calls
 70  
          * <code>Set&lt;LocalDate&gt; getHolidays(int year, Configuration c, String... args)</code>
 71  
          * with the configuration from initialization.
 72  
          */
 73  
         @Override
 74  
         public Set<Holiday> getHolidays(int year, final String... args) {
 75  245
                 Set<Holiday> holidaySet = Collections.synchronizedSet(new HashSet<Holiday>());
 76  245
                 getHolidays(year, configuration, holidaySet, args);
 77  245
                 return holidaySet;
 78  
         }
 79  
 
 80  
         /**
 81  
          * {@inheritDoc}
 82  
          * 
 83  
          * Calls <code>getHolidays(year, args)</code> for each year within the
 84  
          * interval and returns a list of holidays which are then contained in the
 85  
          * interval.
 86  
          */
 87  
         @Override
 88  
         public Set<Holiday> getHolidays(LocalDate startDateInclusive, LocalDate endDateInclusive, final String... args) {
 89  2
                 Objects.requireNonNull(startDateInclusive, "startDateInclusive is null");
 90  2
                 Objects.requireNonNull(endDateInclusive, "endInclusive is null");
 91  2
                 Set<Holiday> holidays = new HashSet<>();
 92  5
                 for (int year = startDateInclusive.getYear(); year <= endDateInclusive.getYear(); year++) {
 93  3
                         Set<Holiday> yearHolidays = getHolidays(year, args);
 94  3
                         for (Holiday h : yearHolidays) {
 95  38
                                 if (!startDateInclusive.isAfter(h.getDate()) && !endDateInclusive.isBefore(h.getDate())) {
 96  5
                                         holidays.add(h);
 97  
                                 }
 98  38
                         }
 99  
                 }
 100  2
                 return holidays;
 101  
         }
 102  
 
 103  
         /**
 104  
          * Parses the provided configuration for the provided year and fills the
 105  
          * list of holidays.
 106  
          * 
 107  
          * @param year
 108  
          * @param c
 109  
          * @param holidaySet
 110  
          * @param args
 111  
          */
 112  
         private void getHolidays(int year, final Configuration c, Set<Holiday> holidaySet, final String... args) {
 113  485
                 if (LOG.isLoggable(Level.FINER)) {
 114  0
                         LOG.finer("Adding holidays for " + c.getDescription());
 115  
                 }
 116  485
                 parseHolidays(year, holidaySet, c.getHolidays());
 117  485
                 if (args != null && args.length > 0) {
 118  240
                         String hierarchy = args[0];
 119  240
                         for (Configuration config : c.getSubConfigurations()) {
 120  3668
                                 if (hierarchy.equalsIgnoreCase(config.getHierarchy())) {
 121  240
                                         getHolidays(year, config, holidaySet, copyOfRange(args, 1, args.length));
 122  240
                                         break;
 123  
                                 }
 124  3428
                         }
 125  
                 }
 126  485
         }
 127  
 
 128  
         /**
 129  
          * Copies the specified range from the original array to a new array. This
 130  
          * is a replacement for Java 1.6 Arrays.copyOfRange() specialized in String.
 131  
          * 
 132  
          * @param original
 133  
          *            the original array to copy range from
 134  
          * @param from
 135  
          *            the start of the range to copy from the original array
 136  
          * @param to
 137  
          *            the inclusive end of the range to copy from the original array
 138  
          * @return the copied range
 139  
          */
 140  
         private String[] copyOfRange(final String[] original, int from, int to) {
 141  240
                 int newLength = to - from;
 142  240
                 if (newLength < 0) {
 143  0
                         throw new IllegalArgumentException(from + " > " + to);
 144  
                 }
 145  240
                 String[] copy = new String[newLength];
 146  240
                 System.arraycopy(original, from, copy, 0, Math.min(original.length - from, newLength));
 147  240
                 return copy;
 148  
         }
 149  
 
 150  
         /**
 151  
          * Iterates of the list of parsers and calls parse on each of them.
 152  
          * 
 153  
          * @param year
 154  
          * @param holidays
 155  
          * @param config
 156  
          */
 157  
         private void parseHolidays(int year, Set<Holiday> holidays, final Holidays config) {
 158  485
                 Collection<HolidayParser> parsers = getParsers(config);
 159  485
                 for (HolidayParser p : parsers) {
 160  776
                         HolidayParserRunner holidayParserRunner = new HolidayParserRunner(year, holidays, config, p);
 161  776
                         holidayParserRunner.run();
 162  776
                 }
 163  485
         }
 164  
 
 165  
         /**
 166  
          * Private class which is used to asyncronisly parse holiday configuration.
 167  
          * 
 168  
          * @author Sven
 169  
          * 
 170  
          */
 171  76
         private static class HolidayParserRunner implements Runnable {
 172  
 
 173  
                 private final int year;
 174  
                 private final Set<Holiday> holidays;
 175  
                 private final Holidays config;
 176  
                 private final HolidayParser parser;
 177  
 
 178  776
                 public HolidayParserRunner(int year, Set<Holiday> holidays, final Holidays config, HolidayParser parser) {
 179  776
                         this.year = year;
 180  776
                         this.holidays = holidays;
 181  776
                         this.config = config;
 182  776
                         this.parser = parser;
 183  776
                 }
 184  
 
 185  
                 @Override
 186  
                 public void run() {
 187  776
                         parser.parse(year, holidays, config);
 188  776
                 }
 189  
 
 190  
         }
 191  
 
 192  
         /**
 193  
          * Creates a list of parsers by reading the configuration and trying to find
 194  
          * an <code>HolidayParser</code> implementation for by XML class type.
 195  
          * 
 196  
          * @param config
 197  
          * @return A list of parsers to for this configuration.
 198  
          */
 199  
         private Collection<HolidayParser> getParsers(final Holidays config) {
 200  485
                 Collection<HolidayParser> parsers = new HashSet<>();
 201  
                 try {
 202  485
                         PropertyDescriptor[] propertiesDescs = Introspector.getBeanInfo(config.getClass()).getPropertyDescriptors();
 203  6790
                         for (PropertyDescriptor propertyDescriptor : propertiesDescs) {
 204  6305
                                 if (List.class.isAssignableFrom(propertyDescriptor.getPropertyType())) {
 205  5820
                                         List<?> l = (List<?>) propertyDescriptor.getReadMethod().invoke(config);
 206  5820
                                         if (!l.isEmpty()) {
 207  776
                                                 String className = l.get(0).getClass().getName();
 208  776
                                                 if (!parserCache.containsKey(className)) {
 209  47
                                                         String propName = PARSER_IMPL_PREFIX + className;
 210  47
                                                         String parserClassName = getManagerParameter().getProperty(propName);
 211  47
                                                         if (parserClassName != null) {
 212  47
                                                                 Class<?> parserClass = classLoadingUtil.loadClass(parserClassName);
 213  47
                                                                 Object parserObject = parserClass.newInstance();
 214  47
                                                                 HolidayParser hp = HolidayParser.class.cast(parserObject);
 215  47
                                                                 parserCache.put(className, hp);
 216  
                                                         }
 217  
                                                 }
 218  776
                                                 if (parserCache.containsKey(className)) {
 219  776
                                                         parsers.add(parserCache.get(className));
 220  
                                                 }
 221  
                                         }
 222  
                                 }
 223  
                         }
 224  0
                 } catch (Exception e) {
 225  0
                         throw new IllegalStateException("Cannot create parsers.", e);
 226  485
                 }
 227  485
                 return parsers;
 228  
         }
 229  
 
 230  
         /**
 231  
          * {@inheritDoc}
 232  
          * 
 233  
          * Initializes the DefaultHolidayManager by loading the holidays XML file as resource
 234  
          * from the classpath. When the XML file is found it will be unmarshalled
 235  
          * with JAXB to some Java classes.
 236  
          */
 237  
         @Override
 238  
         public void doInit() {
 239  76
                 configuration = getConfigurationDataSource().getConfiguration(getManagerParameter());
 240  75
                 validateConfigurationHierarchy(configuration);
 241  74
                 logHierarchy(configuration, 0);
 242  74
         }
 243  
 
 244  
         /**
 245  
          * Logs the hierarchy structure.
 246  
          * 
 247  
          * @param c
 248  
          *            Configuration to log hierarchy for.
 249  
          * @param level
 250  
          *            a int.
 251  
          */
 252  
         protected static void logHierarchy(final Configuration c, int level) {
 253  74
                 if (LOG.isLoggable(Level.FINER)) {
 254  0
                         StringBuilder space = new StringBuilder();
 255  0
                         for (int i = 0; i < level; i++) {
 256  0
                                 space.append("-");
 257  
                         }
 258  0
                         LOG.finer(space + " " + c.getDescription() + "(" + c.getHierarchy() + ").");
 259  0
                         for (Configuration sub : c.getSubConfigurations()) {
 260  0
                                 logHierarchy(sub, level + 1);
 261  0
                         }
 262  
                 }
 263  74
         }
 264  
 
 265  
         /**
 266  
          * Validates the content of the provided configuration by checking for
 267  
          * multiple hierarchy entries within one configuration. It traverses down
 268  
          * the configuration tree.
 269  
          * 
 270  
          * @param c
 271  
          *            a {@link de.jollyday.config.Configuration} object.
 272  
          */
 273  
         protected static void validateConfigurationHierarchy(final Configuration c) {
 274  678
                 Map<String, Integer> hierarchyMap = new HashMap<>();
 275  678
                 Set<String> multipleHierarchies = new HashSet<>();
 276  678
                 for (Configuration subConfig : c.getSubConfigurations()) {
 277  605
                         String hierarchy = subConfig.getHierarchy();
 278  605
                         if (!hierarchyMap.containsKey(hierarchy)) {
 279  604
                                 hierarchyMap.put(hierarchy, Integer.valueOf(1));
 280  
                         } else {
 281  1
                                 int count = hierarchyMap.get(hierarchy).intValue();
 282  1
                                 hierarchyMap.put(hierarchy, Integer.valueOf(++count));
 283  1
                                 multipleHierarchies.add(hierarchy);
 284  
                         }
 285  605
                 }
 286  678
                 if (multipleHierarchies.size() > 0) {
 287  1
                         StringBuilder msg = new StringBuilder();
 288  1
                         msg.append("Configuration for " + c.getHierarchy()
 289  
                                         + " contains  multiple SubConfigurations with the same hierarchy id. ");
 290  1
                         for (String hierarchy : multipleHierarchies) {
 291  1
                                 msg.append(hierarchy + " " + hierarchyMap.get(hierarchy).toString() + " times ");
 292  1
                         }
 293  1
                         throw new IllegalArgumentException(msg.toString().trim());
 294  
                 }
 295  677
                 for (Configuration subConfig : c.getSubConfigurations()) {
 296  603
                         validateConfigurationHierarchy(subConfig);
 297  603
                 }
 298  677
         }
 299  
 
 300  
         /**
 301  
          * {@inheritDoc}
 302  
          * 
 303  
          * Returns the configurations hierarchy.<br>
 304  
          * i.e. Hierarchy 'us' -&gt; Children 'al','ak','ar', ... ,'wv','wy'. Every
 305  
          * child might itself have children. The ids be used to call
 306  
          * getHolidays()/isHoliday().
 307  
          */
 308  
         @Override
 309  
         public CalendarHierarchy getCalendarHierarchy() {
 310  92
                 return createConfigurationHierarchy(configuration, null);
 311  
         }
 312  
 
 313  
         /**
 314  
          * Creates the configuration hierarchy for the provided configuration.
 315  
          * 
 316  
          * @param c
 317  
          * @return configuration hierarchy
 318  
          */
 319  
         private static CalendarHierarchy createConfigurationHierarchy(final Configuration c, CalendarHierarchy h) {
 320  909
                 h = new CalendarHierarchy(h, c.getHierarchy());
 321  909
                 h.setFallbackDescription(c.getDescription());
 322  909
                 for (Configuration sub : c.getSubConfigurations()) {
 323  817
                         CalendarHierarchy subHierarchy = createConfigurationHierarchy(sub, h);
 324  817
                         h.getChildren().put(subHierarchy.getId(), subHierarchy);
 325  817
                 }
 326  909
                 return h;
 327  
         }
 328  
 
 329  
 }