/* * Geotoolkit.org - An Open Source Java GIS Toolkit * http://www.geotoolkit.org * * (C) 2005-2012, Open Source Geospatial Foundation (OSGeo) * (C) 2010-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.openoffice; import java.util.Map; import java.util.Arrays; import java.util.HashMap; import java.util.TimeZone; import java.util.Calendar; import java.util.GregorianCalendar; import java.util.logging.Level; import java.util.logging.Logger; import java.util.logging.LogRecord; import com.sun.star.sheet.XAddIn; import com.sun.star.util.Date; import com.sun.star.lang.Locale; import com.sun.star.lang.XServiceInfo; import com.sun.star.lang.XServiceName; import com.sun.star.beans.XPropertySet; import com.sun.star.uno.AnyConverter; import com.sun.star.lib.uno.helper.WeakBase; import org.apache.sis.util.collection.BackingStoreException; import org.apache.sis.util.Classes; import org.apache.sis.util.logging.Logging; /** * Base class for methods to export as formulas in the * <A HREF="http://www.openoffice.org">OpenOffice</A> spread sheet. * * @author Martin Desruisseaux (IRD, Geomatys) * @version 3.20 * * @since 3.09 (derived from 2.2) * @module */ public abstract class Formulas extends WeakBase implements XAddIn, XServiceName, XServiceInfo { /** * {@code true} for throwing an exception in case of failure, or {@code false} for returning * {@code NaN} instead. This apply only to numerical computations. Formulas returning a text * value returns in the exception message in current implementation. */ protected static final boolean THROW_EXCEPTION = true; /** * Factor for conversions of days to milliseconds. * Used for date conversions as in {@link #toDate}. */ protected static final long DAY_TO_MILLIS = 24*60*60*1000L; /** * The name of the provided service. */ protected static final String ADDIN_SERVICE = "com.sun.star.sheet.AddIn"; /** * Informations about exported methods. */ protected final Map<String,MethodInfo> methods = new HashMap<String,MethodInfo>(); /** * Locale attribute required by {@code com.sun.star.lang.XLocalizable} interface. */ private Locale locale; /** * The locale as an object from the standard Java SDK. * Will be fetched only when first needed. */ private transient java.util.Locale javaLocale; /** * The calendar to uses for date conversions. Will be created only when first needed. */ private transient Calendar calendar; /** * The logger, fetched when first needed. */ private static Logger logger; /** * Default constructor. Subclass constructors need to add entries in the {@link #methods} map. */ protected Formulas() { } /** * Sets the locale to be used by this object. * * @param locale The new locale. */ @Override public void setLocale(final Locale locale) { this.locale = locale; javaLocale = null; } /** * Returns the locale, which is used by this object. * * @return The current locale. */ @Override public final Locale getLocale() { return locale; } /** * Returns the locale as an object from the Java standard SDK. * * @return The current locale. */ protected final java.util.Locale getJavaLocale() { if (javaLocale == null) { if (locale != null) { String language = locale.Language; if (language == null) language = ""; String country = locale.Country; if (country == null) country = ""; String variant = locale.Variant; if (variant == null) variant = ""; javaLocale = new java.util.Locale(language, country, variant); } else { javaLocale = java.util.Locale.getDefault(); } } return javaLocale; } /** * Provides the supported service names of the implementation, including also * indirect service names. * * @return Sequence of service names that are supported. */ @Override public final String[] getSupportedServiceNames() { return new String[] {ADDIN_SERVICE, getServiceName()}; } /** * Tests whether the specified service is supported, i.e. implemented by the implementation. * * @param name Name of service to be tested. * @return {@code true} if the service is supported, {@code false} otherwise. */ @Override public final boolean supportsService(final String name) { return name.equals(ADDIN_SERVICE) || name.equals(getServiceName()); } /** * The service name that can be used to create such an object by a factory. * This is defined as a field in the subclass with exactly the following signature: * * {@preformat java * private static final String __serviceName; * } * * @return The service name. */ @Override public abstract String getServiceName(); /** * Provides the implementation name of the service implementation. * * @return Unique name of the implementation. */ @Override public final String getImplementationName() { return getClass().getName(); } /** * Returns the programmatic name of the category the function belongs to. * The category name is used to group similar functions together. The programmatic * category name should always be in English, it is never shown to the user. It * is usually one of the names listed in {@code com.sun.star.sheet.XAddIn} interface. * * @param function The exact name of a method within its interface. * @return The category name the specified function belongs to. */ @Override public final String getProgrammaticCategoryName(final String function) { final MethodInfo info = methods.get(function); return (info != null) ? info.category : "Add-In"; } /** * Returns the user-visible name of the category the function belongs to. * This is used when category names are shown to the user. * * @param function The exact name of a method within its interface. * @return The user-visible category name the specified function belongs to. */ @Override public final String getDisplayCategoryName(final String function) { return getProgrammaticCategoryName(function); } /** * Returns the internal function name for an user-visible name. The user-visible * name of a function is the name shown to the user. It may be translated to the * {@linkplain #getLocale current language}, so it is never stored in files. It * should be a single word and is used when entering or displaying formulas. * <p> * Attention: The method name contains a spelling error. Due to compatibility * reasons the name cannot be changed. * * @param display The user-visible name of a function. * @return The exact name of the method within its interface. */ @Override public final String getProgrammaticFuntionName(final String display) { for (final Map.Entry<String,MethodInfo> entry : methods.entrySet()) { if (display.equals(entry.getValue().display)) { return entry.getKey(); } } return ""; } /** * Returns the user-visible function name for an internal name. * The user-visible name of a function is the name shown to the user. * It may be translated to the {@linkplain #getLocale current language}, * so it is never stored in files. It should be a single word and is used * when entering or displaying formulas. * * @param function The exact name of a method within its interface. * @return The user-visible name of the specified function. */ @Override public final String getDisplayFunctionName(final String function) { final MethodInfo info = methods.get(function); return (info != null) ? info.display : ""; } /** * Returns the description of a function. The description is shown to the user * when selecting functions. It may be translated to the {@linkplain #getLocale * current language}. * * @param function The exact name of a method within its interface. * @return The description of the specified function. */ @Override public final String getFunctionDescription(final String function) { final MethodInfo info = methods.get(function); return (info != null) ? info.description : ""; } /** * Returns the user-visible name of the specified argument. The argument name is * shown to the user when prompting for arguments. It should be a single word and * may be translated to the {@linkplain #getLocale current language}. * * @param function The exact name of a method within its interface. * @param argument The index of the argument (0-based). * @return The user-visible name of the specified argument. */ @Override public final String getDisplayArgumentName(final String function, int argument) { final MethodInfo info = methods.get(function); if (info != null) { argument <<= 1; final String[] arguments = info.arguments; if (argument >= 0 && argument < arguments.length) { return arguments[argument]; } } return ""; } /** * Returns the description of the specified argument. The argument description is * shown to the user when prompting for arguments. It may be translated to the * {@linkplain #getLocale current language}. * * @param function The exact name of a method within its interface. * @param argument The index of the argument (0-based). * @return The description of the specified argument. */ @Override public final String getArgumentDescription(final String function, int argument) { final MethodInfo info = methods.get(function); if (info != null) { argument = (argument << 1) + 1; final String[] arguments = info.arguments; if (argument >= 0 && argument < arguments.length) { return arguments[argument]; } } return ""; } /** * Sets the timezone for time values to be provided to {@link #toDate}. * If this method is never invoked, then the default timezone is the locale one. * * @param timezone The new timezone. */ protected final void setTimeZone(final String timezone) { final TimeZone tz = TimeZone.getTimeZone(timezone); if (calendar == null) { calendar = new GregorianCalendar(tz); } else { calendar.setTimeZone(tz); } } /** * Returns the spreadsheet epoch. The timezone is the one specified during the * last invocation of {@link #setTimeZone}. The epoch is used for date conversions * as in {@link #toDate}. * * @param xOptions Provided by OpenOffice. * @return The spreadsheet epoch, always as a new Java Date object. */ protected final java.util.Date getEpoch(final XPropertySet xOptions) { final Date date; try { date = (Date) AnyConverter.toObject(Date.class, xOptions.getPropertyValue("NullDate")); } catch (Exception e) { // Les exception lancées par la ligne ci-dessus sont nombreuses... reportException("getEpoch", e, THROW_EXCEPTION); return null; } if (calendar == null) { calendar = new GregorianCalendar(); } calendar.clear(); calendar.set(date.Year, date.Month-1, date.Day); return calendar.getTime(); } /** * Converts a date from a spreadsheet value to a Java {@link java.util.Date} object. * The timezone is the one specified during the last invocation of {@link #setTimeZone}. * * @param xOptions Provided by OpenOffice. * @param time The spreadsheet numerical value for a date, by default in the local timezone. * @return The date as a Java object. */ protected final java.util.Date toDate(final XPropertySet xOptions, final double time) { final java.util.Date date = getEpoch(xOptions); if (date != null) { date.setTime(date.getTime() + Math.round(time * DAY_TO_MILLIS)); } return date; } /** * Converts a date from a Java {@link java.util.Date} object to a spreadsheet value. * The timezone is the one specified during the last invocation of {@link #setTimeZone}. * * @param xOptions Provided by OpenOffice. * @param time The date as a Java object. * @return The spreadsheet numerical value for a date, by default in the local timezone. */ protected final double toDouble(final XPropertySet xOptions, final java.util.Date time) { final java.util.Date epoch = getEpoch(xOptions); if (epoch != null) { return (time.getTime() - epoch.getTime()) / (double)DAY_TO_MILLIS; } else { return Double.NaN; } } /** * The string to returns when a formula don't have any value to return. * * @return The string with a message for missing values. * @todo localize. */ protected static String emptyString() { return "(none)"; } /** * Returns the minimal length of the specified arrays. In the special case where one array * has a length of 1, we assume that this single element will be repeated for all elements * in the other array. */ static int getLength(final Object[] array1, final Object[] array2) { if (array1 == null || array2 == null) { return 0; } if (array1.length == 1) return array2.length; if (array2.length == 1) return array1.length; return Math.min(array1.length, array2.length); } /** * Returns the localized message from the specified exception. If no message is available, * returns a default string. This method never returns a null value. * * @param exception The exception for which to get the localized message. * @return A message. */ protected static String getLocalizedMessage(final Throwable exception) { final String message = exception.getLocalizedMessage(); if (message != null) { return message; } return Classes.getShortClassName(exception); } /** * Returns a table filled with {@link Double#NaN NaN} values. This method is invoked when * an operation failed for a whole table. * * @param rows The number of rows. * @param cols The number of columns. * @return A table of the given size filled with NaN values. */ protected static double[][] getFailure(final int rows, final int cols) { final double[][] dummy = new double[rows][]; for (int i=0; i<rows; i++) { final double[] row = new double[cols]; Arrays.fill(row, Double.NaN); dummy[i] = row; } return dummy; } /** * Reports an exception. This is used if an exception occurred in a method which can't returns * a {@link String} object. This method log the stack trace at the FINE level. We don't use * the WARNING level since this is not a program disfunction; the failure is probably caused * by wrong user-specified parameters. * * @param method The method from which an exception occurred. * @param exception The exception. * @param rethrow {@code true} for rethrowing the exception after the report. */ protected final void reportException(final String method, final Throwable exception, final boolean rethrow) { final Logger logger = getLogger(); final LogRecord record = new LogRecord(Level.FINE, getLocalizedMessage(exception)); record.setLoggerName (logger.getName()); record.setSourceClassName (getClass().getName()); record.setSourceMethodName(method); record.setThrown (exception); logger.log(record); if (rethrow) { if (exception instanceof RuntimeException) { throw (RuntimeException) exception; } if (exception instanceof Error) { throw (Error) exception; } throw new BackingStoreException(exception); } } /** * Returns the logger to use for logging warnings. * * @return The logger to use. */ protected static synchronized Logger getLogger() { if (logger == null) { logger = Logging.getLogger("org.geotoolkit.openoffice"); } return logger; } }