/* * GeoTools - The Open Source Java GIS Toolkit * http://geotools.org * * (C) 2004-2008, Open Source Geospatial Foundation (OSGeo) * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; * version 2.1 of the License. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. */ package org.geotools.parameter; import java.io.FilterWriter; import java.io.IOException; import java.io.Writer; import java.lang.reflect.Array; import java.text.DateFormat; import java.text.NumberFormat; import java.util.ArrayList; import java.util.Collection; import java.util.Date; import java.util.LinkedHashMap; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Set; import org.opengis.metadata.Identifier; import org.opengis.parameter.GeneralParameterDescriptor; import org.opengis.parameter.GeneralParameterValue; import org.opengis.parameter.ParameterDescriptor; import org.opengis.parameter.ParameterDescriptorGroup; import org.opengis.parameter.ParameterValue; import org.opengis.parameter.ParameterValueGroup; import org.opengis.referencing.IdentifiedObject; import org.opengis.referencing.operation.OperationMethod; import org.opengis.util.InternationalString; import org.opengis.util.GenericName; import org.geotools.io.TableWriter; import org.geotools.measure.Angle; import org.geotools.measure.AngleFormat; import org.geotools.resources.Arguments; import org.geotools.resources.Classes; import org.geotools.resources.XArray; import org.geotools.resources.i18n.Vocabulary; import org.geotools.resources.i18n.VocabularyKeys; /** * Format {@linkplain ParameterDescriptorGroup parameter descriptors} or * {@linkplain ParameterValueGroup parameter values} in a tabular format. * This writer assumes a monospaced font and an encoding capable to provide * drawing box characters (e.g. unicode). * * @since 2.1 * * @source $URL$ * @version $Id$ * @author Martin Desruisseaux */ public class ParameterWriter extends FilterWriter { /** * The locale. */ private Locale locale = Locale.getDefault(); /** * The formatter to use for numbers. Will be created only when first needed. */ private transient NumberFormat numberFormat; /** * The formatter to use for dates. Will be created only when first needed. */ private transient DateFormat dateFormat; /** * The formatter to use for angles. Will be created only when first needed. */ private transient AngleFormat angleFormat; /** * Creates a new formatter writting parameters to the * {@linkplain System#out default output stream}. */ public ParameterWriter() { this(Arguments.getWriter(System.out)); } /** * Creates a new formatter writting parameters to the specified output stream. */ public ParameterWriter(final Writer out) { super(out); } /** * Prints the elements of an operation to the * {@linkplain System#out default output stream}. * This is a convenience method for <code>new * ParameterWriter().{@linkplain #format(OperationMethod) format}(operation)</code>. */ public static void print(final OperationMethod operation) { final ParameterWriter writer = new ParameterWriter(); try { writer.format(operation); } catch (IOException exception) { // Should never happen, since we are writting to System.out. throw new AssertionError(exception); } } /** * Prints the elements of a descriptor group to the * {@linkplain System#out default output stream}. * This is a convenience method for <code>new * ParameterWriter().{@linkplain #format(ParameterDescriptorGroup) * format}(descriptor)</code>. */ public static void print(final ParameterDescriptorGroup descriptor) { final ParameterWriter writer = new ParameterWriter(); try { writer.format(descriptor); } catch (IOException exception) { // Should never happen, since we are writting to System.out. throw new AssertionError(exception); } } /** * Prints the elements of a parameter group to the * {@linkplain System#out default output stream}. * This is a convenience method for <code>new * ParameterWriter().{@linkplain #format(ParameterValueGroup) * format}(values)</code>. */ public static void print(final ParameterValueGroup values) { final ParameterWriter writer = new ParameterWriter(); try { writer.format(values); } catch (IOException exception) { // Should never happen, since we are writting to System.out. throw new AssertionError(exception); } } /** * Prints the elements of an operation to the output stream. * * @param operation The operation method to format. * @throws IOException if an error occured will writing to the stream. */ public void format(final OperationMethod operation) throws IOException { synchronized (lock) { format(operation.getName().getCode(), operation.getParameters(), null); } } /** * Prints the elements of a descriptor group to the output stream. * * @param descriptor The descriptor group to format. * @throws IOException if an error occured will writing to the stream. */ public void format(final ParameterDescriptorGroup descriptor) throws IOException { synchronized (lock) { format(descriptor.getName().getCode(), descriptor, null); } } /** * Prints the elements of a parameter group to the output stream. * * @param values The parameter group to format. * @throws IOException if an error occured will writing to the stream. */ public void format(final ParameterValueGroup values) throws IOException { final ParameterDescriptorGroup descriptor = values.getDescriptor(); synchronized (lock) { format(descriptor.getName().getCode(), descriptor, values); } } /** * Implementation of public {@code format} methods. * * @param name The group name, usually {@code descriptor.getCode().getName()}. * @param descriptor The parameter descriptor. Should be equals to * {@code values.getDescriptor()} if {@code values} is non null. * @param values The parameter values, or {@code null} if none. * @throws IOException if an error occured will writing to the stream. */ private void format(final String name, final ParameterDescriptorGroup group, final ParameterValueGroup values) throws IOException { /* * Write the operation name (including aliases) before the table. */ final String lineSeparator = System.getProperty("line.separator", "\n"); out.write(' '); out.write(name); out.write(lineSeparator); Collection<GenericName> alias = group.getAlias(); if (alias != null) { boolean first = true; for (final GenericName a : alias) { out.write(first ? " alias " : " "); out.write(a.toInternationalString().toString(locale)); out.write(lineSeparator); first = false; } } /* * Format the table header (i.e. column names). */ final Vocabulary resources = Vocabulary.getResources(locale); final TableWriter table = new TableWriter(out, TableWriter.SINGLE_VERTICAL_LINE); table.setMultiLinesCells(true); table.writeHorizontalSeparator(); table.write(resources.getString(VocabularyKeys.NAME)); table.nextColumn(); table.write(resources.getString(VocabularyKeys.CLASS)); table.nextColumn(); table.write("Minimum"); // TODO localize table.nextColumn(); table.write("Maximum"); // TODO localize table.nextColumn(); table.write(resources.getString((values==null) ? VocabularyKeys.DEFAULT_VALUE : VocabularyKeys.VALUE)); table.nextColumn(); table.write("Units"); // TODO localize table.nextLine(); table.nextLine(TableWriter.DOUBLE_HORIZONTAL_LINE); /* * Format each element in the parameter group. If values were supplied, we will * iterate through the values instead of the descriptor. We do it that way because * the descriptor can't know which optional values are included and which one are * omitted. */ List<Object> deferredGroups = null; final Object[] array1 = new Object[1]; final Collection<?> elements = (values!=null) ? values.values() : group.descriptors(); for (final Object element : elements) { final GeneralParameterValue generalValue; final GeneralParameterDescriptor generalDescriptor; if (values != null) { generalValue = (GeneralParameterValue) element; generalDescriptor = generalValue.getDescriptor(); } else { generalValue = null; generalDescriptor = (GeneralParameterDescriptor) element; } /* * If the current element is a group, we will format it later (after * all ordinary elements) in order avoid breaking the table layout. */ if (generalDescriptor instanceof ParameterDescriptorGroup) { if (deferredGroups == null) { deferredGroups = new ArrayList<Object>(); } deferredGroups.add(element); continue; } /* * Format the element name, including all alias (if any). * Each alias will be formatted on its own line. */ final Identifier identifier = generalDescriptor.getName(); table.write(identifier.getCode()); alias = generalDescriptor.getAlias(); if (alias != null) { for (final GenericName a : alias) { if (!identifier.equals(a)) { table.write(lineSeparator); table.write(a.tip().toInternationalString().toString(locale)); } } } table.nextColumn(); /* * Format the current element as an ordinary descriptor. If we are iterating * over the descriptors rather than values, then the "value" column will be * filled with the default value specified in descriptors. */ if (generalDescriptor instanceof ParameterDescriptor) { final ParameterDescriptor descriptor = (ParameterDescriptor) generalDescriptor; table.write(Classes.getShortName(descriptor.getValueClass())); table.nextColumn(); table.setAlignment(TableWriter.ALIGN_RIGHT); Object value = descriptor.getMinimumValue(); if (value != null) { table.write(formatValue(value)); } table.nextColumn(); value = descriptor.getMaximumValue(); if (value != null) { table.write(formatValue(value)); } table.nextColumn(); if (generalValue != null) { value = ((ParameterValue) generalValue).getValue(); } else { value = descriptor.getDefaultValue(); } /* * Wraps the value in an array. Because it may be an array of primitive * type, we can't cast to Object[]. Then, each array's element will be * formatted on its own line. */ final Object array; if (value!=null && value.getClass().isArray()) { array = value; } else { array = array1; array1[0] = value; } final int length = Array.getLength(array); for (int i=0; i<length; i++) { value = Array.get(array, i); if (value != null) { if (i != 0) { table.write(lineSeparator); } table.write(formatValue(value)); } } table.nextColumn(); table.setAlignment(TableWriter.ALIGN_LEFT); value = descriptor.getUnit(); if (value != null) { table.write(value.toString()); } } table.writeHorizontalSeparator(); } table.flush(); /* * Now format all groups deferred to the end of this table. * Most of the time, there is no such group. */ if (deferredGroups != null) { for (final Object element : deferredGroups) { final ParameterValueGroup value; final ParameterDescriptorGroup descriptor; if (element instanceof ParameterValueGroup) { value = (ParameterValueGroup) element; descriptor = value.getDescriptor(); } else { value = null; descriptor = (ParameterDescriptorGroup) element; } out.write(lineSeparator); format(name + '/' + descriptor.getName().getCode(), descriptor, value); } } } /** * Formats a summary of a collection of {@linkplain IdentifiedObject identified objects}. * The summary contains the identifier name and alias aligned in a table. * * @param parameters The collection of parameters to format. * @param scopes The set of scopes to include in the table, of {@code null} for all * of them. A restricted a set will produce a table with less columns. * @throws IOException if an error occured will writing to the stream. */ public void summary(final Collection<? extends IdentifiedObject> parameters, final Set<String> scopes) throws IOException { /* * Prepares the list of alias before any write to the output stream. * We need to prepare the list first, because not all identified objects * may have generic names with the same scopes in the same order. * * titles - The column number for each column title. * names - The names (including alias) for each line. */ final Map<Object,Integer> titles = new LinkedHashMap<Object,Integer>(); final List<String[]> names = new ArrayList<String[]>(); final Locale locale = this.locale; // Protect from changes. String[] descriptions = null; titles.put(null, 0); // Special value for the identifier column. for (final IdentifiedObject element : parameters) { final Collection<GenericName> aliases = element.getAlias(); String[] elementNames = new String[titles.size()]; elementNames[0] = element.getName().getCode(); if (aliases != null) { /* * The primary name has been fetch (before this block) for one element, and we * determined that some alias may be available in addition. Add local alias * (i.e. names without their scope) to the 'elementNames' row. */ int count = 0; for (final GenericName alias : aliases) { final GenericName scope = alias.scope().name(); final GenericName name = alias.tip(); final Object title; if (scope != null) { if (scopes!=null && !scopes.contains(scope.toString())) { /* * The user requested only a subset of alias (the 'scopes' argument), * and the current alias is not a member of this subset. Continue the * search to other alias. */ continue; } title = scope.toInternationalString().toString(locale); } else { title = count++; } /* * The alias scope is used as the column's title. If the alias has no scope, * then a sequencial number is used instead. Now check if the column already * exists. If it exists, fetch its position. If it doesn't exist, inserts the * new column at the end of existing columns. */ Integer position = titles.get(title); if (position == null) { position = titles.size(); titles.put(title, position); } /* * Now stores the alias local name at the position we just determined above. * Note that more than one value may exist for the same column. For example * both "WGS 84" and "4326" may appear as EPSG alias (as EPSG name and EPSG * identifier respectively), depending how the parameters given by the user * were constructed. */ final int index = position.intValue(); if (index >= elementNames.length) { elementNames = XArray.resize(elementNames, index+1); } final String oldName = elementNames[index]; final String newName = name.toInternationalString().toString(locale); if (oldName==null || oldName.length()>newName.length()) { /* * Keep the shortest string, since it is often a code used * for identification (e.g. EPSG code). It also help to fit * the table in the window's width. */ elementNames[index] = newName; } } } /* * Before to add the name and alias to the list, fetch the remarks (if any). * They are stored in a separated list and will appear as the very last column. */ final InternationalString remarks = element.getRemarks(); if (remarks != null) { if (descriptions == null) { descriptions = new String[parameters.size()]; } descriptions[names.size()] = remarks.toString(locale); } names.add(elementNames); } /* * Trim the columns that duplicates the identifier column (#0). This is * usually the case of the OGC column (usually #1), since we already use * OGC name as the main identifier in most cases. */ final boolean[] hide = new boolean[titles.size()]; trim: for (int column=hide.length; --column>=1;) { for (final String[] alias : names) { if (alias.length > column) { final String name = alias[column]; if (name!=null && !name.equals(alias[0])) { // No need to looks at the next lines. // Move to previous column. continue trim; } } } // A column duplicating the identifier column has been found. hide[column] = true; } /* * Writes the table. The header will contains one column for each alias's * scope (or authority) declared in 'titles', in the same order. It will * also contains a "Description" column if there is some. */ int column = 0; synchronized (lock) { final TableWriter table = new TableWriter(out, TableWriter.SINGLE_VERTICAL_LINE); table.setMultiLinesCells(true); table.writeHorizontalSeparator(); /* * Writes all column headers. */ for (final Object element : titles.keySet()) { if (hide[column++]) { continue; } final String title; if (element == null) { title = "Identifier"; // TODO: localize } else if (element instanceof String) { title = (String) element; } else { title = "Alias " + element; // TODO: localize } table.write(title); table.nextColumn(); } if (descriptions != null) { table.write("Description"); // TODO: localize } table.writeHorizontalSeparator(); /* * Writes all row. */ int counter = 0; for (final String[] aliases : names) { for (column=0; column<hide.length; column++) { if (hide[column]) { continue; } if (column < aliases.length) { final String alias = aliases[column]; if (alias != null) { table.write(alias); } } table.nextColumn(); } if (descriptions != null) { final String remarks = descriptions[counter++]; if (remarks != null) { table.write(remarks); } } table.nextLine(); } table.writeHorizontalSeparator(); table.flush(); } } /** * Returns the current locale. Newly constructed {@code ParameterWriter} * use the {@linkplain Locale#getDefault system default}. */ public Locale getLocale() { return locale; } /** * Set the locale to use for table formatting. */ public void setLocale(final Locale locale) { synchronized (lock) { this.locale = locale; numberFormat = null; dateFormat = null; angleFormat = null; } } /** * Format the specified value as a string. This method is automatically invoked * by {@code format(...)} methods. The default implementation format * {@link Number}, {@link Date} and {@link Angle} object according the * {@linkplain #getLocale current locale}. This method can been overridden if * more objects need to be formatted in a special way. * * @param value the value to format. * @return The value formatted as a string. */ protected String formatValue(final Object value) { if (value instanceof Number) { if (numberFormat == null) { numberFormat = NumberFormat.getNumberInstance(locale); } return numberFormat.format(value); } if (value instanceof Date) { if (dateFormat == null) { dateFormat = DateFormat.getDateInstance(DateFormat.MEDIUM, locale); } return dateFormat.format(value); } if (value instanceof Angle) { if (angleFormat == null) { angleFormat = AngleFormat.getInstance(locale); } return angleFormat.format(value); } return String.valueOf(value); } }