/**
* Copyright 2010 Sven Diedrichsen
*
* 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 de.jollyday.impl;
import de.jollyday.CalendarHierarchy;
import de.jollyday.Holiday;
import de.jollyday.HolidayManager;
import de.jollyday.config.Configuration;
import de.jollyday.config.Holidays;
import de.jollyday.parser.HolidayParser;
import de.jollyday.util.ClassLoadingUtil;
import java.beans.Introspector;
import java.beans.PropertyDescriptor;
import java.time.LocalDate;
import java.util.*;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* Manager implementation for reading data from the configuration datasource.
* It uses a list a parsers for parsing the different type of XML nodes.
*
* @author Sven Diedrichsen
*/
public class DefaultHolidayManager extends HolidayManager {
private static final Logger LOG = Logger.getLogger(DefaultHolidayManager.class.getName());
/**
* The configuration prefix for parser implementations.
*/
private static final String PARSER_IMPL_PREFIX = "parser.impl.";
/**
* Parser cache by XML class name.
*/
private final Map<String, HolidayParser> parserCache = new HashMap<>();
/**
* Configuration parsed on initialization.
*/
protected Configuration configuration;
/**
* Utility class to handle class loading
*/
private final ClassLoadingUtil classLoadingUtil = new ClassLoadingUtil();
/**
* {@inheritDoc}
*
* Calls
* <code>Set<LocalDate> getHolidays(int year, Configuration c, String... args)</code>
* with the configuration from initialization.
*/
@Override
public Set<Holiday> getHolidays(int year, final String... args) {
Set<Holiday> holidaySet = Collections.synchronizedSet(new HashSet<>());
getHolidays(year, configuration, holidaySet, args);
return holidaySet;
}
/**
* {@inheritDoc}
*
* Calls <code>getHolidays(year, args)</code> for each year within the
* interval and returns a list of holidays which are then contained in the
* interval.
*/
@Override
public Set<Holiday> getHolidays(LocalDate startDateInclusive, LocalDate endDateInclusive, final String... args) {
Objects.requireNonNull(startDateInclusive, "startDateInclusive is null");
Objects.requireNonNull(endDateInclusive, "endInclusive is null");
Set<Holiday> holidays = new HashSet<>();
for (int year = startDateInclusive.getYear(); year <= endDateInclusive.getYear(); year++) {
Set<Holiday> yearHolidays = getHolidays(year, args);
for (Holiday h : yearHolidays) {
if (!startDateInclusive.isAfter(h.getDate()) && !endDateInclusive.isBefore(h.getDate())) {
holidays.add(h);
}
}
}
return holidays;
}
/**
* Parses the provided configuration for the provided year and fills the
* list of holidays.
*
* @param year the year to get the holidays for
* @param c the holiday configuration
* @param holidaySet the set of holidays
* @param args the arguments to descend down the configuration tree
*/
private void getHolidays(int year, final Configuration c, Set<Holiday> holidaySet, final String... args) {
if (LOG.isLoggable(Level.FINER)) {
LOG.finer("Adding holidays for " + c.getDescription());
}
parseHolidays(year, holidaySet, c.getHolidays());
if (args != null && args.length > 0) {
String hierarchy = args[0];
for (Configuration config : c.getSubConfigurations()) {
if (hierarchy.equalsIgnoreCase(config.getHierarchy())) {
getHolidays(year, config, holidaySet, copyOfRange(args, 1, args.length));
break;
}
}
}
}
/**
* Copies the specified range from the original array to a new array. This
* is a replacement for Java 1.6 Arrays.copyOfRange() specialized in String.
*
* @param original
* the original array to copy range from
* @param from
* the start of the range to copy from the original array
* @param to
* the inclusive end of the range to copy from the original array
* @return the copied range
*/
private String[] copyOfRange(final String[] original, int from, int to) {
int newLength = to - from;
if (newLength < 0) {
throw new IllegalArgumentException(from + " > " + to);
}
String[] copy = new String[newLength];
System.arraycopy(original, from, copy, 0, Math.min(original.length - from, newLength));
return copy;
}
/**
* Iterates of the list of parsers and calls parse on each of them.
*
* @param year the year to parse the holidays for
* @param holidays the set to put the holidays into
* @param config the holiday configuration
*/
private void parseHolidays(int year, Set<Holiday> holidays, final Holidays config) {
Collection<HolidayParser> parsers = getParsers(config);
for (HolidayParser p : parsers) {
p.parse(year, holidays, config);
}
}
/**
* Creates a list of parsers by reading the configuration and trying to find
* an <code>HolidayParser</code> implementation for by XML class type.
*
* @param config the holiday configuration
* @return A list of parsers to for this configuration.
*/
private Collection<HolidayParser> getParsers(final Holidays config) {
Collection<HolidayParser> parsers = new HashSet<>();
try {
PropertyDescriptor[] propertiesDescs = Introspector.getBeanInfo(config.getClass()).getPropertyDescriptors();
for (PropertyDescriptor propertyDescriptor : propertiesDescs) {
if (List.class.isAssignableFrom(propertyDescriptor.getPropertyType())) {
List<?> l = (List<?>) propertyDescriptor.getReadMethod().invoke(config);
if (!l.isEmpty()) {
String className = l.get(0).getClass().getName();
if (!parserCache.containsKey(className)) {
String propName = PARSER_IMPL_PREFIX + className;
String parserClassName = getManagerParameter().getProperty(propName);
if (parserClassName != null) {
Class<?> parserClass = classLoadingUtil.loadClass(parserClassName);
Object parserObject = parserClass.newInstance();
HolidayParser hp = HolidayParser.class.cast(parserObject);
parserCache.put(className, hp);
}
}
if (parserCache.containsKey(className)) {
parsers.add(parserCache.get(className));
}
}
}
}
} catch (Exception e) {
throw new IllegalStateException("Cannot create parsers.", e);
}
return parsers;
}
/**
* {@inheritDoc}
*
* Initializes the DefaultHolidayManager by loading the holidays XML file as resource
* from the classpath. When the XML file is found it will be unmarshalled
* with JAXB to some Java classes.
*/
@Override
public void doInit() {
configuration = getConfigurationDataSource().getConfiguration(getManagerParameter());
validateConfigurationHierarchy(configuration);
logHierarchy(configuration, 0);
}
/**
* Logs the hierarchy structure.
*
* @param c
* Configuration to log hierarchy for.
* @param level
* a int.
*/
protected static void logHierarchy(final Configuration c, int level) {
if (LOG.isLoggable(Level.FINER)) {
StringBuilder space = new StringBuilder();
for (int i = 0; i < level; i++) {
space.append("-");
}
LOG.finer(space + " " + c.getDescription() + "(" + c.getHierarchy() + ").");
for (Configuration sub : c.getSubConfigurations()) {
logHierarchy(sub, level + 1);
}
}
}
/**
* Validates the content of the provided configuration by checking for
* multiple hierarchy entries within one configuration. It traverses down
* the configuration tree.
*
* @param c a {@link de.jollyday.config.Configuration} object.
*/
protected static void validateConfigurationHierarchy(final Configuration c) {
Map<String, Integer> hierarchyMap = new HashMap<>();
Set<String> multipleHierarchies = new HashSet<>();
for (Configuration subConfig : c.getSubConfigurations()) {
String hierarchy = subConfig.getHierarchy();
if (!hierarchyMap.containsKey(hierarchy)) {
hierarchyMap.put(hierarchy, 1);
} else {
int count = hierarchyMap.get(hierarchy);
hierarchyMap.put(hierarchy, ++count);
multipleHierarchies.add(hierarchy);
}
}
if (multipleHierarchies.size() > 0) {
StringBuilder msg = new StringBuilder();
msg.append("Configuration for ").append(c.getHierarchy())
.append(" contains multiple SubConfigurations with the same hierarchy id. ");
for (String hierarchy : multipleHierarchies) {
msg.append(hierarchy).append(" ").append(hierarchyMap.get(hierarchy)
.toString()).append(" times ");
}
throw new IllegalArgumentException(msg.toString().trim());
}
for (Configuration subConfig : c.getSubConfigurations()) {
validateConfigurationHierarchy(subConfig);
}
}
/**
* {@inheritDoc}
*
* Returns the configurations hierarchy.<br>
* i.e. Hierarchy 'us' -> Children 'al','ak','ar', ... ,'wv','wy'. Every
* child might itself have children. The ids be used to call
* getHolidays()/isHoliday().
*/
@Override
public CalendarHierarchy getCalendarHierarchy() {
return createConfigurationHierarchy(configuration, null);
}
/**
* Creates the configuration hierarchy for the provided configuration.
*
* @param c the full configuration
* @param h the calendars hierarchy
* @return configuration hierarchy
*/
private static CalendarHierarchy createConfigurationHierarchy(final Configuration c, CalendarHierarchy h) {
h = new CalendarHierarchy(h, c.getHierarchy());
h.setFallbackDescription(c.getDescription());
for (Configuration sub : c.getSubConfigurations()) {
CalendarHierarchy subHierarchy = createConfigurationHierarchy(sub, h);
h.getChildren().put(subHierarchy.getId(), subHierarchy);
}
return h;
}
}