/* * � Copyright IBM Corp. 2012 * * 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 com.ibm.domino.services.calendar.json; import static com.ibm.domino.services.calendar.json.JsonConstants.JSON_EVENTS; import static com.ibm.domino.services.calendar.json.JsonConstants.JSON_HREF; import static com.ibm.domino.services.calendar.json.JsonConstants.JSON_PRODUCT_ID; import static com.ibm.domino.services.calendar.json.JsonConstants.JSON_SCHEDULE_METHOD; import static com.ibm.domino.services.calendar.json.JsonConstants.JSON_TIMEZONES; import static com.ibm.domino.services.calendar.service.CalendarService.CALENDAR_SERVICE_LOGGER; import static net.fortuna.ical4j.model.Parameter.TZID; import static net.fortuna.ical4j.model.Property.DTEND; import static net.fortuna.ical4j.model.Property.DTSTART; import static net.fortuna.ical4j.model.Property.METHOD; import static net.fortuna.ical4j.model.Property.PRODID; import java.io.IOException; import java.net.URI; import java.net.URISyntaxException; import java.text.ParseException; import java.util.HashMap; import java.util.Map; import java.util.Stack; import net.fortuna.ical4j.data.ContentHandler; import net.fortuna.ical4j.model.Component; import net.fortuna.ical4j.model.ComponentFactory; import net.fortuna.ical4j.model.Parameter; import net.fortuna.ical4j.model.ParameterFactoryImpl; import net.fortuna.ical4j.model.ParameterList; import net.fortuna.ical4j.model.Property; import net.fortuna.ical4j.model.PropertyFactoryImpl; import net.fortuna.ical4j.model.TimeZone; import net.fortuna.ical4j.model.component.VAlarm; import net.fortuna.ical4j.model.component.VEvent; import net.fortuna.ical4j.model.component.VTimeZone; import net.fortuna.ical4j.model.property.DtEnd; import net.fortuna.ical4j.model.property.DtStart; import net.fortuna.ical4j.model.property.RecurrenceId; import com.ibm.commons.util.io.json.JsonException; import com.ibm.commons.util.io.json.JsonGenerator.Generator; import com.ibm.commons.util.io.json.JsonGenerator.StringBuilderGenerator; import com.ibm.commons.util.io.json.JsonJavaFactory; import com.ibm.domino.commons.util.UriHelper; /** * As events are parsed from iCalendar format, this class generates JSON content. * * TODO: Revisit this for poorly formed iCalendar. There are some cases where poorly * formed iCalendar will result in malformed JSON. For example, when generating the * JSON for a notice, we can leave a trailing comma after the noticeType property. * This doesn't seem to be an issue for well formed iCalendar, so I'm leaving it for * now. */ public class JsonCalendarGenerator implements ContentHandler { // These fields are initialized by the constructor private URI _baseUrl; private boolean _isNotice; // These fields hold the current state while parsing private int _eventCount = 0; private int _timeZoneCount = 0; private boolean _startComponment = false; private int processState = 0; private Map<String, TimeZone> _timeZones = new HashMap<String, TimeZone>(); private Stack<Component> _componentStack = new Stack<Component>(); private ParameterList _parameters = null; private String _propertyValue = null; private Generator _generator = null; /** * Constructs a new JsonCalendarGenerator. * * @param sb Destination for JSON content. * @param baseUrl Base URL for all events to be parsed. When this is a notice, specify the * URL for the notice. * @param isNotice <code>true</code> when this is a notice. */ public JsonCalendarGenerator(StringBuilder sb, URI baseUrl, boolean isNotice) { _baseUrl = baseUrl; _isNotice = isNotice; _generator = new StringBuilderGenerator(JsonJavaFactory.instanceEx, sb, false); } public void startCalendar() { try { _generator.out("{"); _generator.nl(); _generator.incIndent(); if ( _isNotice && _baseUrl != null ) { _generator.indent(); _generator.outPropertyName(JSON_HREF); _generator.out(":"); _generator.outStringLiteral(_baseUrl.toString()); _generator.out(","); _generator.nl(); } } catch(IOException e) { throw new IllegalArgumentException(e); } } public void endCalendar() { try { if ( _eventCount > 0 || _timeZoneCount > 0 ) { _generator.nl(); _generator.decIndent(); _generator.indent(); _generator.out("]"); _generator.nl(); _generator.decIndent(); } _generator.out("}"); } catch(IOException e) { throw new IllegalArgumentException(e); } } public void startComponent(String componentName) { // Create a component and push it on the stack Component component = ComponentFactory.getInstance().createComponent(componentName); _componentStack.push(component); _startComponment = true; } public void endComponent(String componentName) { try { // Pop the current component off the stack Component component = _componentStack.pop(); if ( _componentStack.empty() ) { // The stack is empty. Write out this component. if ( component instanceof VEvent ) { if ( _eventCount == 0 ) { if ( _timeZoneCount > 0 ) { // Close the time zone array _generator.nl(); _generator.decIndent(); _generator.indent(); _generator.out("],"); _generator.nl(); } // Start the event array _generator.indent(); _generator.outPropertyName(JSON_EVENTS); _generator.out(": ["); _generator.nl(); _generator.incIndent(); } else { // Add a new event to the array _generator.out(","); _generator.nl(); } VEvent event = (VEvent)component; URI eventURI = null; if ( !_isNotice ) { eventURI = UriHelper.appendPathSegment(_baseUrl, event.getUid().getValue()); RecurrenceId recurrenceId = event.getRecurrenceId(); if ( recurrenceId != null ) { eventURI = UriHelper.appendPathSegment(eventURI, recurrenceId.getValue()); } } JsonEventAdapter eventAdapter = new JsonEventAdapter(event, eventURI); _generator.toJson(eventAdapter); _eventCount++; } else if ( component instanceof VTimeZone ) { if ( _eventCount > 0 ) { // If we have already started processing events, // ignore this time zone. return; } if ( _timeZoneCount == 0 ) { // Start the time zone array _generator.indent(); _generator.outPropertyName(JSON_TIMEZONES); _generator.out(": ["); _generator.nl(); _generator.incIndent(); } else { // Add a time zone to the array _generator.out(","); _generator.nl(); } VTimeZone vtz = (VTimeZone)component; JsonTimeZoneAdapter tzAdapter = new JsonTimeZoneAdapter(vtz); _generator.toJson(tzAdapter); _timeZoneCount++; // Add this time zone to the map String tzid = vtz.getTimeZoneId().getValue(); TimeZone tz = new TimeZone(vtz); _timeZones.put(tzid, tz); } else { CALENDAR_SERVICE_LOGGER.getLogger().fine("Un support top level component:["+component.getClass()+"]"); // $NON-NLS-1$ } } else { // The stack is not empty. Add this component to it's parent. Component parent = _componentStack.peek(); if ( parent instanceof VTimeZone ) { ((VTimeZone)parent).getObservances().add(component); } else if ( parent instanceof VEvent ) { if ( component instanceof VAlarm ) { ((VEvent)parent).getAlarms().add(component); } } // TODO: Handle other types of nested components here } } catch (JsonException e) { throw new IllegalArgumentException(e); } catch (IOException e) { throw new IllegalArgumentException(e); } } public void startProperty(String property) { // Start a new parameter list _parameters = new ParameterList(); } @SuppressWarnings("unused") // $NON-NLS-1$ public void endProperty(String propertyName) { try { // TODO: Some of this code should be moved to JsonCalendarAdapter. if( _componentStack.empty() && _propertyValue != null){ // the property in the icalendar must before any component // icalbody = calprops component if(_startComponment){ CALENDAR_SERVICE_LOGGER.getLogger().fine("Un expected top level property:["+propertyName+"]"); // $NON-NLS-1$ return; } if ( METHOD.equals(propertyName)) { _generator.indent(); _generator.outPropertyName(JSON_SCHEDULE_METHOD); _generator.out(":"); _generator.outStringLiteral(_propertyValue.toLowerCase()); _generator.out(","); _generator.nl(); return; } if ( propertyName.startsWith("X-") && _componentStack.empty() && _propertyValue != null) { // $NON-NLS-1$ _generator.indent(); _generator.outPropertyName(propertyName.toLowerCase()); _generator.out(":"); Property xProperty = PropertyFactoryImpl.getInstance().createProperty(propertyName, _parameters, _propertyValue); JsonXPropertyAdapter tzAdapter = new JsonXPropertyAdapter(xProperty); try { _generator.toJson(tzAdapter); } catch (JsonException e) { _generator.outStringLiteral(_propertyValue); } // iCalendar must have at last one component // component = 1*(eventc / todoc / journalc / freebusyc / // timezonec / iana-comp / x-comp) _generator.out(","); _generator.nl(); return; } // TODO: Remove this code once we make a final decision about the // productId property. For now, we don't generate productId. if ( false && PRODID.equals(propertyName) && _componentStack.empty() && _propertyValue != null) { _generator.indent(); _generator.outPropertyName(JSON_PRODUCT_ID); _generator.out(":"); _generator.outStringLiteral(_propertyValue); _generator.out(","); _generator.nl(); return; } } TimeZone tz = null; Parameter tzid = _parameters.getParameter(TZID); if ( tzid != null && tzid.getValue() != null ) { tz = _timeZones.get(tzid.getValue()); } Property property = null; if ( tz != null ) { if ( DTSTART.equals(propertyName) ) { property = new DtStart(_propertyValue, tz); } else if ( DTEND.equals(propertyName) ) { property = new DtEnd(_propertyValue, tz); } } if ( property == null ) { // Create a property from the current parameter list and property value property = PropertyFactoryImpl.getInstance().createProperty(propertyName, _parameters, _propertyValue); } // Add the property to the current component if ( ! _componentStack.empty() ) { Component component = _componentStack.peek(); component.getProperties().add(property); } } catch (IOException e) { throw new IllegalArgumentException(e); } catch (URISyntaxException e) { throw new IllegalArgumentException(e); } catch (ParseException e) { throw new IllegalArgumentException(e); } finally { _parameters = null; _propertyValue = null; } } public void parameter(String paramName, String paramValue) throws URISyntaxException { // Add this parameter to the current parameter list Parameter parameter = ParameterFactoryImpl.getInstance().createParameter(paramName, paramValue); _parameters.add(parameter); } public void propertyValue(String value) throws URISyntaxException, ParseException, IOException { _propertyValue = value; } }