Java tutorial
/* * Copyright 2008 Google Inc. * * 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.google.feedserver.util; import com.google.feedserver.client.FeedServerEntry; import com.google.gdata.data.OtherContent; import org.apache.commons.cli.CommandLine; import org.apache.commons.cli.GnuParser; import org.apache.commons.cli.HelpFormatter; import org.apache.commons.cli.Option; import org.apache.commons.cli.Options; import org.apache.commons.cli.ParseException; import org.xml.sax.SAXException; import java.beans.IntrospectionException; import java.io.BufferedInputStream; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.lang.reflect.Field; import java.lang.reflect.InvocationTargetException; import java.util.ArrayList; import java.util.List; import javax.xml.parsers.ParserConfigurationException; /** * Uses reflection to scan registered beans for any annotation containing * {@link Flag} or {@link ConfigFile}. It uses the "help" entry in the Flag * decorator for flag help. In addition if you specify @ConfigFile with @Flag, * this flag will be used to read an XML file from the filesystem containing a * payload-in-content entry for this bean. It will read the file first then * reads the flags. Flags supersede file content. With this class you can create * configuration files using payload-in-content files and have them represented * as Beans for consumption in code. * <p> * <br/> Boolean, Integer and String fields are supported. * * <br/> example: to setup --filename as a flag. in class Foo with flag help. * </p> * <p> * <blockquote> * * <pre> * * public class FooBean { * {@literal @}Flag(help = "config file for foobean") * {@literal @}ConfigFile * private String configFileName = "/etc/configfile"; * {@literal @}Flag(help = "filename to access") * private String filename = "/tmp/defaultfile"; * public getFilename() { * return filename; * } * * public setFilename(String filename) { * this.filename = filename; * } * } * * public class Foo { * public static void main(String[] args) { * FooBean fooBean = new FooBean(); * BeanCliHelper cliHelper = new BeanCliHelper(); * cliHelper.register(fooBean); cliHelper.parse(args); * System.stdout.println("Filename is " + fooBean.getFilename()); * } * } * * </pre> * * </blockquote> * </p> * * Note: classes with identical flag field names will get overwritten with the * last one processed. TODO(rayc) support collision detection and use fully * qualified class name for flag option. * * @author r@kuci.org (Ray Colline) * */ public class BeanCliHelper { // We allow tests to set the file contents to avoid going to disk private static String testFileContents; private List<Object> beans; private CommandLine flags; private Options options; public BeanCliHelper() { beans = new ArrayList<Object>(); } public void register(Object bean) { beans.add(bean); } /** * With provided command line string, populates all registered classes with * decorated fields with their config file then command-line values. * * @param args command-line. */ public void parse(String[] args) throws ConfigurationBeanException, ParseException, java.text.ParseException { options = createOptions(); GnuParser parser = new GnuParser(); try { flags = parser.parse(options, args); } catch (ParseException e) { usage(); throw new ConfigurationBeanException("Command line parse error.", e); } /* * Print help text and exit. */ if (flags.hasOption("help")) { usage(); System.exit(0); } populateBeansFromConfigFile(); populateBeansFromCommandLine(); } /** * Prints usage information. */ public void usage() { new HelpFormatter().printHelp("Usage", options); } /** * Must be called after {@link GnuParser#parse(Options, String[])} and if bean * has {@link ConfigFile} decorator load the file that field points to and * populate bean. If no decorator is present we just no-op. If there is not a * default location for the config file in the bean, or they did not specify * the flag on the commandline, we no-op and assume they want to configure * entirely from defaults or commandline. */ private void populateBeansFromConfigFile() throws ConfigurationBeanException, ParseException, java.text.ParseException { populateBeansFromCommandLine(); // we do an initial pass to pick up // ConfigFile flag // We go through all registered beans. for (Object bean : beans) { // Go through each field to find annotated fields. We only support one // ConfigFile per // bean. The first one encountered here will be used. for (Field field : bean.getClass().getDeclaredFields()) { ConfigFile configFileAnnotation = field.getAnnotation(ConfigFile.class); String configFileName = ""; if (configFileAnnotation == null) { // no config file in this bean, we no-op. continue; } try { // retrieve the field value configFileName = (String) bean.getClass().getMethod( "get" + field.getName().substring(0, 1).toUpperCase() + field.getName().substring(1), (Class[]) null).invoke(bean, (Object[]) null); if (configFileName == null) { return; // There is no config file default or flag entry, we no-op. } // load the file into a string. String configFileContents = readFileIntoString(configFileName); // parse the contents into the bean. FeedServerEntry configEntry = new FeedServerEntry(configFileContents); ContentUtil contentUtil = new ContentUtil(); contentUtil.fillBean((OtherContent) configEntry.getContent(), bean); // BeanUtil throws many exceptions, re-wrap them and throw our // exception. } catch (RuntimeException e) { throw new ConfigurationBeanException(e); } catch (IllegalAccessException e) { throw new ConfigurationBeanException(e); } catch (InvocationTargetException e) { throw new ConfigurationBeanException(e); } catch (NoSuchMethodException e) { throw new ConfigurationBeanException(e); } catch (IOException e) { throw new ConfigurationBeanException("Error reading config file " + configFileName, e); } catch (IntrospectionException e) { throw new ConfigurationBeanException(e); } catch (SAXException e) { throw new ConfigurationBeanException(e); } catch (ParserConfigurationException e) { throw new ConfigurationBeanException(e); } } } } /** * Loop through each registered class and create and parse command line * options for fields decorated with {@link Flag}. */ private void populateBeansFromCommandLine() { // Go through all registered beans. for (Object bean : beans) { // Search for all fields in the bean with Flag decorator. for (Field field : bean.getClass().getDeclaredFields()) { Flag flag = field.getAnnotation(Flag.class); if (flag == null) { // not decorated, continue. continue; } String argName = field.getName(); // Boolean Flags if (field.getType().getName().equals(Boolean.class.getName()) || field.getType().getName().equals(Boolean.TYPE.getName())) { if (flags.hasOption(argName)) { setField(field, bean, new Boolean(true)); } else if (flags.hasOption("no" + argName)) { setField(field, bean, new Boolean(false)); } // Integer Flags } else if (field.getType().getName().equals(Integer.class.getName()) || field.getType().getName().equals(Integer.TYPE.getName())) { String argValue = flags.getOptionValue(argName, null); if (argValue != null) { try { setField(field, bean, Integer.valueOf(argValue)); } catch (NumberFormatException e) { throw new RuntimeException(e); } } // String Flag } else if (field.getType().getName().equals(String.class.getName())) { String argValue = flags.getOptionValue(argName, null); if (argValue != null) { setField(field, bean, argValue); } // Repeated String Flag } else if (field.getType().getName().equals(String[].class.getName())) { String[] argValues = flags.getOptionValues(argName); if (argValues != null) { setField(field, bean, argValues); } } } } } /** * Sets value in the supplied field's setter to the given value. * * @param field the flag field. * @param value the value, usually a Boolean or a String. * @throws RuntimeException if the field is mis-configured. */ private void setField(Field field, Object bean, Object value) { try { bean.getClass() .getMethod("set" + field.getName().substring(0, 1).toUpperCase() + field.getName().substring(1), field.getType()) .invoke(bean, value); // Lots of errors can happen when using introspection. Most are // programming errors // so we throw RuntimeExceptions. } catch (IllegalArgumentException e) { throw new RuntimeException("Field error:" + field.getName(), e); } catch (IllegalAccessException e) { throw new RuntimeException("Field error:" + field.getName(), e); } catch (NullPointerException e) { throw new RuntimeException("Field error:" + field.getName(), e); } catch (SecurityException e) { throw new RuntimeException("Field error:" + field.getName(), e); } catch (NoSuchMethodException e) { throw new RuntimeException("Field error:" + field.getName(), e); } catch (InvocationTargetException e) { throw new RuntimeException("Field error:" + field.getName(), e); } } /** * For each class registered, we extract options based on the {@link Flag} * decorator set on each field in the bean. We use the help argument on the * decorator to set command line option usage text. * * Note: classes with identical flag field names will get overwritten with the * last one processed. TODO(rayc) support collision detection and use fully * qualified class name for flag option. * * see {@link Flag} for more info. * * @return Options all command-line options registered for parsing. */ private Options createOptions() { Options options = new Options(); options.addOption(new Option("help", false, "Print out usage.")); // Go through all registered beans. for (Object bean : beans) { // Go through all fields. for (Field field : bean.getClass().getDeclaredFields()) { Flag flag = field.getAnnotation(Flag.class); if (flag == null) { continue; // no decorator we move on. } // Check type we only support boolean, String and Integer. if ((field.getType() != Integer.class) && (field.getType() != Integer.TYPE) && (field.getType() != String.class) && (field.getType() != Boolean.class) && (field.getType() != Boolean.TYPE)) { throw new RuntimeException("Field: " + field.getName() + " flag type not supported"); } // Create options. String argName = field.getName(); if (field.getType().getName().equals(Boolean.class.getName()) || field.getType().getName().equals(Boolean.TYPE.getName())) { options.addOption(new Option(argName, false, flag.help())); options.addOption(new Option("no" + argName, false, flag.help())); } else { options.addOption(new Option(argName, true, flag.help())); } } } return options; } /** * Helper that loads a file into a string. * * @param fileName properties file with rules configuration. * @returns a String representing the file. * @throws IOException if any errors are encountered reading the properties * file */ static String readFileIntoString(final String fileName) throws IOException { if (testFileContents != null) { return testFileContents; } File file = new File(fileName); byte[] fileContents = new byte[(int) file.length()]; new BufferedInputStream(new FileInputStream(fileName)).read(fileContents); return new String(fileContents); } /** * We use this to set the config file for unit tests. * * @param contents a test payload-in-content file represented as a string. */ static void setTestFileContents(String contents) { testFileContents = contents; } }