001package biweekly.io;
002
003import java.io.Closeable;
004import java.io.IOException;
005import java.util.Collection;
006import java.util.HashSet;
007import java.util.LinkedList;
008import java.util.List;
009import java.util.Map;
010import java.util.Set;
011
012import biweekly.ICalVersion;
013import biweekly.ICalendar;
014import biweekly.component.ICalComponent;
015import biweekly.component.RawComponent;
016import biweekly.io.scribe.ScribeIndex;
017import biweekly.io.scribe.component.ICalComponentScribe;
018import biweekly.io.scribe.property.ICalPropertyScribe;
019import biweekly.property.ICalProperty;
020import biweekly.property.RawProperty;
021
022/*
023 Copyright (c) 2013-2015, Michael Angstadt
024 All rights reserved.
025
026 Redistribution and use in source and binary forms, with or without
027 modification, are permitted provided that the following conditions are met: 
028
029 1. Redistributions of source code must retain the above copyright notice, this
030 list of conditions and the following disclaimer. 
031 2. Redistributions in binary form must reproduce the above copyright notice,
032 this list of conditions and the following disclaimer in the documentation
033 and/or other materials provided with the distribution. 
034
035 THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
036 ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
037 WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
038 DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
039 ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
040 (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
041 LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
042 ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
043 (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
044 SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
045 */
046
047/**
048 * Writes iCalendar objects to a data stream.
049 * @author Michael Angstadt
050 */
051public abstract class StreamWriter implements Closeable {
052        protected ScribeIndex index = new ScribeIndex();
053        protected WriteContext context;
054        protected TimezoneInfo tzinfo = new TimezoneInfo();
055
056        /**
057         * Writes an iCalendar object to the data stream.
058         * @param ical the iCalendar object to write
059         * @throws IllegalArgumentException if the scribe class for a component or
060         * property object cannot be found (only happens when an experimental
061         * property/component scribe is not registered with the
062         * {@code registerScribe} method.)
063         * @throws IOException if there's a problem writing to the data stream
064         */
065        public void write(ICalendar ical) throws IOException {
066                Collection<Class<? extends Object>> unregistered = findScribeless(ical);
067                if (!unregistered.isEmpty()) {
068                        throw new IllegalArgumentException("No scribes were found for the following component/property classes: " + unregistered);
069                }
070
071                context = new WriteContext(getTargetVersion(), tzinfo);
072                _write(ical);
073        }
074
075        /**
076         * Gets the version that the next iCalendar object will be written as.
077         * @return the version
078         */
079        protected abstract ICalVersion getTargetVersion();
080
081        /**
082         * Writes an iCalendar object to the data stream.
083         * @param ical the iCalendar object to write
084         * @throws IOException if there's a problem writing to the data stream
085         */
086        protected abstract void _write(ICalendar ical) throws IOException;
087
088        /**
089         * Gets the timezone-related info for this writer.
090         * @return the timezone-related info
091         */
092        public TimezoneInfo getTimezoneInfo() {
093                return tzinfo;
094        }
095
096        /**
097         * Sets the timezone-related info for this writer.
098         * @param tzinfo the timezone-related info
099         */
100        public void setTimezoneInfo(TimezoneInfo tzinfo) {
101                this.tzinfo = tzinfo;
102        }
103
104        /**
105         * <p>
106         * Registers an experimental property scribe. Can also be used to override
107         * the scribe of a standard property (such as DTSTART). Calling this method
108         * is the same as calling:
109         * </p>
110         * <p>
111         * {@code getScribeIndex().register(scribe)}.
112         * </p>
113         * @param scribe the scribe to register
114         */
115        public void registerScribe(ICalPropertyScribe<? extends ICalProperty> scribe) {
116                index.register(scribe);
117        }
118
119        /**
120         * <p>
121         * Registers an experimental component scribe. Can also be used to override
122         * the scribe of a standard component (such as VEVENT). Calling this method
123         * is the same as calling:
124         * </p>
125         * <p>
126         * {@code getScribeIndex().register(scribe)}.
127         * </p>
128         * @param scribe the scribe to register
129         */
130        public void registerScribe(ICalComponentScribe<? extends ICalComponent> scribe) {
131                index.register(scribe);
132        }
133
134        /**
135         * Gets the object that manages the component/property scribes.
136         * @return the scribe index
137         */
138        public ScribeIndex getScribeIndex() {
139                return index;
140        }
141
142        /**
143         * Sets the object that manages the component/property scribes.
144         * @param scribe the scribe index
145         */
146        public void setScribeIndex(ScribeIndex scribe) {
147                this.index = scribe;
148        }
149
150        /**
151         * Gets the component/property classes that don't have scribes associated
152         * with them.
153         * @param ical the iCalendar object
154         * @return the component/property classes
155         */
156        private Collection<Class<?>> findScribeless(ICalendar ical) {
157                Set<Class<?>> unregistered = new HashSet<Class<?>>();
158                LinkedList<ICalComponent> components = new LinkedList<ICalComponent>();
159                components.add(ical);
160
161                while (!components.isEmpty()) {
162                        ICalComponent component = components.removeLast();
163
164                        Class<? extends ICalComponent> componentClass = component.getClass();
165                        if (componentClass != RawComponent.class && index.getComponentScribe(componentClass) == null) {
166                                unregistered.add(componentClass);
167                        }
168
169                        for (Map.Entry<Class<? extends ICalProperty>, List<ICalProperty>> entry : component.getProperties()) {
170                                List<ICalProperty> properties = entry.getValue();
171                                if (properties.isEmpty()) {
172                                        continue;
173                                }
174
175                                Class<? extends ICalProperty> clazz = entry.getKey();
176                                if (clazz != RawProperty.class && index.getPropertyScribe(clazz) == null) {
177                                        unregistered.add(clazz);
178                                }
179                        }
180
181                        components.addAll(component.getComponents().values());
182                }
183
184                return unregistered;
185        }
186}