001: /*---------------------------------------------------------------------------*\
002: $Id: Tool.java 7041 2007-09-09 01:04:47Z bmc $
003: ---------------------------------------------------------------------------
004: This software is released under a BSD-style license:
005:
006: Copyright (c) 2004-2007 Brian M. Clapper. All rights reserved.
007:
008: Redistribution and use in source and binary forms, with or without
009: modification, are permitted provided that the following conditions are
010: met:
011:
012: 1. Redistributions of source code must retain the above copyright notice,
013: this list of conditions and the following disclaimer.
014:
015: 2. The end-user documentation included with the redistribution, if any,
016: must include the following acknowlegement:
017:
018: "This product includes software developed by Brian M. Clapper
019: (bmc@clapper.org, http://www.clapper.org/bmc/). That software is
020: copyright (c) 2004-2007 Brian M. Clapper."
021:
022: Alternately, this acknowlegement may appear in the software itself,
023: if wherever such third-party acknowlegements normally appear.
024:
025: 3. Neither the names "clapper.org", "curn", nor any of the names of the
026: project contributors may be used to endorse or promote products
027: derived from this software without prior written permission. For
028: written permission, please contact bmc@clapper.org.
029:
030: 4. Products derived from this software may not be called "curn", nor may
031: "clapper.org" appear in their names without prior written permission
032: of Brian M. Clapper.
033:
034: THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED
035: WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
036: MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN
037: NO EVENT SHALL BRIAN M. CLAPPER BE LIABLE FOR ANY DIRECT, INDIRECT,
038: INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
039: NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
040: DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
041: THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
042: (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
043: THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
044: \*---------------------------------------------------------------------------*/
045:
046: package org.clapper.curn;
047:
048: import java.io.File;
049: import java.io.StringWriter;
050: import java.io.PrintWriter;
051: import java.net.MalformedURLException;
052: import java.net.URL;
053: import java.text.ParseException;
054: import java.text.SimpleDateFormat;
055: import java.util.ArrayList;
056: import java.util.Calendar;
057: import java.util.Collection;
058: import java.util.Date;
059: import java.util.HashSet;
060: import java.util.Iterator;
061: import java.util.Map;
062: import java.util.NoSuchElementException;
063: import java.util.Set;
064: import java.util.TreeMap;
065: import javax.naming.ConfigurationException;
066:
067: import org.clapper.util.classutil.ClassUtil;
068: import org.clapper.util.cmdline.CommandLineUtility;
069: import org.clapper.util.cmdline.CommandLineException;
070: import org.clapper.util.cmdline.CommandLineUsageException;
071: import org.clapper.util.cmdline.CommandLineUserException;
072: import org.clapper.util.cmdline.UsageInfo;
073: import org.clapper.util.io.WordWrapWriter;
074: import org.clapper.util.logging.Logger;
075: import org.clapper.util.misc.BuildInfo;
076:
077: /**
078: * <p><i>curn</i>: Customizable Utilitarian RSS Notifier.</p>
079: *
080: * <p><i>curn</i> is an RSS reader. It scans a configured set of URLs, each
081: * one representing an RSS feed, and summarizes the results in an
082: * easy-to-read text format. <i>curn</i> keeps track of URLs it's seen
083: * before, using an on-disk cache; when using the cache, it will suppress
084: * displaying URLs it has already reported (though that behavior can be
085: * disabled). <i>curn</i> can be extended to use any RSS parser; by
086: * default, it uses the ROME parser.</p>
087: *
088: * <p>This class is a command-line wrapper for <i>curn</i>. Run it with
089: * no parameters for a usage summary.</p>
090: *
091: * @version <tt>$Revision: 7041 $</tt>
092: */
093: public class Tool extends CommandLineUtility implements
094: PostConfigPlugIn {
095: /*----------------------------------------------------------------------*\
096: Private Constants
097: \*----------------------------------------------------------------------*/
098:
099: private static Collection<DateParseInfo> DATE_FORMATS; // NOPMD
100:
101: static {
102: DATE_FORMATS = new ArrayList<DateParseInfo>();
103:
104: DATE_FORMATS.add(new DateParseInfo("yyyy/MM/dd hh:mm:ss a",
105: false));
106: DATE_FORMATS
107: .add(new DateParseInfo("yyyy/MM/dd hh:mm:ss", false));
108: DATE_FORMATS
109: .add(new DateParseInfo("yyyy/MM/dd hh:mm a", false));
110: DATE_FORMATS.add(new DateParseInfo("yyyy/MM/dd hh:mm", false));
111: DATE_FORMATS.add(new DateParseInfo("yyyy/MM/dd h:mm a", false));
112: DATE_FORMATS.add(new DateParseInfo("yyyy/MM/dd h:mm", false));
113: DATE_FORMATS.add(new DateParseInfo("yyyy/MM/dd hh a", false));
114: DATE_FORMATS.add(new DateParseInfo("yyyy/MM/dd h a", false));
115: DATE_FORMATS
116: .add(new DateParseInfo("yyyy/MM/dd HH:mm:ss", false));
117: DATE_FORMATS.add(new DateParseInfo("yyyy/MM/dd HH:mm", false));
118: DATE_FORMATS.add(new DateParseInfo("yyyy/MM/dd H:mm", false));
119: DATE_FORMATS.add(new DateParseInfo("yyyy/MM/dd", false));
120: DATE_FORMATS.add(new DateParseInfo("yy/MM/dd", false));
121: DATE_FORMATS.add(new DateParseInfo("hh:mm:ss a", true));
122: DATE_FORMATS.add(new DateParseInfo("hh:mm:ss", true));
123: DATE_FORMATS.add(new DateParseInfo("hh:mm a", true));
124: DATE_FORMATS.add(new DateParseInfo("hh:mm", true));
125: DATE_FORMATS.add(new DateParseInfo("h:mm a", true));
126: DATE_FORMATS.add(new DateParseInfo("h:mm", true));
127: DATE_FORMATS.add(new DateParseInfo("hh a", true));
128: DATE_FORMATS.add(new DateParseInfo("h a", true));
129: DATE_FORMATS.add(new DateParseInfo("HH:mm:ss a", true));
130: DATE_FORMATS.add(new DateParseInfo("HH:mm:ss", true));
131: DATE_FORMATS.add(new DateParseInfo("HH:mm a", true));
132: DATE_FORMATS.add(new DateParseInfo("HH:mm", true));
133: DATE_FORMATS.add(new DateParseInfo("H:mm a", true));
134: DATE_FORMATS.add(new DateParseInfo("H:mm", true));
135: };
136:
137: /*----------------------------------------------------------------------*\
138: Private Data Items
139: \*----------------------------------------------------------------------*/
140:
141: private String configPath = null;
142: private boolean useCache = true;
143: private Date currentTime = new Date();
144: private boolean optShowBuildInfo = false;
145: private boolean optShowPlugIns = false;
146: private boolean optShowVersion = false;
147: private Boolean optUpdateCache = null;
148: private boolean optAbortOnUndefinedConfigVar = true;
149:
150: /**
151: * For log messages
152: */
153: private static final Logger log = new Logger(Tool.class);
154:
155: /**
156: * For error messages
157: */
158: private static WordWrapWriter err = new WordWrapWriter(System.err);
159:
160: /*----------------------------------------------------------------------*\
161: Main Program
162: \*----------------------------------------------------------------------*/
163:
164: public static void main(final String[] args) {
165: Tool tool = new Tool();
166:
167: try {
168: tool.execute(args);
169: }
170:
171: catch (CommandLineUsageException ex) {
172: // Already reported
173:
174: System.exit(1);
175: }
176:
177: catch (CommandLineUserException ex) {
178: // Don't dump a stack trace.
179:
180: err.println(ex.getMessages(true));
181: }
182:
183: catch (CommandLineException ex) {
184: ex.printStackTrace(System.err);
185: System.exit(1);
186: }
187:
188: catch (Exception ex) {
189: ex.printStackTrace(System.err);
190: System.exit(1);
191: }
192: }
193:
194: /*----------------------------------------------------------------------*\
195: Constructor
196: \*----------------------------------------------------------------------*/
197:
198: private Tool() {
199: // Cannot be instantiated directly.
200: }
201:
202: /*----------------------------------------------------------------------*\
203: Public Methods Required by PlugIn Interface
204: \*----------------------------------------------------------------------*/
205:
206: public String getPlugInName() {
207: return "curn command-line interface";
208: }
209:
210: public String getPlugInSortKey() {
211: return ClassUtil.getShortClassName(this .getClass());
212: }
213:
214: public void initPlugIn() throws CurnException {
215: }
216:
217: public void runPostConfigPlugIn(final CurnConfig config)
218: throws CurnException {
219: try {
220: adjustConfiguration(config);
221: }
222:
223: catch (ConfigurationException ex) {
224: throw new CurnException(ex);
225: }
226: }
227:
228: /*----------------------------------------------------------------------*\
229: Protected Methods
230: \*----------------------------------------------------------------------*/
231:
232: /**
233: * Called by <tt>parseParams()</tt> to handle any option it doesn't
234: * recognize. If the option takes any parameters, the overridden
235: * method must extract the parameter by advancing the supplied
236: * <tt>Iterator</tt> (which returns <tt>String</tt> objects). This
237: * default method simply throws an exception.
238: *
239: * @param shortOption the short option character, or
240: * {@link UsageInfo#NO_SHORT_OPTION} if there isn't
241: * one (i.e., if this is a long-only option).
242: * @param longOption the long option string, without any leading
243: * "-" characters, or null if this is a short-only
244: * option
245: * @param it the <tt>Iterator</tt> for the remainder of the
246: * command line, for extracting parameters.
247: *
248: * @throws CommandLineUsageException on error
249: * @throws NoSuchElementException overran the iterator (i.e., missing
250: * parameter)
251: */
252: @Override
253: protected void parseCustomOption(final char shortOption,
254: final String longOption, final Iterator<String> it)
255: throws CommandLineUsageException, NoSuchElementException {
256: switch (shortOption) {
257: case 'a': // --authors
258: deprecatedOption(shortOption, longOption);
259: break;
260:
261: case 'A': // --no-authors
262: deprecatedOption(shortOption, longOption);
263: break;
264:
265: case 'B': // --build-info
266: optShowBuildInfo = true;
267: break;
268:
269: case 'C': // --no-cache
270: useCache = false;
271: break;
272:
273: case 'd': // --show-dates
274: deprecatedOption(shortOption, longOption);
275: break;
276:
277: case 'D': // --no-dates
278: deprecatedOption(shortOption, longOption);
279: break;
280:
281: case 'p': // --plug-ins
282: optShowPlugIns = true;
283: break;
284:
285: case 'r': // --rss-version
286: deprecatedOption(shortOption, longOption);
287: break;
288:
289: case 'R': // --no-rss-version
290: deprecatedOption(shortOption, longOption);
291: break;
292:
293: case 't': // --time
294: currentTime = parseDateTime(it.next());
295: break;
296:
297: case 'T': // --threads
298: deprecatedOption(shortOption, longOption);
299: break;
300:
301: case 'u': // --no-update
302: optUpdateCache = Boolean.FALSE;
303: break;
304:
305: case 'U': // --allow-undefined-cfg-vars
306: optAbortOnUndefinedConfigVar = false;
307: break;
308:
309: case 'v':
310: optShowVersion = true;
311: break;
312:
313: case 'z': // --gzip
314: deprecatedOption(shortOption, longOption);
315: break;
316:
317: case 'Z': // --no-gzip
318: deprecatedOption(shortOption, longOption);
319: break;
320:
321: default:
322: // Should not happen.
323: throw new IllegalStateException("(BUG) Unknown option. "
324: + "Why am I here?");
325: }
326: }
327:
328: /**
329: * <p>Called by <tt>parseParams()</tt> once option parsing is complete,
330: * this method must handle any additional parameters on the command
331: * line. It's not necessary for the method to ensure that the iterator
332: * has the right number of strings left in it. If you attempt to pull
333: * too many parameters from the iterator, it'll throw a
334: * <tt>NoSuchElementException</tt>, which <tt>parseParams()</tt> traps
335: * and converts into a suitable error message. Similarly, if there are
336: * any parameters left in the iterator when this method returns,
337: * <tt>parseParams()</tt> throws an exception indicating that there are
338: * too many parameters on the command line.</p>
339: *
340: * <p>This method is called unconditionally, even if there are no
341: * parameters left on the command line, so it's a useful place to do
342: * post-option consistency checks, as well.</p>
343: *
344: * @param it the <tt>Iterator</tt> for the remainder of the
345: * command line
346: *
347: * @throws CommandLineUsageException on error
348: * @throws NoSuchElementException attempt to iterate past end of args;
349: * <tt>parseParams()</tt> automatically
350: * handles this exception, so it's
351: * safe for subclass implementations of
352: * this method not to handle it
353: */
354: @Override
355: protected void processPostOptionCommandLine(
356: final Iterator<String> it)
357: throws CommandLineUsageException, NoSuchElementException {
358: // If we're showing build information or the version, forget about
359: // the remainder of the command line.
360:
361: if (!(optShowBuildInfo || optShowVersion || optShowPlugIns))
362: configPath = it.next();
363: }
364:
365: /**
366: * Called by <tt>parseParams()</tt> to get the custom command-line
367: * options and parameters handled by the subclass. This list is used
368: * solely to build a usage message. The overridden method must fill the
369: * supplied <tt>UsageInfo</tt> object:
370: *
371: * <ul>
372: * <li> Each parameter must be added to the object, via the
373: * <tt>UsageInfo.addParameter()</tt> method. The first argument
374: * to <tt>addParameter()</tt> is the parameter string (e.g.,
375: * "<dbCfg>" or "input_file"). The second parameter is the
376: * one-line description. The description may be of any length,
377: * but it should be a single line.
378: *
379: * <li> Each option must be added to the object, via the
380: * <tt>UsageInfo.addOption()</tt> method. The first argument to
381: * <tt>addOption()</tt> is the option string (e.g., "-x" or
382: * "-version"). The second parameter is the one-line
383: * description. The description may be of any length, but it
384: * should be a single line.
385: * </ul>
386: *
387: * That information will be combined with the common options supported
388: * by the base class, and used to build a usage message.
389: *
390: * @param info The <tt>UsageInfo</tt> object to fill.
391: */
392: @Override
393: protected void getCustomUsageInfo(final UsageInfo info) {
394: info.setCommandName("curn");
395:
396: // Note: A null explanation denotes a "hidden" option not shown in
397: // the usage output. Here, those options are deprecated, but
398: // retained for backward compatibility.
399:
400: info.addOption('a', "show-authors", null);
401: info.addOption('A', "no-authors", null);
402: info
403: .addOption(
404: 'B',
405: "build-info",
406: "Show full build information, then exit. "
407: + "This option shows a bit more information than the "
408: + UsageInfo.LONG_OPTION_PREFIX
409: + "version option. This option can be combined with "
410: + "the "
411: + UsageInfo.LONG_OPTION_PREFIX
412: + "plug-ins option to show the loaded plug-ins.");
413: info.addOption('C', "no-cache",
414: "Don't use a cache file at all.");
415: info.addOption('d', "show-dates", null);
416: info.addOption('D', "no-dates", null);
417: info
418: .addOption(
419: 'p',
420: "plug-ins",
421: "Show the list of located plug-ins and output "
422: + "handlers, then exit. This option can be combined "
423: + "with either "
424: + UsageInfo.LONG_OPTION_PREFIX
425: + "build-info or "
426: + UsageInfo.LONG_OPTION_PREFIX
427: + "version to show version information, as well.");
428: info.addOption('r', "rss-version", null);
429: info.addOption('R', "no-rss-version", null);
430: info.addOption('T', "threads", "<n>", null);
431: info.addOption('u', "no-update",
432: "Read the cache, but don't update it.");
433: info
434: .addOption(
435: 'U',
436: "allow-undefined-cfg-vars",
437: "Don't abort when an undefined variable is "
438: + "encountered in the configuration file; substitute "
439: + "an empty string, instead. Normally, an undefined "
440: + "configuration variable will cause curn to abort.");
441: info
442: .addOption(
443: 'v',
444: "version",
445: "Show version information, then exit. "
446: + "This option can be combined with the "
447: + UsageInfo.LONG_OPTION_PREFIX
448: + "plug-ins option to show the loaded plug-ins.");
449: info.addOption('z', "gzip", null);
450: info.addOption('Z', "no-gzip", null);
451:
452: StringWriter sw = new StringWriter();
453: PrintWriter pw = new PrintWriter(sw);
454: Date sampleDate;
455: BuildInfo buildInfo = Version.getBuildInfo();
456: SimpleDateFormat dateFormat;
457: String dateString = buildInfo.getBuildDate();
458:
459: try {
460: dateFormat = new SimpleDateFormat(
461: BuildInfo.DATE_FORMAT_STRING);
462: sampleDate = dateFormat.parse(dateString);
463: }
464:
465: catch (Exception ex) {
466: log.error("Can't parse build date string \"" + dateString
467: + "\" using format \""
468: + BuildInfo.DATE_FORMAT_STRING + "\"", ex);
469: sampleDate = new Date();
470: }
471:
472: Set<String> printed = new HashSet<String>();
473: for (DateParseInfo dpi : DATE_FORMATS) {
474: String s = dpi.formatDate(sampleDate);
475: if (!printed.contains(s)) {
476: pw.println();
477: pw.print(s);
478: printed.add(s);
479: }
480: }
481:
482: info
483: .addOption(
484: 't',
485: "time",
486: "<time>",
487: "For the purposes of cache expiration, pretend the "
488: + "current time is <time>. <time> may be in one of "
489: + "the following formats."
490: + sw.toString());
491:
492: info.addParameter("config",
493: "Path or URL to configuration file", true);
494: }
495:
496: /**
497: * Run the curn tool. This method parses the command line arguments,
498: * storing the results in an internal configuration; then, it
499: * instantiates a <tt>Curn</tt> object and calls its <tt>run()</tt>
500: * method.
501: *
502: * @throws CommandLineException error occurred
503: */
504: protected void runCommand() throws CommandLineException {
505: try {
506: boolean abort = false;
507:
508: if (optShowBuildInfo) {
509: abort = true;
510: Version.getInstance().showBuildInfo();
511: }
512:
513: else if (optShowVersion) {
514: abort = true;
515: Version.getInstance().showVersion();
516: }
517:
518: if (optShowPlugIns) {
519: showPlugIns();
520: abort = true;
521: }
522:
523: // Is the configuration file a URL or a path?
524:
525: if (!abort) {
526: // Allocate Curn object, which loads plug-ins.
527:
528: Curn curn = CurnFactory.newCurn();
529:
530: // Add this object as a plug-in.
531:
532: MetaPlugIn.getMetaPlugIn().addPlugIn(this );
533:
534: // Fire it up.
535:
536: curn.setCurrentTime(currentTime);
537: curn
538: .setAbortOnUndefinedConfigVariable(optAbortOnUndefinedConfigVar);
539: curn.run(getConfigurationURL(), this .useCache);
540: }
541: }
542:
543: catch (CurnUsageException ex) {
544: throw new CommandLineUserException(ex);
545: }
546:
547: catch (CurnException ex) {
548: throw new CommandLineException(ex);
549: }
550:
551: catch (Exception ex) {
552: ex.printStackTrace(System.err);
553: throw new CommandLineException(ex);
554: }
555: }
556:
557: /*----------------------------------------------------------------------*\
558: Private Methods
559: \*----------------------------------------------------------------------*/
560:
561: private void adjustConfiguration(final CurnConfig config)
562: throws ConfigurationException {
563: log.debug("adjustConfiguration() called.");
564:
565: // Adjust the configuration, if necessary, based on the command-line
566: // parameters.
567:
568: if (optUpdateCache != null)
569: config.setMustUpdateFeedMetadata(optUpdateCache
570: .booleanValue());
571: }
572:
573: private URL getConfigurationURL() throws CommandLineUsageException {
574: URL configURL = null;
575: try {
576: configURL = new URL(configPath);
577: }
578:
579: catch (MalformedURLException ex) {
580: // It's not a URL. Assume it's a path.
581:
582: File f = new File(configPath);
583:
584: if (!f.exists()) {
585: throw new CommandLineUsageException(
586: Constants.BUNDLE_NAME,
587: "Tool.badConfigPath",
588: "Configuration file argument \"{0}\" is not a "
589: + "valid URL and does not specify the path to an "
590: + "existing file.",
591: new Object[] { configPath });
592: }
593:
594: try {
595: configURL = f.toURI().toURL();
596: }
597:
598: catch (MalformedURLException ex2) {
599: throw new CommandLineUsageException(
600: Constants.BUNDLE_NAME, "Tool.badFileToURL",
601: "Cannot convert file \"{0}\" to a URL",
602: new Object[] { f }, ex2);
603: }
604: }
605:
606: return configURL;
607: }
608:
609: private Date parseDateTime(final String s)
610: throws CommandLineUsageException {
611: Date date = null;
612:
613: for (DateParseInfo dpi : DATE_FORMATS) {
614: try {
615: date = dpi.format.parse(s);
616: if (date != null) {
617: if (dpi.timeOnly) {
618: // The date pattern specified only a time, which
619: // means the date part defaulted to the epoch. Make
620: // it today, instead.
621:
622: Calendar cal = Calendar.getInstance();
623: Calendar calNow = Calendar.getInstance();
624:
625: calNow.setTime(new Date());
626: cal.setTime(date);
627: cal.set(calNow.get(Calendar.YEAR), calNow
628: .get(Calendar.MONTH), calNow
629: .get(Calendar.DAY_OF_MONTH));
630: date = cal.getTime();
631: }
632:
633: break;
634: }
635: }
636:
637: catch (ParseException ex) {
638: }
639: }
640:
641: if (date == null) {
642: throw new CommandLineUsageException(Constants.BUNDLE_NAME,
643: "Tool.badDateTime", "Bad date/time: \"{0}\"",
644: new Object[] { s });
645: }
646:
647: return date;
648: }
649:
650: private void showPlugIns() throws CurnException {
651: Map<String, Class> plugIns = new TreeMap<String, Class>();
652:
653: PlugInManager.listPlugIns(plugIns);
654:
655: System.out.println();
656: System.out.println("Plug-ins:");
657: System.out.println();
658: for (String name : plugIns.keySet()) {
659: System.out.println(name + " ("
660: + plugIns.get(name).getName() + ")");
661: }
662: }
663:
664: private void deprecatedOption(final char shortOption,
665: final String longOption) {
666: CurnUtil.getErrorOut().println(
667: "WARNING: Ignoring deprecated "
668: + UsageInfo.SHORT_OPTION_PREFIX + shortOption
669: + " (" + UsageInfo.LONG_OPTION_PREFIX
670: + longOption + ") command-line option.");
671: }
672: }
|