blob: 8a34de61ffaa593d2d06c7ba6c029022163f5330 [file] [log] [blame]
/*
* 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.util.privy;
import java.text.Format;
import java.util.Map;
import java.util.Arrays;
import java.util.Currency;
import java.util.Locale;
import java.util.TimeZone;
import java.io.IOException;
import java.nio.charset.Charset;
import org.opengis.util.Type;
import org.opengis.util.Record;
import org.opengis.util.GenericName;
import org.opengis.util.InternationalString;
import org.apache.sis.io.CompoundFormat;
import org.apache.sis.io.LineAppender;
import org.apache.sis.util.CharSequences;
import org.apache.sis.util.Workaround;
import org.apache.sis.util.Localized;
import org.apache.sis.util.resources.Vocabulary;
// Specific to the main branch:
import org.opengis.util.CodeList;
/**
* Creates string representation of property values of unknown type.
* Tabulations are replaced by spaces, and line feeds can optionally
* be replaced by the Pilcrow character.
*
* Subclasses need to override {@link #getLocale()}, and should also override {@link #toString(Object)}.
*
* @author Martin Desruisseaux (Geomatys)
*/
public abstract class PropertyFormat extends LineAppender implements Localized {
/**
* The string to insert for missing values.
*/
private static final String MISSING = " ";
/**
* The format for the column in process of being written. This is a format to use for the column as a whole.
* This field is updated for every new column to write. May be {@code null} if the format is unspecified.
*/
protected transient Format columnFormat;
/**
* Creates a new instance which will write to the given appendable.
*
* @param out where to format the objects.
*/
protected PropertyFormat(final Appendable out) {
super(out);
}
/**
* Appends a textual representation of the given value.
*
* @param value the value to format (may be {@code null}).
* @throws IOException if an error occurred while writing the value.
*/
public final void appendValue(final Object value) throws IOException {
appendValue(value, false);
}
/**
* Appends a textual representation of the given value, with a check for nested collections.
*
* @param value the value to format (may be {@code null}).
* @param recursive {@code true} if this method is invoking itself for writing collection values.
*/
private void appendValue(final Object value, final boolean recursive) throws IOException {
final CharSequence text;
if (value == null) {
text = MISSING;
} else if (columnFormat != null) {
if (columnFormat instanceof CompoundFormat<?>) {
appendCompound((CompoundFormat<?>) columnFormat, value);
return;
}
text = columnFormat.format(value);
} else if (value instanceof InternationalString) {
text = freeText(((InternationalString) value).toString(getLocale()));
} else if (value instanceof CharSequence) {
text = freeText(value.toString());
} else if (value instanceof CodeList<?>) {
text = MetadataServices.getInstance().getCodeTitle((CodeList<?>) value, getLocale());
} else if (value instanceof Enum<?>) {
text = CharSequences.upperCaseToSentence(((Enum<?>) value).name());
} else if (value instanceof Boolean) {
text = Vocabulary.forLocale(getLocale()).getString((Boolean) value ? Vocabulary.Keys.True : Vocabulary.Keys.False);
} else if (value instanceof Type) {
appendName(((Type) value).getTypeName());
return;
} else if (value instanceof Locale) {
final Locale locale = getLocale();
text = (locale != Locale.ROOT) ? ((Locale) value).getDisplayName(locale) : value.toString();
} else if (value instanceof TimeZone) {
final Locale locale = getLocale();
text = (locale != Locale.ROOT) ? ((TimeZone) value).getDisplayName(locale) : ((TimeZone) value).getID();
} else if (value instanceof Charset) {
final Locale locale = getLocale();
text = (locale != Locale.ROOT) ? ((Charset) value).displayName(locale) : ((Charset) value).name();
} else if (value instanceof Currency) {
final Locale locale = getLocale();
text = (locale != Locale.ROOT) ? ((Currency) value).getDisplayName(locale) : value.toString();
} else if (value instanceof Record) {
appendCollection(((Record) value).getAttributes().values(), recursive);
return;
} else if (value instanceof Iterable<?>) {
appendCollection((Iterable<?>) value, recursive);
return;
} else if (value instanceof Object[]) {
appendCollection(Arrays.asList((Object[]) value), recursive);
return;
} else if (value instanceof Map.Entry<?,?>) {
final Map.Entry<?,?> entry = (Map.Entry<?,?>) value;
final Object k = entry.getKey();
final Object v = entry.getValue();
if (k == null) {
append(null);
} else {
appendValue(k, recursive);
}
if (v != null) {
append(" → ");
appendValue(v, recursive);
}
return;
} else {
text = toString(value);
}
append(text);
}
/**
* Invoked by {@link PropertyFormat} for formatting a value which has not been recognized as one of the types
* to be handled in a special way. Some of the types handled in a special way are {@link InternationalString},
* {@link CodeList}, {@link Enum}, {@link Type}, {@link Locale}, {@link TimeZone}, {@link Charset},
* {@link Currency}, {@link Record}, {@link Iterable} and arrays. Other types should be handled by this method.
* In particular, {@link Number}, {@link java.util.Date} and {@link org.apache.sis.measure.Angle}
* are <strong>not</strong> handled by default by this {@link PropertyFormat} class and should be handled here.
*
* @param value the value to format (never {@code null}).
* @return the formatted value.
*/
protected String toString(final Object value) {
return freeText(value.toString());
}
/**
* Invoked after formatting a text that could be anything. It current version, it includes all kinds of
* {@link CharSequence} including {@link InternationalString}, together with {@link Object#toString()}
* values computed by the default {@link #toString(Object)} implementation.
*
* The default {@code freeText(…)} implementation removes white space and control characters.
* Subclasses can override for example for making a text shorter.
*
* @param text the free text, or {@code null}.
* @return the text to append.
*/
protected String freeText(final String text) {
// Really want `trim()` because there is sometimes control characters to remove.
return (text != null) ? text.trim() : MISSING;
}
/**
* Writes the values of the given collection. A maximum of 10 values will be written.
* If the collection contains other collections, the other collections will <strong>not</strong>
* be written recursively.
*/
private void appendCollection(final Iterable<?> values, final boolean recursive) throws IOException {
if (values != null) {
if (recursive) {
append('…'); // Do not format collections inside collections.
} else {
int count = 0;
for (final Object value : values) {
if (value != null) {
if (count != 0) append(", ");
appendValue(value, true);
if (++count == 10) { // Arbitrary limit.
append(", …");
break;
}
}
}
}
}
}
/**
* Workaround for the inability to define the variable {@code <V>} locally.
*/
@Workaround(library="JDK", version="1.7")
private <V> void appendCompound(final CompoundFormat<V> format, final Object value) throws IOException {
format.format(format.getValueType().cast(value), this);
}
/**
* Localizes the given name in the display locale, or formats "(Unnamed)" if no localized value is found.
*/
private void appendName(final GenericName name) throws IOException {
final Locale locale = getLocale();
if (name != null) {
final InternationalString i18n = name.toInternationalString();
if (i18n != null) {
final String localized = i18n.toString(locale);
if (localized != null) {
append(localized);
return;
}
}
final String localized = name.toString();
if (localized != null) {
append(localized);
return;
}
}
append('(').append(Vocabulary.forLocale(locale).getString(Vocabulary.Keys.Unnamed)).append(')');
}
}