Skip to content

Commit

Permalink
Support for contextual zone definitions (local and global)
Browse files Browse the repository at this point in the history
  • Loading branch information
benfortuna committed Jun 22, 2019
1 parent 3d796b6 commit 7dca855
Show file tree
Hide file tree
Showing 17 changed files with 917 additions and 132 deletions.
75 changes: 6 additions & 69 deletions src/main/java/net/fortuna/ical4j/data/DefaultContentHandler.java
@@ -1,16 +1,15 @@
package net.fortuna.ical4j.data;

import net.fortuna.ical4j.model.*;
import net.fortuna.ical4j.model.component.*;
import net.fortuna.ical4j.model.component.CalendarComponent;
import net.fortuna.ical4j.model.component.VTimeZone;
import net.fortuna.ical4j.model.parameter.TzId;
import net.fortuna.ical4j.model.property.DateListProperty;
import net.fortuna.ical4j.model.property.DateProperty;
import net.fortuna.ical4j.util.Constants;

import java.io.IOException;
import java.net.URISyntaxException;
import java.text.ParseException;
import java.util.ArrayList;
import java.time.zone.ZoneRulesProvider;
import java.util.List;
import java.util.function.Consumer;
import java.util.function.Supplier;
Expand All @@ -25,8 +24,6 @@ public class DefaultContentHandler implements ContentHandler {

private final TimeZoneRegistry tzRegistry;

private List<TzId> propertiesWithTzId;

private final Consumer<Calendar> consumer;

private PropertyBuilder propertyBuilder;
Expand Down Expand Up @@ -57,30 +54,11 @@ public DefaultContentHandler(Consumer<Calendar> consumer, TimeZoneRegistry tzReg
@Override
public void startCalendar() {
calendar = new Calendar();
propertiesWithTzId = new ArrayList<>();
}

@Override
public void endCalendar() throws IOException {
if (propertiesWithTzId.size() > 0 && tzRegistry != null) {
for (CalendarComponent component : calendar.getComponents()) {
resolveTimezones(component.getProperties());

if (component instanceof VAvailability) {
for (Component available : ((VAvailability) component).getAvailable()) {
resolveTimezones(available.getProperties());
}
} else if (component instanceof VEvent) {
for (Component alarm : ((VEvent) component).getAlarms()) {
resolveTimezones(alarm.getProperties());
}
} else if (component instanceof VToDo) {
for (Component todo : ((VToDo) component).getAlarms()) {
resolveTimezones(todo.getProperties());
}
}
}
}
public void endCalendar() {
ZoneRulesProvider.registerProvider(new ZoneRulesProviderImpl(tzRegistry));
consumer.accept(calendar);
}

Expand Down Expand Up @@ -142,8 +120,6 @@ public void endProperty(String name) throws URISyntaxException, ParseException,
} else if (calendar != null) {
calendar.getProperties().add(property);
}

property = null;
}

@Override
Expand All @@ -157,7 +133,7 @@ public void parameter(String name, String value) throws URISyntaxException {
// VTIMEZONE may be defined later, so so keep
// track of dates until all components have been
// parsed, and then try again later
propertiesWithTzId.add((TzId) parameter);
((TzId) parameter).setTimeZoneRegistry(tzRegistry);
}

propertyBuilder.parameter(parameter);
Expand All @@ -174,43 +150,4 @@ private void assertProperty(PropertyBuilder property) {
throw new CalendarException("Expected property not initialised");
}
}

private void resolveTimezones(List<Property> properties) throws IOException {

// Go through each property and try to resolve the TZID.
for (TzId tzParam : propertiesWithTzId) {

//lookup timezone
final TimeZone timezone = tzRegistry.getTimeZone(tzParam.getValue());

// If timezone found, then update date property
if (timezone != null) {
for (Property property : properties) {
if (tzParam.equals(property.getParameter(Parameter.TZID))) {

// Get the String representation of date(s) as
// we will need this after changing the timezone
final String strDate = property.getValue();

// Change the timezone
if (property instanceof DateProperty) {
((DateProperty) property).setTimeZone(timezone);
} else if (property instanceof DateListProperty) {
((DateListProperty) property).setTimeZone(timezone);
} else {
throw new CalendarException("Invalid parameter: " + tzParam.getName());
}

// Reset value
try {
property.setValue(strDate);
} catch (ParseException | URISyntaxException e) {
// shouldn't happen as its already been parsed
throw new CalendarException(e);
}
}
}
}
}
}
}
@@ -0,0 +1,80 @@
package net.fortuna.ical4j.model;

import net.fortuna.ical4j.data.ParserException;
import net.fortuna.ical4j.model.component.VTimeZone;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.IOException;
import java.text.ParseException;
import java.time.zone.ZoneRules;
import java.time.zone.ZoneRulesProvider;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;

/**
* A default {@link ZoneRulesProvider} implementation for included timezone definitions. To avoid conflicting with
* the standard Java zone rules this provider maintains an internal map of local zone ids to globally unique ids.
*
* NOTE: Globally unique zone identifiers are transient and will be regenerated for each instance of this class. They
* are only used to support registration and use of alternative definitions in the scope of this library.
*/
public class DefaultZoneRulesProvider extends ZoneRulesProvider {

private static final Logger LOG = LoggerFactory.getLogger(DefaultZoneRulesProvider.class);

private static final String DEFAULT_RESOURCE_PREFIX = "zoneinfo/";

private final TimeZoneLoader zoneLoader;

private final Map<String, ZoneRules> zoneRulesMap;

public DefaultZoneRulesProvider() {
this(new TimeZoneLoader(DEFAULT_RESOURCE_PREFIX));
}

public DefaultZoneRulesProvider(TimeZoneLoader timeZoneLoader) {
this.zoneLoader = timeZoneLoader;
for (String id : zoneLoader.getAvailableIDs()) {
TimeZoneRegistry.ZONE_IDS.put("ical4j~" + UUID.randomUUID().toString(), id);
}
this.zoneRulesMap = new ConcurrentHashMap<>();
}

@Override
protected Set<String> provideZoneIds() {
return TimeZoneRegistry.ZONE_IDS.keySet();
}

@Override
protected ZoneRules provideRules(String zoneId, boolean forCaching) {
ZoneRules retVal = null;
if (zoneRulesMap.containsKey(zoneId)) {
retVal = zoneRulesMap.get(zoneId);
} else {
try {
String localZoneId = TimeZoneRegistry.ZONE_IDS.get(zoneId);
VTimeZone vTimeZone = zoneLoader.loadVTimeZone(localZoneId);
retVal = new ZoneRulesBuilder().vTimeZone(vTimeZone).build();
zoneRulesMap.put(zoneId, retVal);
} catch (IOException | ParserException | ParseException e) {
LOG.error("Error loading zone rules", e);
}
}
return retVal;
}

@Override
protected NavigableMap<String, ZoneRules> provideVersions(String zoneId) {
NavigableMap<String, ZoneRules> retVal = new TreeMap<>();
if (zoneRulesMap.containsKey(zoneId)) {
retVal.put(zoneId, zoneRulesMap.get(zoneId));
}
return retVal;
}

@Override
protected boolean provideRefresh() {
return super.provideRefresh();
}
}
3 changes: 2 additions & 1 deletion src/main/java/net/fortuna/ical4j/model/TimeZone.java
Expand Up @@ -89,7 +89,8 @@ public final int getOffset(final int era, final int year, final int month, final
final int second = ms / 1000;
ms -= second * 1000;

OffsetDateTime date = OffsetDateTime.of(year, month, dayOfMonth, hour, minute, second, ms * 1000, ZoneOffset.UTC);
// convert zero-based month of old API to new API by adding 1..
OffsetDateTime date = OffsetDateTime.of(year, month + 1, dayOfMonth, hour, minute, second, ms * 1000, ZoneOffset.ofTotalSeconds(getRawOffset() / 1000));
final Observance observance = vTimeZone.getApplicableObservance(date);
if (observance != null) {
final TzOffsetTo offset = observance.getProperty(Property.TZOFFSETTO);
Expand Down
13 changes: 9 additions & 4 deletions src/main/java/net/fortuna/ical4j/model/TimeZoneLoader.java
Expand Up @@ -13,8 +13,10 @@
import org.apache.commons.lang3.Validate;
import org.slf4j.LoggerFactory;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.InetSocketAddress;
import java.net.Proxy;
import java.net.URL;
Expand Down Expand Up @@ -46,14 +48,11 @@ public class TimeZoneLoader {
private static final String MESSAGE_MISSING_DEFAULT_TZ_CACHE_IMPL = "Error loading default cache implementation. Please ensure the JCache API dependency is included in the classpath, or override the cache implementation (e.g. via configuration: net.fortuna.ical4j.timezone.cache.impl=net.fortuna.ical4j.util.MapTimeZoneCache)";

private static Proxy proxy = null;
private static final Set<String> TIMEZONE_DEFINITIONS = new HashSet<String>();
private static final String DATE_TIME_TPL = "yyyyMMdd'T'HHmmss";
private static final String RRULE_TPL = "FREQ=YEARLY;BYMONTH=%d;BYDAY=%d%s";
private static final Standard NO_TRANSITIONS;

static {
TIMEZONE_DEFINITIONS.addAll(Arrays.asList(net.fortuna.ical4j.model.TimeZone.getAvailableIDs()));

NO_TRANSITIONS = new Standard();
TzOffsetFrom offsetFrom = new TzOffsetFrom(ZoneOffset.UTC);
TzOffsetTo offsetTo = new TzOffsetTo(ZoneOffset.UTC);
Expand Down Expand Up @@ -90,6 +89,12 @@ public TimeZoneLoader(String resourcePrefix, TimeZoneCache cache) {
this.cache = cache;
}

public String[] getAvailableIDs() {
return new BufferedReader(new InputStreamReader(
ResourceLoader.getResourceAsStream("net/fortuna/ical4j/model/tz.availableIds")))
.lines().toArray(String[]::new);
}

/**
* Loads an existing VTimeZone from the classpath corresponding to the specified Java timezone.
*
Expand Down Expand Up @@ -153,7 +158,7 @@ private VTimeZone updateDefinition(VTimeZone vTimeZone) throws IOException, Pars
}

private static VTimeZone generateTimezoneForId(String timezoneId) throws ParseException {
if (!TIMEZONE_DEFINITIONS.contains(timezoneId)) {
if (!ZoneId.getAvailableZoneIds().contains(timezoneId)) {
return null;
}
TimeZone javaTz = TimeZone.getTimeZone(timezoneId);
Expand Down
28 changes: 28 additions & 0 deletions src/main/java/net/fortuna/ical4j/model/TimeZoneRegistry.java
Expand Up @@ -31,6 +31,16 @@
*/
package net.fortuna.ical4j.model;

import java.time.DateTimeException;
import java.time.ZoneId;
import java.time.zone.ZoneRules;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;

import static java.time.ZoneId.getAvailableZoneIds;
import static java.time.ZoneId.of;

/**
* $Id$
*
Expand All @@ -42,6 +52,18 @@
*/
public interface TimeZoneRegistry {

Map<String, String> ZONE_IDS = new ConcurrentHashMap<>();

Map<String, String> ZONE_ALIASES = new ConcurrentHashMap<>();

static ZoneId getGlobalZoneId(String tzId) {
// Ensure zone rules are loaded..
Set<String> ids = getAvailableZoneIds();
return of(ZONE_IDS.entrySet().stream().filter(entry -> entry.getValue().equals(tzId))
.findFirst().orElseThrow(() -> new DateTimeException(String.format("Unknown timezone identifier [%s]", tzId))).getKey(),
ZONE_ALIASES);
}

/**
* Registers a new timezone for use with iCalendar objects. If a timezone
* with the same identifier is already registered this timezone will take
Expand Down Expand Up @@ -74,4 +96,10 @@ public interface TimeZoneRegistry {
* is registered with the specified identifier null is returned.
*/
TimeZone getTimeZone(final String id);

Map<String, ZoneRules> getZoneRules();

ZoneId getZoneId(String tzId);

String getTzId(String zoneId);
}

0 comments on commit 7dca855

Please sign in to comment.