/*
* Geotoolkit.org - An Open Source Java GIS Toolkit
* http://www.geotoolkit.org
*
* (C) 2007-2012, Open Source Geospatial Foundation (OSGeo)
* (C) 2009-2012, Geomatys
*
* 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.geotoolkit.util.logging;
import java.text.AttributedCharacterIterator;
import java.text.FieldPosition;
import java.text.Format;
import java.text.ParseException;
import java.text.ParsePosition;
import java.util.Locale;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.logging.LogRecord;
import org.geotoolkit.resources.Errors;
import org.apache.sis.util.Classes;
import org.apache.sis.util.CharSequences;
import org.apache.sis.util.logging.Logging;
/**
* Wraps a {@link Format} object in order to either parse fully a string, or log a warning.
* This class provides a {@link #parse(String)} method which performs the following tasks:
* <p>
* <ul>
* <li>Checks if the string was fully parsed and log a warning if it was not. This is
* different than the default {@link #parseObject(String)} behavior which check only
* if the <em>beginning</em> of the string was parsed and ignore any remaining characters.</li>
* <li>Ensures that the parsed object is of some specific class specified at construction time.</li>
* <li>If the string can't be fully parsed or is not of the expected class, logs a warning.</li>
* </ul>
*
* @param <T> The expected type of values to be parsed.
*
* @author Martin Desruisseaux (IRD)
* @since 2.4
* @module
*/
public class LoggedFormat<T> extends Format {
/**
* For cross-version compatibility.
*/
private static final long serialVersionUID = 4578880360344271325L;
/**
* The wrapped format.
*/
private final Format format;
/**
* The expected type for the parsed values.
*/
private final Class<T> type;
/**
* The level to use for the messages to be logged.
*/
private Level level;
/**
* The logger where to log warnings, or {@code null} if none.
*
* @see #setLogger
*/
private String logger;
/**
* The class to declare in as the warning emitter, or {@code null} if none.
*
* @see #setCaller
*/
private String className;
/**
* The method to declare in as the warning emitter, or {@code null} if none.
*
* @see #setCaller
*/
private String methodName;
/**
* Creates a new format wrapping the specified one.
*
* @param format The format to use for parsing and formatting.
* @param type The expected type of parsed values.
*/
protected LoggedFormat(final Format format, final Class<T> type) {
this.format = format;
this.type = type;
this.level = Level.WARNING;
}
/**
* Creates a new format wrapping the specified one.
*
* @param <T> The expected type of parsed values.
* @param format The format to use for parsing and formatting.
* @param type The expected type of parsed values.
* @return A new format which will delegate parsing to the given format and log failures.
*/
public static <T> LoggedFormat<T> getInstance(final Format format, final Class<T> type) {
return new LoggedFormat<>(format, type);
}
/**
* Sets the logger where to send the warnings eventually emitted by the {@link #parse} method.
*
* @param logger The logger where to log warnings, or {@code null} if none.
*/
public void setLogger(final String logger) {
this.logger = logger;
}
/**
* Sets the logger level for the warnings eventually emitted by the {@link #parse} method.
* The default value is {@link Level#WARNING}.
*
* @param level The new logging level.
*
* @since 2.5
*/
public void setLevel(final Level level) {
if (level != null) {
this.level = level;
}
}
/**
* Sets the {@linkplain LogRecord#setSourceClassName source class name} and
* {@linkplain LogRecord#setSourceMethodName source method name} for the warnings
* eventually emitted by the {@link #parse} method.
*
* @param caller The class to declare as the warning emitter, or {@code null} if none.
* @param method The method to declare as the warning emitter, or {@code null} if none.
*/
public void setCaller(final Class<?> caller, final String method) {
this.className = (caller != null) ? caller.getCanonicalName() : null;
this.methodName = method;
}
/**
* Parses the specified string. If the string can't be parsed, then this method returns
* {@code null}. If it can be parsed at least partially and is of the kind specified at
* construction time, then it is returned. If the string has not been fully parsed, then
* a {@linkplain LogRecord log record} is prepared and logged.
*
* @param text The text to parse, or {@code null}.
* @return The parsed object, or {@code null} if {@code text} was null or can't be parsed.
*/
public T parse(String text) {
if (text == null || (text=text.trim()).isEmpty()) {
return null;
}
final ParsePosition position = new ParsePosition(0);
final Object value = parseObject(text, position);
int index = position.getIndex();
final int error = position.getErrorIndex();
if (error >= 0 && error < index) {
index = error;
}
if (index < text.length()) {
doLogWarning(formatUnparsable(text, 0, index, getWarningLocale(), level));
} else if (value!=null && !type.isInstance(value)) {
doLogWarning(Errors.getResources(getWarningLocale()).getLogRecord(level,
Errors.Keys.IllegalClass_2, value.getClass(), type));
return null;
}
return type.cast(value);
}
/**
* Parses text from a string to produce an object. This method delegates the work to the
* {@linkplain Format format} specified at construction time. This method to not perform
* any logging.
*
* @param text The text to parse.
* @return An object parsed from the string.
* @throws ParseException if parsing failed.
*/
@Override
public Object parseObject(final String text) throws ParseException {
return format.parseObject(text);
}
/**
* Parses text from a string to produce an object. This method delegates the work to the
* {@linkplain Format format} specified at construction time. This method to not perform
* any logging.
*
* @param text The text to parse.
* @param position Index and error index information.
* @return An object parsed from the string, or {@code null} in case of error.
*/
@Override
public Object parseObject(final String text, final ParsePosition position) {
return format.parseObject(text, position);
}
/**
* Formats the specified object. This method delegates the work to the
* {@linkplain Format format} specified at construction time.
*
* @param value The object to format.
* @param toAppendTo The buffer where the text is to be appended.
* @param position Identifies a field in the formatted text.
* @return The string buffer passed in with formatted text appended.
*/
@Override
public StringBuffer format(final Object value, final StringBuffer toAppendTo,
final FieldPosition position)
{
return format.format(value, toAppendTo, position);
}
/**
* Formats the specified object. This method delegates the work to the
* {@linkplain Format format} specified at construction time.
*
* @param value The object to format.
* @return The character iterator describing the formatted value.
*/
@Override
public AttributedCharacterIterator formatToCharacterIterator(final Object value) {
return format.formatToCharacterIterator(value);
}
/**
* Logs a warning. The caller is set before to invoke the user-overridable method.
*/
private void doLogWarning(final LogRecord warning) {
if (className != null) {
warning.setSourceClassName(className);
}
if (methodName != null) {
warning.setSourceMethodName(methodName);
}
logWarning(warning);
}
/**
* Logs a warning. This method is invoked automatically by the {@link #parse parse} method
* when a text can't be fully parsed. The default implementation logs the warning to the
* logger specified by the last call to the {@link #setLogger setLogger} method. Subclasses
* may override this method if they want to change the log record before the logging.
*
* @param warning The warning to log.
*/
protected void logWarning(final LogRecord warning) {
if (logger != null) {
final Logger logger = Logging.getLogger(this.logger);
if (warning.getLoggerName() == null) {
warning.setLoggerName(logger.getName());
}
logger.log(warning);
}
}
/**
* Returns the locale to use for formatting warnings. The default implementation returns
* the {@linkplain Locale#getDefault() default locale}.
*
* @return The locale to use for formatting warnings.
*/
protected Locale getWarningLocale() {
return Locale.getDefault(Locale.Category.DISPLAY);
}
/**
* Formats an error message for an unparsable string. This method performs the same work that
* {@link #formatUnparsable(String, int, int, Locale, Level) formatUnparsable(..., Level)},
* except that the result is returned as a {@link String} rather than a {@link LogRecord}.
* This is provided as a convenience method for creating the message to give to an
* {@linkplain Exception#Exception(String) exception constructor}.
*
* @param text The unparsable string.
* @param index The parse position. This is usually {@link ParsePosition#getIndex}.
* @param errorIndex The index where the error occurred. This is usually
* {@link ParsePosition#getErrorIndex}.
* @param locale The locale for the message, or {@code null} for the default one.
* @return A formatted error message.
*
* @since 2.5
*/
public static String formatUnparsable(final String text, final int index,
final int errorIndex, final Locale locale)
{
return (String) doFormatUnparsable(text, index, errorIndex, locale, null);
}
/**
* Formats a log record for an unparsable string. This method is invoked by the
* {@link #parse parse} method for formatting the log record to be given to the
* {@link #logWarning} method. It is made public as a convenience for implementors
* who wish to manage loggings outside this {@code LoggedFormat} class.
*
* @param text The unparsable string.
* @param index The parse position. This is usually {@link ParsePosition#getIndex}.
* @param errorIndex The index where the error occurred. This is usually
* {@link ParsePosition#getErrorIndex}.
* @param locale The locale for the log message, or {@code null} for the default one.
* @param level The log record level.
* @return A formatted log record.
*
* @since 2.5
*/
public static LogRecord formatUnparsable(final String text, final int index,
final int errorIndex, final Locale locale, Level level)
{
if (level == null) {
// It is necessary to ensure that the level argument is non-null,
// otherwise we would get a ClassCastException in the code below.
level = Level.WARNING;
}
return (LogRecord) doFormatUnparsable(text, index, errorIndex, locale, level);
}
/**
* Implementation of {@code formatUnparsable} methods. Returns a {@link LogRecord}
* if {@code level} is non-null, or a {@link String} otherwise.
*/
private static Object doFormatUnparsable(String text, final int index, int errorIndex,
final Locale locale, final Level level)
{
final Errors resources = Errors.getResources(locale);
final int length = text.length();
if (errorIndex < index) {
errorIndex = index;
}
if (errorIndex == length) {
if (level != null) {
return resources.getLogRecord(level, Errors.Keys.UnexpectedEndOfString);
}
return resources.getString(Errors.Keys.UnexpectedEndOfString);
}
final String error = CharSequences.token(text, errorIndex).toString();
text = text.substring(index);
if (level != null) {
return resources.getLogRecord(level, Errors.Keys.UnparsableString_2, text, error);
}
return resources.getString(Errors.Keys.UnparsableString_2, text, error);
}
/**
* Returns a string representation for debugging purpose.
*/
@Override
public String toString() {
final StringBuilder buffer = new StringBuilder(Classes.getShortClassName(this))
.append('[').append(Classes.getShortClassName(format));
if (logger != null) {
buffer.append(", logger=").append(logger);
}
return buffer.append(']').toString();
}
}