| /* |
| * Licensed to the Apache Software Foundation (ASF) under one or more |
| * contributor license agreements. See the NOTICE file distributed with |
| * this work for additional information regarding copyright ownership. |
| * The ASF licenses this file to You 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 org.apache.sis.feature; |
| |
| import java.util.List; |
| import java.util.ArrayList; |
| import java.util.Set; |
| import java.util.EnumSet; |
| import java.util.Iterator; |
| import java.util.Collection; |
| import java.util.Locale; |
| import java.util.TimeZone; |
| import java.util.concurrent.atomic.AtomicReference; |
| import java.io.IOException; |
| import java.io.UncheckedIOException; |
| import java.text.Format; |
| import java.text.FieldPosition; |
| import java.text.ParsePosition; |
| import java.text.ParseException; |
| import org.opengis.referencing.IdentifiedObject; |
| import org.opengis.util.InternationalString; |
| import org.opengis.util.GenericName; |
| import org.apache.sis.io.TableAppender; |
| import org.apache.sis.io.TabularFormat; |
| import org.apache.sis.util.Deprecable; |
| import org.apache.sis.util.Characters; |
| import org.apache.sis.util.CharSequences; |
| import org.apache.sis.util.ArgumentChecks; |
| import org.apache.sis.util.logging.Logging; |
| import org.apache.sis.util.resources.Errors; |
| import org.apache.sis.util.resources.Vocabulary; |
| import org.apache.sis.util.privy.Strings; |
| import org.apache.sis.util.privy.CollectionsExt; |
| import org.apache.sis.geometry.wrapper.Geometries; |
| import org.apache.sis.geometry.wrapper.GeometryWrapper; |
| import org.apache.sis.referencing.IdentifiedObjects; |
| import org.apache.sis.math.MathFunctions; |
| |
| // Specific to the geoapi-3.1 and geoapi-4.0 branches: |
| import org.opengis.feature.IdentifiedType; |
| import org.opengis.feature.Property; |
| import org.opengis.feature.PropertyType; |
| import org.opengis.feature.Attribute; |
| import org.opengis.feature.AttributeType; |
| import org.opengis.feature.Feature; |
| import org.opengis.feature.FeatureType; |
| import org.opengis.feature.FeatureAssociation; |
| import org.opengis.feature.FeatureAssociationRole; |
| import org.opengis.feature.Operation; |
| |
| |
| /** |
| * Formats {@linkplain AbstractFeature features} or {@linkplain DefaultFeatureType feature types} in a tabular format. |
| * This format assumes a monospaced font and an encoding supporting drawing box characters (e.g. UTF-8). |
| * |
| * <h2>Example</h2> |
| * A feature named “City” and containing 3 properties (“name”, “population” and “twin town”) |
| * may be formatted like below. The two first properties are {@linkplain AbstractAttribute attributes} |
| * while the last property is an {@linkplain AbstractAssociation association} to another feature. |
| * |
| * <pre class="text"> |
| * City |
| * ┌────────────┬─────────┬──────────────┬───────────┐ |
| * │ Name │ Type │ Multiplicity │ Value │ |
| * ├────────────┼─────────┼──────────────┼───────────┤ |
| * │ name │ String │ [1 … 1] │ Paderborn │ |
| * │ population │ Integer │ [1 … 1] │ 143,174 │ |
| * │ twin town │ City │ [0 … ∞] │ Le Mans │ |
| * └────────────┴─────────┴──────────────┴───────────┘</pre> |
| * |
| * <h2>Limitations</h2> |
| * <ul> |
| * <li>The current implementation can only format features — parsing is not yet implemented.</li> |
| * <li>{@code FeatureFormat}, like most {@code java.text.Format} subclasses, is not thread-safe.</li> |
| * </ul> |
| * |
| * @author Martin Desruisseaux (Geomatys) |
| * @version 1.4 |
| * @since 0.5 |
| */ |
| public class FeatureFormat extends TabularFormat<Object> { |
| /** |
| * For cross-version compatibility. |
| */ |
| private static final long serialVersionUID = -5792086817264884947L; |
| |
| /** |
| * The separator to use in comma-separated lists. |
| */ |
| private static final String SEPARATOR = ", "; |
| |
| /** |
| * An instance created when first needed and potentially shared. |
| */ |
| private static final AtomicReference<FeatureFormat> INSTANCE = new AtomicReference<>(); |
| |
| /** |
| * The locale for international strings. |
| */ |
| private final Locale displayLocale; |
| |
| /** |
| * The columns to include in the table formatted by this {@code FeatureFormat}. |
| * By default, all columns having at least one value are included. |
| */ |
| private final EnumSet<Column> columns = EnumSet.allOf(Column.class); |
| |
| /** |
| * Maximal length of attribute values, in number of characters. |
| * If a value is longer than this length, it will be truncated. |
| * |
| * <p>This is defined as a static final variable for now because its value is approximate: |
| * it is a number of characters instead of a number of code points, and that length may be |
| * exceeded by a few characters if the overflow happen while appending the list separator.</p> |
| */ |
| private static final int MAXIMAL_VALUE_LENGTH = 40; |
| |
| /** |
| * The bit patterns of the last {@link Float#NaN} value for which {@link MathFunctions#toNanOrdinal(float)} could |
| * not get the ordinal value. We use this information for avoiding flooding the logger with the same message. |
| */ |
| private transient int illegalNaN; |
| |
| /** |
| * Creates a new formatter for the default locale and timezone. |
| */ |
| public FeatureFormat() { |
| super(Locale.getDefault(Locale.Category.FORMAT), TimeZone.getDefault()); |
| displayLocale = Locale.getDefault(Locale.Category.DISPLAY); |
| columnSeparator = " │ "; |
| } |
| |
| /** |
| * Creates a new formatter for the given locale and timezone. |
| * |
| * @param locale the locale, or {@code null} for {@code Locale.ROOT}. |
| * @param timezone the timezone, or {@code null} for UTC. |
| */ |
| public FeatureFormat(final Locale locale, final TimeZone timezone) { |
| super(locale, timezone); |
| displayLocale = (locale != null) ? locale : Locale.ROOT; |
| columnSeparator = " │ "; |
| } |
| |
| /** |
| * Returns the type of objects formatted by this class. This method has to return {@code Object.class} |
| * since it is the only common parent to {@link Feature} and {@link FeatureType}. |
| * |
| * @return {@code Object.class} |
| */ |
| @Override |
| public final Class<Object> getValueType() { |
| return Object.class; |
| } |
| |
| /** |
| * Returns the locale for the given category. |
| * |
| * <ul> |
| * <li>{@link java.util.Locale.Category#FORMAT} specifies the locale to use for values.</li> |
| * <li>{@link java.util.Locale.Category#DISPLAY} specifies the locale to use for labels.</li> |
| * </ul> |
| * |
| * @param category the category for which a locale is desired. |
| * @return the locale for the given category (never {@code null}). |
| */ |
| @Override |
| public Locale getLocale(final Locale.Category category) { |
| return (category == Locale.Category.DISPLAY) ? displayLocale : super.getLocale(category); |
| } |
| |
| /** |
| * Returns all columns that may be shown in the tables to format. |
| * The columns included in the set may be shown, but not necessarily; |
| * some columns will still be omitted if they are completely empty. |
| * However, columns <em>not</em> included in the set are guaranteed to be omitted. |
| * |
| * @return all columns that may be shown in the tables to format. |
| * |
| * @since 0.8 |
| */ |
| public Set<Column> getAllowedColumns() { |
| return columns.clone(); |
| } |
| |
| /** |
| * Sets all columns that may be shown in the tables to format. |
| * Note that the columns specified to this method are not guaranteed to be shown; |
| * some columns will still be omitted if they are completely empty. |
| * |
| * @param inclusion all columns that may be shown in the tables to format. |
| * |
| * @since 0.8 |
| */ |
| public void setAllowedColumns(final Set<Column> inclusion) { |
| ArgumentChecks.ensureNonNull("inclusion", inclusion); |
| columns.clear(); |
| columns.addAll(inclusion); |
| } |
| |
| /** |
| * Identifies the columns to include in the table formatted by {@code FeatureFormat}. |
| * By default, all columns having at least one non-null value are shown. But a smaller |
| * set of columns can be specified to the {@link FeatureFormat#setAllowedColumns(Set)} |
| * method for formatting narrower tables. |
| * |
| * @see FeatureFormat#setAllowedColumns(Set) |
| * |
| * @since 0.8 |
| */ |
| public enum Column { |
| /** |
| * Natural language designator for the property. |
| * This is the character sequence returned by {@link PropertyType#getDesignation()}. |
| * This column is omitted if no property has a designation. |
| */ |
| DESIGNATION(Vocabulary.Keys.Designation), |
| |
| /** |
| * Name of the property. |
| * This is the character sequence returned by {@link PropertyType#getName()}. |
| */ |
| NAME(Vocabulary.Keys.Name), |
| |
| /** |
| * Type of property values. This is the type returned by {@link AttributeType#getValueClass()} or |
| * {@link FeatureAssociationRole#getValueType()}. |
| */ |
| TYPE(Vocabulary.Keys.Type), |
| |
| /** |
| * Cardinality (for attributes) or multiplicity (for attribute types). |
| * The cardinality is the actual number of attribute values. |
| * The multiplicity is the minimum and maximum occurrences of attribute values. |
| * The multiplicity is made from the numbers returned by {@link AttributeType#getMinimumOccurs()} |
| * and {@link AttributeType#getMaximumOccurs()}. |
| */ |
| CARDINALITY(Vocabulary.Keys.Cardinality), |
| |
| /** |
| * Property value (for properties) or default value (for property types). |
| * This is the value returned by {@link Attribute#getValue()}, {@link FeatureAssociation#getValue()} |
| * or {@link AttributeType#getDefaultValue()}. |
| */ |
| VALUE(Vocabulary.Keys.Value), |
| |
| /** |
| * Other attributes that describes the attribute. |
| * This is made from the map returned by {@link Attribute#characteristics()}. |
| * This column is omitted if no property has characteristics. |
| */ |
| CHARACTERISTICS(Vocabulary.Keys.Characteristics), |
| |
| /** |
| * Whether a property is deprecated, or other remarks. |
| * This column is omitted if no property has remarks. |
| */ |
| REMARKS(Vocabulary.Keys.Remarks); |
| |
| /** |
| * The {@link Vocabulary} key to use for formatting the header of this column. |
| */ |
| final short resourceKey; |
| |
| /** |
| * Creates a new column enumeration constant. |
| */ |
| private Column(final short key) { |
| resourceKey = key; |
| } |
| } |
| |
| /** |
| * Invoked when the formatter needs to move to the next column. |
| */ |
| private void nextColumn(final TableAppender table) { |
| table.append(beforeFill); |
| table.nextColumn(fillCharacter); |
| } |
| |
| /** |
| * Formats the given object to the given stream of buffer. |
| * The object may be an instance of any of the following types: |
| * |
| * <ul> |
| * <li>{@link Feature}</li> |
| * <li>{@link FeatureType}</li> |
| * </ul> |
| * |
| * @throws IOException if an error occurred while writing to the given appendable. |
| */ |
| @Override |
| public void format(final Object object, final Appendable toAppendTo) throws IOException { |
| ArgumentChecks.ensureNonNull("object", object); |
| ArgumentChecks.ensureNonNull("toAppendTo", toAppendTo); |
| /* |
| * Separate the Feature (optional) and the FeatureType (mandatory) instances. |
| */ |
| final FeatureType featureType; |
| final Feature feature; |
| if (object instanceof Feature) { |
| feature = (Feature) object; |
| featureType = feature.getType(); |
| } else if (object instanceof FeatureType) { |
| featureType = (FeatureType) object; |
| feature = null; |
| } else { |
| throw new IllegalArgumentException(Errors.forLocale(displayLocale) |
| .getString(Errors.Keys.UnsupportedType_1, object.getClass())); |
| } |
| /* |
| * Computes the columns to show. We start with the set of columns specified by setAllowedColumns(Set), |
| * then we check if some of those columns are empty. For example, in many cases there are no attributes |
| * with characteritic, in which case we will ommit the whole "characteristics" column. We perform such |
| * check only for optional information, not for mandatory information like property names. |
| */ |
| final EnumSet<Column> visibleColumns = columns.clone(); |
| { |
| boolean hasDesignation = false; |
| boolean hasCharacteristics = false; |
| boolean hasDeprecatedTypes = false; |
| for (final PropertyType propertyType : featureType.getProperties(true)) { |
| if (!hasDesignation) { |
| hasDesignation = propertyType.getDesignation().isPresent(); |
| } |
| if (!hasCharacteristics && propertyType instanceof AttributeType<?>) { |
| hasCharacteristics = !((AttributeType<?>) propertyType).characteristics().isEmpty(); |
| } |
| if (!hasDeprecatedTypes && propertyType instanceof Deprecable) { |
| hasDeprecatedTypes = ((Deprecable) propertyType).isDeprecated(); |
| } |
| } |
| if (!hasDesignation) visibleColumns.remove(Column.DESIGNATION); |
| if (!hasCharacteristics) visibleColumns.remove(Column.CHARACTERISTICS); |
| if (!hasDeprecatedTypes) visibleColumns.remove(Column.REMARKS); |
| } |
| /* |
| * Format the feature type name. In the case of feature type, format also the names of super-type |
| * after the UML symbol for inheritance (an arrow with white head). We do not use the " : " ASCII |
| * character for avoiding confusion with the ":" separator in namespaces. After the feature (type) |
| * name, format the column header: property name, type, cardinality and (default) value. |
| */ |
| toAppendTo.append(toString(featureType.getName())); |
| if (feature == null) { |
| String separator = " ⇾ "; // UML symbol for inheritance. |
| for (final FeatureType parent : featureType.getSuperTypes()) { |
| toAppendTo.append(separator).append(toString(parent.getName())); |
| separator = SEPARATOR; |
| } |
| final InternationalString definition = featureType.getDefinition(); |
| if (definition != null) { |
| final String text = Strings.trimOrNull(definition.toString(displayLocale)); |
| if (text != null) { |
| toAppendTo.append(getLineSeparator()).append(text); |
| } |
| } |
| } |
| toAppendTo.append(getLineSeparator()); |
| /* |
| * Create a table and format the header. Columns will be shown in Column enumeration order. |
| */ |
| final Vocabulary resources = Vocabulary.forLocale(displayLocale); |
| final TableAppender table = new TableAppender(toAppendTo, columnSeparator); |
| table.setMultiLinesCells(true); |
| table.nextLine('─'); |
| boolean isFirstColumn = true; |
| for (final Column column : visibleColumns) { |
| short key = column.resourceKey; |
| if (feature == null) { |
| if (key == Vocabulary.Keys.Cardinality) key = Vocabulary.Keys.Multiplicity; |
| if (key == Vocabulary.Keys.Value) key = Vocabulary.Keys.DefaultValue; |
| } |
| if (!isFirstColumn) nextColumn(table); |
| table.append(resources.getString(key)); |
| isFirstColumn = false; |
| } |
| table.nextLine(); |
| table.nextLine('─'); |
| /* |
| * Done writing the header. Now write all property rows. For each row, the first part in the loop |
| * extracts all information needed without formatting anything yet. If we detect in that part that |
| * a row has no value, it will be skipped if and only if that row is optional (minimum occurrence |
| * of zero). |
| */ |
| final StringBuffer buffer = new StringBuffer(); |
| final FieldPosition dummyFP = new FieldPosition(-1); |
| final List<String> remarks = new ArrayList<>(); |
| for (final PropertyType propertyType : featureType.getProperties(true)) { |
| Object value = null; |
| int cardinality = -1; |
| if (feature != null) { |
| if (!(propertyType instanceof AttributeType<?>) && |
| !(propertyType instanceof FeatureAssociationRole) && |
| !DefaultFeatureType.isParameterlessOperation(propertyType)) |
| { |
| continue; |
| } |
| value = feature.getPropertyValue(propertyType.getName().toString()); |
| if (value == null) { |
| if (propertyType instanceof AttributeType<?> |
| && ((AttributeType<?>) propertyType).getMinimumOccurs() == 0 |
| && ((AttributeType<?>) propertyType).characteristics().isEmpty()) |
| { |
| continue; // If optional, no value and no characteristics, skip the full row. |
| } |
| if (propertyType instanceof FeatureAssociationRole |
| && ((FeatureAssociationRole) propertyType).getMinimumOccurs() == 0) |
| { |
| continue; // If optional and no value, skip the full row. |
| } |
| cardinality = 0; |
| } else if (value instanceof Collection<?>) { |
| cardinality = ((Collection<?>) value).size(); |
| } else { |
| cardinality = 1; |
| } |
| } else if (propertyType instanceof AttributeType<?>) { |
| value = ((AttributeType<?>) propertyType).getDefaultValue(); |
| } else if (propertyType instanceof Operation) { |
| buffer.append(" = "); |
| try { |
| if (propertyType instanceof AbstractOperation) { |
| ((AbstractOperation) propertyType).formatResultFormula(buffer); |
| } else { |
| AbstractOperation.defaultFormula(((Operation) propertyType).getParameters(), buffer); |
| } |
| } catch (IOException e) { |
| throw new UncheckedIOException(e); // Should never happen since we write in a StringBuffer. |
| } |
| value = CharSequences.trimWhitespaces(buffer).toString(); |
| buffer.setLength(0); |
| } |
| final String valueType; // The value to write in the type column. |
| final Class<?> valueClass; // AttributeType.getValueClass() if applicable. |
| final int minimumOccurs, maximumOccurs; // Negative values mean no cardinality. |
| final IdentifiedType resultType; // Result of operation if applicable. |
| if (propertyType instanceof Operation) { |
| resultType = ((Operation) propertyType).getResult(); // May be null |
| } else { |
| resultType = propertyType; |
| } |
| if (resultType instanceof AttributeType<?>) { |
| final AttributeType<?> pt = (AttributeType<?>) resultType; |
| minimumOccurs = pt.getMinimumOccurs(); |
| maximumOccurs = pt.getMaximumOccurs(); |
| valueClass = pt.getValueClass(); |
| valueType = getFormat(Class.class).format(valueClass, buffer, dummyFP).toString(); |
| buffer.setLength(0); |
| } else if (resultType instanceof FeatureAssociationRole) { |
| final FeatureAssociationRole pt = (FeatureAssociationRole) resultType; |
| minimumOccurs = pt.getMinimumOccurs(); |
| maximumOccurs = pt.getMaximumOccurs(); |
| valueType = toString(DefaultAssociationRole.getValueTypeName(pt)); |
| valueClass = Feature.class; |
| } else { |
| valueType = (resultType != null) ? toString(resultType.getName()) : ""; |
| valueClass = null; |
| minimumOccurs = -1; |
| maximumOccurs = -1; |
| } |
| /* |
| * At this point we determined that the row should not be skipped |
| * and we got all information to format. |
| */ |
| isFirstColumn = true; |
| for (final Column column : visibleColumns) { |
| if (!isFirstColumn) nextColumn(table); |
| isFirstColumn = false; |
| switch (column) { |
| /* |
| * Human-readable name of the property. May contains any characters (spaces, ideographs, etc). |
| * In many cases, this information is not provided and the whole column is skipped. |
| */ |
| case DESIGNATION: { |
| propertyType.getDesignation().ifPresent((d) -> { |
| table.append(d.toString(displayLocale)); |
| }); |
| break; |
| } |
| /* |
| * Machine-readable name of the property (identifier). This information is mandatory. |
| * This name is usually shorter than the designation and should contain only valid |
| * Unicode identifier characters (e.g. no spaces). |
| */ |
| case NAME: { |
| table.append(toString(propertyType.getName())); |
| break; |
| } |
| /* |
| * The base class or interface for all values in properties of the same type. |
| * This is typically String, Number, Integer, Geometry or URL. |
| */ |
| case TYPE: { |
| table.append(valueType); |
| break; |
| } |
| /* |
| * Minimum and maximum number of occurrences allowed for this property. |
| * If we are formatting a Feature instead of a FeatureType, then the |
| * actual number of values is also formatted. Example: 42 ∈ [0 … ∞] |
| */ |
| case CARDINALITY: { |
| table.setCellAlignment(TableAppender.ALIGN_RIGHT); |
| if (cardinality >= 0) { |
| table.append(getFormat(Integer.class).format(cardinality, buffer, dummyFP)); |
| buffer.setLength(0); |
| } |
| if (maximumOccurs >= 0) { |
| if (cardinality >= 0) { |
| table.append(' ') |
| .append((cardinality >= minimumOccurs && cardinality <= maximumOccurs) ? '∈' : '∉') |
| .append(' '); |
| } |
| final Format format = getFormat(Integer.class); |
| table.append('[').append(format.format(minimumOccurs, buffer, dummyFP)).append(" … "); |
| buffer.setLength(0); |
| if (maximumOccurs != Integer.MAX_VALUE) { |
| table.append(format.format(maximumOccurs, buffer, dummyFP)); |
| } else { |
| table.append('∞'); |
| } |
| buffer.setLength(0); |
| table.append(']'); |
| } |
| break; |
| } |
| /* |
| * If formatting a FeatureType, the default value. If formatting a Feature, the actual value. |
| * A java.text.Format instance dedicated to the value class is used if possible. In addition |
| * to types for which a java.text.Format may be available, we also have to check for other |
| * special cases. If there is more than one value, they are formatted as a coma-separated list. |
| */ |
| case VALUE: { |
| table.setCellAlignment(TableAppender.ALIGN_LEFT); |
| final Format format = getFormat(valueClass); // Null if valueClass is null. |
| final Iterator<?> it = CollectionsExt.toCollection(value).iterator(); |
| String separator = ""; |
| int length = 0; |
| while (it.hasNext()) { |
| value = it.next(); |
| if (value != null) { |
| if (propertyType instanceof FeatureAssociationRole) { |
| final String p = DefaultAssociationRole.getTitleProperty((FeatureAssociationRole) propertyType); |
| if (p != null) { |
| value = ((Feature) value).getPropertyValue(p); |
| if (value == null) continue; |
| } |
| } else if (format != null && valueClass.isInstance(value)) { // Null safe because of getFormat(valueClass) contract. |
| /* |
| * Convert numbers, dates, angles, etc. to character sequences before to append them in the table. |
| * Note that DecimalFormat writes Not-a-Number as "NaN" in some locales and as "�" in other locales |
| * (U+FFFD - Unicode replacement character). The "�" seems to be used mostly for historical reasons; |
| * as of 2017 the Unicode Common Locale Data Repository (CLDR) seems to define "NaN" for all locales. |
| * We could configure DecimalFormatSymbols for using "NaN", but (for now) we rather substitute "�" by |
| * "NaN" here for avoiding to change the DecimalFormat configuration and for distinguishing the NaNs. |
| */ |
| final StringBuffer t = format.format(value, buffer, dummyFP); |
| if (value instanceof Number) { |
| final float f = ((Number) value).floatValue(); |
| if (Float.isNaN(f)) { |
| if ("�".contentEquals(t)) { |
| t.setLength(0); |
| t.append("NaN"); |
| } |
| try { |
| final int n = MathFunctions.toNanOrdinal(f); |
| if (n > 0) t.append(" #").append(n); |
| } catch (IllegalArgumentException e) { |
| // May happen if the NaN is a signaling NaN instead of a quiet NaN. |
| final int bits = Float.floatToRawIntBits(f); |
| if (bits != illegalNaN) { |
| illegalNaN = bits; |
| Logging.recoverableException(AbstractIdentifiedType.LOGGER, FeatureFormat.class, "format", e); |
| } |
| } |
| } |
| } |
| value = t; |
| } |
| /* |
| * All values: the numbers, dates, angles, etc. formatted above, any other character sequences |
| * (e.g. InternationalString), or other kind of values - some of them handled in a special way. |
| */ |
| length = formatValue(value, table.append(separator), length); |
| buffer.setLength(0); |
| if (length < 0) break; // Value is too long, abandon remaining iterations. |
| separator = SEPARATOR; |
| length += SEPARATOR.length(); |
| } |
| } |
| break; |
| } |
| /* |
| * Characteristics are optional information attached to some values. For example if a property |
| * value is a temperature measurement, a characteritic of that value may be the unit of measure. |
| * Characteristics are handled as "attributes of attributes". |
| */ |
| case CHARACTERISTICS: { |
| if (propertyType instanceof AttributeType<?>) { |
| int length = 0; |
| String separator = ""; |
| format: for (final AttributeType<?> ct : ((AttributeType<?>) propertyType).characteristics().values()) { |
| /* |
| * Format the characteristic name. We will append the value(s) later. |
| * We keep trace of the text length in order to stop formatting if the |
| * text become too long. |
| */ |
| final GenericName cn = ct.getName(); |
| final String cs = toString(cn); |
| table.append(separator).append(cs); |
| length += separator.length() + cs.length(); |
| Collection<?> cv = CollectionsExt.singletonOrEmpty(ct.getDefaultValue()); |
| if (feature != null) { |
| /* |
| * Usually, the property `cp` below is null because all features use the same |
| * characteristic value (for example the same unit of measurement), which is |
| * given by the default value `cv`. Nevertheless we have to check if current |
| * feature overrides this characteristic. |
| */ |
| final Property cp = feature.getProperty(propertyType.getName().toString()); |
| if (cp instanceof Attribute<?>) { // Should always be true, but we are paranoiac. |
| Attribute<?> ca = ((Attribute<?>) cp).characteristics().get(cn.toString()); |
| if (ca != null) cv = ca.getValues(); |
| } |
| } |
| /* |
| * Now format the value, separated from the name with " = ". Example: unit = m/s |
| * If the value accepts multi-occurrences, we will format the value between {…}. |
| * We use {…} because we may have more than one characteristic in the same cell, |
| * so we need a way to distinguish multi-values from multi-characteristics. |
| */ |
| final boolean multi = ct.getMaximumOccurs() > 1; |
| String sep = multi ? " = {" : " = "; |
| for (Object c : cv) { |
| length = formatValue(c, table.append(sep), length += sep.length()); |
| if (length < 0) break format; // Value is too long, abandon remaining iterations. |
| sep = SEPARATOR; |
| } |
| separator = SEPARATOR; |
| if (multi && sep == SEPARATOR) { |
| table.append('}'); |
| } |
| } |
| } |
| break; |
| } |
| case REMARKS: { |
| if (org.apache.sis.feature.Field.isDeprecated(propertyType)) { |
| table.append(resources.getString(Vocabulary.Keys.Deprecated)); |
| final InternationalString r = ((Deprecable) propertyType).getRemarks().orElse(null); |
| if (r != null) { |
| remarks.add(r.toString(displayLocale)); |
| appendSuperscript(remarks.size(), table); |
| } |
| } |
| break; |
| } |
| } |
| } |
| table.nextLine(); |
| } |
| table.nextLine('─'); |
| table.flush(); |
| /* |
| * If there is any remarks, write them below the table. |
| */ |
| final int n = remarks.size(); |
| for (int i=0; i<n; i++) { |
| appendSuperscript(i+1, toAppendTo); |
| toAppendTo.append(' ').append(remarks.get(i)).append(lineSeparator); |
| } |
| } |
| |
| /** |
| * Returns the display name for the given {@code GenericName}. |
| */ |
| private String toString(final GenericName name) { |
| if (name == null) { // Should not be null, but let be safe. |
| return ""; |
| } |
| final InternationalString i18n = name.toInternationalString(); |
| if (i18n != null) { // Should not be null, but let be safe. |
| final String s = i18n.toString(displayLocale); |
| if (s != null) { |
| return s; |
| } |
| } |
| return name.toString(); |
| } |
| |
| /** |
| * Appends the given attribute value, in a truncated form if it exceed the maximal value length. |
| * |
| * @param value the value to append. |
| * @param table where to append the value. |
| * @param length number of characters appended before this method call in the current table cell. |
| * @return number of characters appended after this method call in the current table cell, or -1 if |
| * the length exceed the maximal length (in which case the caller should break iteration). |
| */ |
| private int formatValue(final Object value, final TableAppender table, final int length) { |
| String text; |
| if (value instanceof InternationalString) { |
| text = ((InternationalString) value).toString(displayLocale); |
| } else if (value instanceof GenericName) { |
| text = toString((GenericName) value); |
| } else if (value instanceof IdentifiedType) { |
| text = toString(((IdentifiedType) value).getName()); |
| } else if (value instanceof IdentifiedObject) { |
| text = IdentifiedObjects.getIdentifierOrName((IdentifiedObject) value); |
| } else { |
| text = Geometries.wrap(value).map(GeometryWrapper::toString).orElseGet(value::toString); |
| } |
| final int remaining = MAXIMAL_VALUE_LENGTH - length; |
| if (remaining >= text.length()) { |
| table.append(text); |
| return length + text.length(); |
| } else { |
| table.append(text, 0, Math.max(0, remaining - 1)).append('…'); |
| return -1; |
| } |
| } |
| |
| /** |
| * Appends the given number as an superscript if possible, or as an ordinary number otherwise. |
| */ |
| private static void appendSuperscript(final int n, final Appendable toAppendTo) throws IOException { |
| if (n >= 0 && n < 10) { |
| toAppendTo.append(Characters.toSuperScript((char) ('0' + n))); |
| } else { |
| toAppendTo.append('(').append(String.valueOf(n)).append(')'); |
| } |
| } |
| |
| /** |
| * Formats the given object using a shared instance of {@code ParameterFormat}. |
| * This is used for {@link DefaultFeatureType#toString()} implementation. |
| */ |
| static String sharedFormat(final Object object) { |
| FeatureFormat f = INSTANCE.getAndSet(null); |
| if (f == null) { |
| f = new FeatureFormat(); |
| } |
| final String s = f.format(object); |
| INSTANCE.set(f); |
| return s; |
| } |
| |
| /** |
| * Not yet supported. |
| * |
| * @return currently never return. |
| * @throws ParseException currently always thrown. |
| */ |
| @Override |
| public Object parse(final CharSequence text, final ParsePosition pos) throws ParseException { |
| throw new ParseException(Errors.forLocale(displayLocale) |
| .getString(Errors.Keys.UnsupportedOperation_1, "parse"), pos.getIndex()); |
| } |
| |
| /** |
| * Returns a clone of this format. |
| * |
| * @return a clone of this format. |
| */ |
| @Override |
| public FeatureFormat clone() { |
| return (FeatureFormat) super.clone(); |
| } |
| } |