001: /*
002: * Licensed to the Apache Software Foundation (ASF) under one or more
003: * contributor license agreements. See the NOTICE file distributed with
004: * this work for additional information regarding copyright ownership.
005: * The ASF licenses this file to You under the Apache License, Version 2.0
006: * (the "License"); you may not use this file except in compliance with
007: * the License. You may obtain a copy of the License at
008: *
009: * http://www.apache.org/licenses/LICENSE-2.0
010: *
011: * Unless required by applicable law or agreed to in writing, software
012: * distributed under the License is distributed on an "AS IS" BASIS,
013: * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014: * See the License for the specific language governing permissions and
015: * limitations under the License.
016: */
017: package org.apache.cocoon.generation;
018:
019: import java.io.IOException;
020: import java.io.Serializable;
021: import java.text.DateFormat;
022: import java.text.DecimalFormat;
023: import java.text.SimpleDateFormat;
024: import java.util.ArrayList;
025: import java.util.Calendar;
026: import java.util.List;
027: import java.util.Locale;
028: import java.util.Map;
029:
030: import org.apache.avalon.framework.parameters.Parameters;
031: import org.apache.cocoon.ProcessingException;
032: import org.apache.cocoon.caching.CacheableProcessingComponent;
033: import org.apache.cocoon.environment.SourceResolver;
034: import org.apache.commons.lang.BooleanUtils;
035: import org.apache.excalibur.source.SourceValidity;
036: import org.apache.excalibur.source.impl.validity.NOPValidity;
037: import org.xml.sax.SAXException;
038: import org.xml.sax.helpers.AttributesImpl;
039:
040: /**
041: * @cocoon.sitemap.component.documentation
042: * Generates an XML document representing a calendar for a given month and year.
043: *
044: * @cocoon.sitemap.component.documentation.caching TBD
045: * @cocoon.sitemap.component.name calendar
046: * @cocoon.sitemap.component.label content
047: * @cocoon.sitemap.component.logger sitemap.generator.calendar
048: *
049: *
050: * <p>
051: * Here is a sample output:
052: * </p>
053: * <pre>
054: * <calendar:calendar xmlns:calendar="http://apache.org/cocoon/calendar/1.0"
055: * year="2004" month="January" prevMonth="12" prevYear="2003"
056: * nextMonth="02" nextYear="2004">
057: * <calendar:week number="1">
058: * <calendar:day number="1" weekday="THURSDAY" date="January 1, 2004"/>
059: * <calendar:day number="2" weekday="FRIDAY" date="January 2, 2004"/>
060: * <calendar:day number="3" weekday="SATURDAY" date="January 3, 2004"/>
061: * <calendar:day number="4" weekday="SUNDAY" date="January 4, 2004"/>
062: * </calendar:week>
063: * ...
064: * </calendar:calendar>
065: * </pre>
066: * <p>
067: * The <i>src</i> parameter is ignored.
068: * </p>
069: * <p>
070: * <b>Configuration options:</b>
071: * <dl>
072: * <dt> <i>month</i> (optional)</dt>
073: * <dd> Sets the month for the calendar (January is 1). Default is the current month.</dd>
074: * <dt> <i>year</i> (optional)</dt>
075: * <dd> Sets the year for the calendar. Default is the current year.</dd>
076: * <dt> <i>dateFormat</i> (optional)</dt>
077: * <dd> Sets the format for the date attribute of each node, as
078: * described in java.text.SimpleDateFormat. If unset, the default
079: * format for the current locale will be used.</dd>
080: * <dt> <i>lang</i> (optional)</dt>
081: * <dd> Sets the ISO language code for determining the locale.</dd>
082: * <dt> <i>country</i> (optional)</dt>
083: * <dd> Sets the ISO country code for determining the locale.</dd>
084: * <dt> <i>padWeeks</i> (optional)</dt>
085: * <dd> If set to true, full weeks will be generated by adding
086: * days from the end of the previous month and the beginning
087: * of the following month.</dd>
088: * </dl>
089: * </p>
090: *
091: * @version CVS $Id: CalendarGenerator.java 433543 2006-08-22 06:22:54Z crossley $
092: */
093: public class CalendarGenerator extends ServiceableGenerator implements
094: CacheableProcessingComponent {
095:
096: /** The URI of the namespace of this generator. */
097: protected static final String URI = "http://apache.org/cocoon/calendar/1.0";
098:
099: /** The namespace prefix for this namespace. */
100: protected static final String PREFIX = "calendar";
101:
102: /** Node and attribute names */
103: protected static final String CALENDAR_NODE_NAME = "calendar";
104: protected static final String WEEK_NODE_NAME = "week";
105: protected static final String DAY_NODE_NAME = "day";
106: protected static final String MONTH_ATTR_NAME = "month";
107: protected static final String YEAR_ATTR_NAME = "year";
108: protected static final String DATE_ATTR_NAME = "date";
109: protected static final String NUMBER_ATTR_NAME = "number";
110: protected static final String WEEKDAY_ATTR_NAME = "weekday";
111: protected static final String PREV_MONTH_ATTR_NAME = "prevMonth";
112: protected static final String PREV_YEAR_ATTR_NAME = "prevYear";
113: protected static final String NEXT_MONTH_ATTR_NAME = "nextMonth";
114: protected static final String NEXT_YEAR_ATTR_NAME = "nextYear";
115:
116: /** Formatter for month number */
117: protected static final DecimalFormat monthNumberFormatter = new DecimalFormat(
118: "00");
119:
120: /** Convenience object, so we don't need to create an AttributesImpl for every element. */
121: protected AttributesImpl attributes;
122:
123: /**
124: * The cache key needs to be generated for the configuration of this
125: * generator, so storing the parameters for generateKey().
126: */
127: protected List cacheKeyParList;
128:
129: /** The year to generate the calendar for */
130: protected int year;
131:
132: /** The month to generate the calendar for */
133: protected int month;
134:
135: /** The format for dates */
136: protected DateFormat dateFormatter;
137:
138: /** The format for month names */
139: protected DateFormat monthFormatter;
140:
141: /** The current locale */
142: protected Locale locale;
143:
144: /** Do we need to pad out the first and last weeks? */
145: protected boolean padWeeks;
146:
147: /* Add the day of the week
148: *
149: * since SUNDAY=1, we start with a dummy
150: * entry.
151: */
152: protected String weekdays[] = { "", "SUNDAY", "MONDAY", "TUESDAY",
153: "WEDNESDAY", "THURSDAY", "FRIDAY", "SATURDAY" };
154:
155: /**
156: * Set the request parameters. Must be called before the generate method.
157: *
158: * @param resolver the SourceResolver object
159: * @param objectModel a <code>Map</code> containing model object
160: * @param src the source URI (ignored)
161: * @param par configuration parameters
162: */
163: public void setup(SourceResolver resolver, Map objectModel,
164: String src, Parameters par) throws ProcessingException,
165: SAXException, IOException {
166: super .setup(resolver, objectModel, src, par);
167:
168: this .cacheKeyParList = new ArrayList();
169: this .cacheKeyParList.add(src);
170:
171: // Determine the locale
172: String langString = par.getParameter("lang", null);
173: locale = Locale.getDefault();
174: if (langString != null) {
175: this .cacheKeyParList.add(langString);
176: String countryString = par.getParameter("country", "");
177: if (!"".equals(countryString)) {
178: this .cacheKeyParList.add(countryString);
179: }
180: locale = new Locale(langString, countryString);
181: }
182:
183: // Determine year and month. Default is current year and month.
184: Calendar now = Calendar.getInstance(locale);
185: this .year = par.getParameterAsInteger("year", now
186: .get(Calendar.YEAR));
187: this .cacheKeyParList.add(String.valueOf(this .year));
188: this .month = par.getParameterAsInteger("month", now
189: .get(Calendar.MONTH) + 1) - 1;
190: this .cacheKeyParList.add(String.valueOf(this .month));
191:
192: String dateFormatString = par.getParameter("dateFormat", null);
193: this .cacheKeyParList.add(dateFormatString);
194: if (dateFormatString != null) {
195: this .dateFormatter = new SimpleDateFormat(dateFormatString,
196: locale);
197: } else {
198: this .dateFormatter = DateFormat.getDateInstance(
199: DateFormat.LONG, locale);
200: }
201: this .padWeeks = par.getParameterAsBoolean("padWeeks", false);
202: this .cacheKeyParList.add(BooleanUtils
203: .toBooleanObject(this .padWeeks));
204: this .monthFormatter = new SimpleDateFormat("MMMM", locale);
205: this .attributes = new AttributesImpl();
206: }
207:
208: /**
209: * Generate XML data.
210: *
211: * @throws SAXException if an error occurs while outputting the document
212: */
213: public void generate() throws SAXException, ProcessingException {
214: Calendar start = Calendar.getInstance(locale);
215: start.clear();
216: start.set(Calendar.YEAR, this .year);
217: start.set(Calendar.MONTH, this .month);
218: start.set(Calendar.DAY_OF_MONTH, 1);
219: Calendar end = (Calendar) start.clone();
220: end.add(Calendar.MONTH, 1);
221:
222: // Determine previous and next months
223: Calendar prevMonth = (Calendar) start.clone();
224: prevMonth.add(Calendar.MONTH, -1);
225:
226: this .contentHandler.startDocument();
227: this .contentHandler.startPrefixMapping(PREFIX, URI);
228: attributes.clear();
229: attributes.addAttribute("", YEAR_ATTR_NAME, YEAR_ATTR_NAME,
230: "CDATA", String.valueOf(year));
231: attributes.addAttribute("", MONTH_ATTR_NAME, MONTH_ATTR_NAME,
232: "CDATA", monthFormatter.format(start.getTime()));
233:
234: // Add previous and next month
235: attributes.addAttribute("", PREV_YEAR_ATTR_NAME,
236: PREV_YEAR_ATTR_NAME, "CDATA", String.valueOf(prevMonth
237: .get(Calendar.YEAR)));
238: attributes.addAttribute("", PREV_MONTH_ATTR_NAME,
239: PREV_MONTH_ATTR_NAME, "CDATA", monthNumberFormatter
240: .format(prevMonth.get(Calendar.MONTH) + 1));
241: attributes.addAttribute("", NEXT_YEAR_ATTR_NAME,
242: NEXT_YEAR_ATTR_NAME, "CDATA", String.valueOf(end
243: .get(Calendar.YEAR)));
244: attributes.addAttribute("", NEXT_MONTH_ATTR_NAME,
245: NEXT_MONTH_ATTR_NAME, "CDATA", monthNumberFormatter
246: .format(end.get(Calendar.MONTH) + 1));
247:
248: this .contentHandler.startElement(URI, CALENDAR_NODE_NAME,
249: PREFIX + ':' + CALENDAR_NODE_NAME, attributes);
250: int weekNo = start.get(Calendar.WEEK_OF_MONTH);
251: int firstDay = start.getFirstDayOfWeek();
252: if (start.get(Calendar.DAY_OF_WEEK) != firstDay) {
253: attributes.clear();
254: attributes.addAttribute("", NUMBER_ATTR_NAME,
255: NUMBER_ATTR_NAME, "CDATA", String.valueOf(weekNo));
256: this .contentHandler.startElement(URI, WEEK_NODE_NAME,
257: PREFIX + ':' + WEEK_NODE_NAME, attributes);
258: if (padWeeks) {
259: Calendar previous = (Calendar) start.clone();
260: while (previous.get(Calendar.DAY_OF_WEEK) != firstDay) {
261: previous.add(Calendar.DAY_OF_MONTH, -1);
262: }
263: while (previous.before(start)) {
264: attributes.clear();
265: attributes.addAttribute("", NUMBER_ATTR_NAME,
266: NUMBER_ATTR_NAME, "CDATA",
267: String.valueOf(previous
268: .get(Calendar.DAY_OF_MONTH)));
269: attributes
270: .addAttribute("", WEEKDAY_ATTR_NAME,
271: WEEKDAY_ATTR_NAME, "CDATA",
272: weekdays[previous
273: .get(Calendar.DAY_OF_WEEK)]);
274: attributes.addAttribute("", DATE_ATTR_NAME,
275: DATE_ATTR_NAME, "CDATA", dateFormatter
276: .format(previous.getTime()));
277: this .contentHandler.startElement(URI,
278: DAY_NODE_NAME,
279: PREFIX + ':' + DAY_NODE_NAME, attributes);
280: addContent(previous, locale);
281: this .contentHandler.endElement(URI, DAY_NODE_NAME,
282: PREFIX + ':' + DAY_NODE_NAME);
283: previous.add(Calendar.DAY_OF_MONTH, 1);
284: }
285: }
286: }
287: while (start.before(end)) {
288: if (start.get(Calendar.DAY_OF_WEEK) == firstDay) {
289: weekNo = start.get(Calendar.WEEK_OF_MONTH);
290: attributes.clear();
291: attributes.addAttribute("", NUMBER_ATTR_NAME,
292: NUMBER_ATTR_NAME, "CDATA", String
293: .valueOf(weekNo));
294: this .contentHandler.startElement(URI, WEEK_NODE_NAME,
295: PREFIX + ':' + WEEK_NODE_NAME, attributes);
296: }
297: attributes.clear();
298: attributes.addAttribute("", NUMBER_ATTR_NAME,
299: NUMBER_ATTR_NAME, "CDATA", String.valueOf(start
300: .get(Calendar.DAY_OF_MONTH)));
301: attributes.addAttribute("", WEEKDAY_ATTR_NAME,
302: WEEKDAY_ATTR_NAME, "CDATA", weekdays[start
303: .get(Calendar.DAY_OF_WEEK)]);
304: attributes.addAttribute("", DATE_ATTR_NAME, DATE_ATTR_NAME,
305: "CDATA", dateFormatter.format(start.getTime()));
306: this .contentHandler.startElement(URI, DAY_NODE_NAME, PREFIX
307: + ':' + DAY_NODE_NAME, attributes);
308: addContent(start, locale);
309: this .contentHandler.endElement(URI, DAY_NODE_NAME, PREFIX
310: + ':' + DAY_NODE_NAME);
311: start.add(Calendar.DAY_OF_MONTH, 1);
312: if (start.get(Calendar.DAY_OF_WEEK) == firstDay
313: || (!padWeeks && !start.before(end))) {
314: this .contentHandler.endElement(URI, WEEK_NODE_NAME,
315: PREFIX + ':' + WEEK_NODE_NAME);
316: }
317: }
318:
319: if (padWeeks) {
320: while (firstDay != end.get(Calendar.DAY_OF_WEEK)) {
321: attributes.clear();
322: attributes.addAttribute("", NUMBER_ATTR_NAME,
323: NUMBER_ATTR_NAME, "CDATA", String.valueOf(end
324: .get(Calendar.DAY_OF_MONTH)));
325: attributes.addAttribute("", WEEKDAY_ATTR_NAME,
326: WEEKDAY_ATTR_NAME, "CDATA", weekdays[end
327: .get(Calendar.DAY_OF_WEEK)]);
328: attributes.addAttribute("", DATE_ATTR_NAME,
329: DATE_ATTR_NAME, "CDATA", dateFormatter
330: .format(end.getTime()));
331: this .contentHandler.startElement(URI, DAY_NODE_NAME,
332: PREFIX + ':' + DAY_NODE_NAME, attributes);
333: addContent(end, locale);
334: this .contentHandler.endElement(URI, DAY_NODE_NAME,
335: PREFIX + ':' + DAY_NODE_NAME);
336: end.add(Calendar.DAY_OF_MONTH, 1);
337: if (firstDay == end.get(Calendar.DAY_OF_WEEK)) {
338: this .contentHandler.endElement(URI, WEEK_NODE_NAME,
339: PREFIX + ':' + WEEK_NODE_NAME);
340: }
341: }
342: }
343: this .contentHandler.endElement(URI, CALENDAR_NODE_NAME, PREFIX
344: + ':' + CALENDAR_NODE_NAME);
345: this .contentHandler.endPrefixMapping(PREFIX);
346: this .contentHandler.endDocument();
347: }
348:
349: /**
350: * Add content to a <day> element. This method is intended to be overridden
351: * by subclasses that want to add content to one or more days of the calendar.
352: *
353: * @param date The date corresponding to the current element.
354: * @param locale The current locale.
355: * @throws SAXException if an error occurs while outputting the document
356: */
357: protected void addContent(Calendar date, Locale locale)
358: throws SAXException {
359: }
360:
361: /* (non-Javadoc)
362: * @see org.apache.cocoon.caching.CacheableProcessingComponent#getKey()
363: */
364: public Serializable getKey() {
365: StringBuffer buffer = new StringBuffer();
366: int len = this .cacheKeyParList.size();
367: for (int i = 0; i < len; i++) {
368: buffer.append(this .cacheKeyParList.get(i) + ":");
369: }
370: return buffer.toString();
371: }
372:
373: /* (non-Javadoc)
374: * @see org.apache.cocoon.caching.CacheableProcessingComponent#getValidity()
375: */
376: public SourceValidity getValidity() {
377: return NOPValidity.SHARED_INSTANCE;
378: }
379:
380: /**
381: * Recycle resources
382: * @see org.apache.avalon.excalibur.pool.Recyclable#recycle()
383: */
384: public void recycle() {
385: this.cacheKeyParList = null;
386: this.attributes = null;
387: this.dateFormatter = null;
388: this.monthFormatter = null;
389: this.locale = null;
390: super.recycle();
391: }
392:
393: }
|