OpenConcerto

Dépôt officiel du code source de l'ERP OpenConcerto
sonarqube

svn://code.openconcerto.org/openconcerto

Rev

Rev 180 | Blame | Compare with Previous | Last modification | View Log | RSS feed

/*
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
 * 
 * Copyright 2011-2019 OpenConcerto, by ILM Informatique. All rights reserved.
 * 
 * The contents of this file are subject to the terms of the GNU General Public License Version 3
 * only ("GPL"). You may not use this file except in compliance with the License. You can obtain a
 * copy of the License at http://www.gnu.org/licenses/gpl-3.0.html See the License for the specific
 * language governing permissions and limitations under the License.
 * 
 * When distributing the software, include this License Header Notice in each file.
 */
 
 package org.openconcerto.openoffice;

import org.openconcerto.utils.FormatGroup;
import org.openconcerto.utils.TimeUtils;
import org.openconcerto.utils.XMLCalendarFormat;
import org.openconcerto.utils.XMLDateFormat;

import java.math.BigDecimal;
import java.text.Format;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Arrays;
import java.util.Calendar;
import java.util.Date;
import java.util.List;
import java.util.Locale;
import java.util.TimeZone;

import javax.xml.datatype.Duration;

import net.jcip.annotations.GuardedBy;
import net.jcip.annotations.Immutable;

/**
 * A type of value, as per 16.1 "Data Types" and 6.7.1 "Variable Value Types and Values"
 */
@Immutable
public enum ODValueType {

    /**
     * Parses to {@link BigDecimal} to return the exact number.
     */
    FLOAT("value", Number.class) {

        @Override
        public String format(Object o) {
            // avoid 1.23E+3
            if (o instanceof BigDecimal)
                return ((BigDecimal) o).toPlainString();
            else
                return ((Number) o).toString();
        }

        @Override
        public BigDecimal parse(String s) {
            return new BigDecimal(s);
        }

    },
    PERCENTAGE("value", Number.class) {

        @Override
        public String format(Object o) {
            return FLOAT.format(o);
        }

        @Override
        public Object parse(String s) {
            return FLOAT.parse(s);
        }

    },
    CURRENCY("value", Number.class) {

        @Override
        public String format(Object o) {
            return FLOAT.format(o);
        }

        @Override
        public Object parse(String s) {
            return FLOAT.parse(s);
        }
    },
    // TODO support LocalDateTime
    DATE("date-value", Date.class, Calendar.class) {

        @Override
        public String format(Object o) {
            return formatDate(o);
        }

        @Override
        public Date parse(String date) {
            if (date.length() == 0)
                return null;
            else {
                try {
                    return parseDateValue(date).getTime();
                } catch (ParseException e) {
                    throw new IllegalStateException("wrong date: " + date, e);
                }
            }
        }

    },
    TIME("time-value", Duration.class, java.time.Duration.class, Calendar.class) {

        @Override
        public String format(Object o) {
            if (o instanceof Duration) {
                return o.toString();
            } else if (o instanceof java.time.Duration) {
                // w/o days or larger : PTnHnMnS
                return o.toString();
            } else {
                final Calendar cal = (Calendar) o;
                return TimeUtils.timePartToDuration(cal).toString();
            }
        }

        @Override
        public Duration parse(String date) {
            if (date.length() == 0)
                return null;
            else {
                return TimeUtils.getTypeFactory().newDuration(date);
            }
        }

    },
    BOOLEAN("boolean-value", Boolean.class) {

        @Override
        public String format(Object o) {
            return ((Boolean) o).toString().toLowerCase();
        }

        @Override
        public Boolean parse(String s) {
            return Boolean.valueOf(s);
        }

    },
    STRING("string-value", String.class) {

        @Override
        public String format(Object o) {
            return o.toString();
        }

        @Override
        public String parse(String s) {
            return s;
        }
    };

    private final String attr;
    private final List<Class<?>> acceptedClasses;

    private ODValueType(String attr, Class<?>... classes) {
        this.attr = attr;
        this.acceptedClasses = Arrays.asList(classes);
    }

    /**
     * The name of the value attribute for this value type.
     * 
     * @return the value attribute, e.g. "boolean-value".
     */
    public final String getValueAttribute() {
        return this.attr;
    }

    public boolean canFormat(Class<?> toFormat) {
        for (final Class<?> c : this.acceptedClasses)
            if (c.isAssignableFrom(toFormat))
                return true;
        return false;
    }

    public abstract String format(Object o);

    public abstract Object parse(String s);

    /**
     * The value for the value-type attribute.
     * 
     * @return the value for the value-type attribute, e.g. "float".
     */
    public final String getName() {
        return this.name().toLowerCase();
    }

    /**
     * The instance for the passed value type.
     * 
     * @param name the value of the value-type attribute, e.g. "date".
     * @return the corresponding instance, never <code>null</code>, e.g. {@link #DATE}.
     * @throws IllegalArgumentException if <code>name</code> isn't a valid type.
     */
    public static ODValueType get(String name) {
        return ODValueType.valueOf(name.toUpperCase());
    }

    /**
     * Try to guess the value type for the passed object.
     * 
     * @param o the object.
     * @return a value type capable of formatting <code>o</code> or <code>null</code>.
     * @throws NullPointerException if <code>o</code> is <code>null</code>.
     */
    public static ODValueType forObject(Object o) throws NullPointerException {
        if (o == null)
            throw new NullPointerException();
        if (o instanceof Number)
            return FLOAT;
        else if (o instanceof Boolean)
            return BOOLEAN;
        else if (o instanceof String)
            return STRING;
        else if (o instanceof Duration || o instanceof java.time.Duration)
            return TIME;
        else if (DATE.canFormat(o.getClass()))
            return DATE;
        else
            return null;
    }

    static private final TimeZone UTC_TZ = TimeZone.getTimeZone("UTC");
    // use nulls and not (TimeZone|Locale).getDefault() so as to not need to listen to changes
    // LibreOffice behavior as of 4.1 is to no longer ignore explicit time zone when reading dates.
    @GuardedBy("ODValueType")
    static private DateConfig DATE_CONFIG = new DateConfig(null, null, null, Boolean.FALSE);

    // see http://www.w3.org/TR/2004/REC-xmlschema-2-20041028/#isoformats

    @GuardedBy("ODValueType")
    static private Format DATE_FORMAT, DATE_PARSER;
    static {
        updateFormat();
    }

    static private synchronized String formatDate(Object obj) {
        return DATE_FORMAT.format(obj);
    }

    static private synchronized void updateFormat() {
        // always remove time zone on write
        DATE_FORMAT = new XMLCalendarFormat(getTimeZone(false), getLocale(false));
        DATE_PARSER = getFormatParser(DATE_CONFIG, true);
    }

    static private synchronized SimpleDateFormat createDateFormat(final String pattern, final DateConfig dateConf) {
        final SimpleDateFormat res = new SimpleDateFormat(pattern, dateConf.getLocale(true));
        res.setTimeZone(!dateConf.isTimeZoneIgnored() ? UTC_TZ : dateConf.getTimeZone(true));
        return res;
    }

    static private synchronized Format getFormatParser(final DateConfig dateConf, final boolean forceCreate) {
        if (!forceCreate && dateConf.equals(DATE_CONFIG)) {
            return DATE_PARSER;
        } else {
            final Format xmlDF;
            if (dateConf.isTimeZoneIgnored()) {
                xmlDF = new XMLCalendarFormat(dateConf.getTimeZone(false), dateConf.getLocale(false));
            } else {
                xmlDF = new XMLDateFormat(UTC_TZ, null);
            }
            // first date and time so we don't loose time information on format() or parse()
            // MAYBE add HH':'mm':'ss,SSS for OOo 1
            return new FormatGroup(xmlDF, createDateFormat("yyyy-MM-dd'T'HH':'mm':'ss", dateConf), createDateFormat("yyyy-MM-dd", dateConf));
        }
    }

    static private synchronized final void setDateConfig(final DateConfig newVal) {
        if (!newVal.equals(DATE_CONFIG)) {
            DATE_CONFIG = newVal;
            updateFormat();
        }
    }

    /**
     * Set the framework default time zone. Pass <code>null</code> to always use the VM default
     * (passing {@link TimeZone#getDefault()} would set the value once and for all and wouldn't be
     * changed by {@link TimeZone#setDefault(TimeZone)}).
     * 
     * @param tz the new default time zone, <code>null</code> to use the VM default.
     */
    static public synchronized final void setTimeZone(final TimeZone tz) {
        setDateConfig(DATE_CONFIG.setTimeZone(tz));
    }

    /**
     * The framework default time zone.
     * 
     * @param notNull <code>true</code> if <code>null</code> should be replaced by
     *        {@link TimeZone#getDefault()}.
     * @return the default time zone, can only be <code>null</code> if <code>notNull</code> is
     *         <code>false</code>.
     */
    static public synchronized final TimeZone getTimeZone(final boolean notNull) {
        return DATE_CONFIG.getTimeZone(notNull);
    }

    /**
     * Set the framework default locale. Pass <code>null</code> to always use the VM default
     * (passing {@link Locale#getDefault()} would set the value once and for all and wouldn't be
     * changed by {@link Locale#setDefault(Locale)}).
     * 
     * @param locale the new default locale, <code>null</code> to use the VM default.
     */
    static public synchronized final void setLocale(final Locale locale) {
        setDateConfig(DATE_CONFIG.setLocale(locale));
    }

    /**
     * The framework default locale.
     * 
     * @param notNull <code>true</code> if <code>null</code> should be replaced by
     *        {@link Locale#getDefault()}.
     * @return the default locale, can only be <code>null</code> if <code>notNull</code> is
     *         <code>false</code>.
     */
    static public synchronized final Locale getLocale(final boolean notNull) {
        return DATE_CONFIG.getLocale(notNull);
    }

    /**
     * Get the framework default calendar.
     * 
     * @return the default calendar.
     * @see #getTimeZone(boolean)
     * @see #getLocale(boolean)
     */
    static public synchronized final Calendar getCalendar() {
        return DATE_CONFIG.getCalendar();
    }

    static public synchronized final void setTimeZoneIgnored(final boolean b) {
        setDateConfig(DATE_CONFIG.setTimeZoneIgnored(b));
    }

    /**
     * Whether to ignore explicit time zone in dates. Prior to 4.1 LibreOffice would ignore explicit
     * time zones, i.e. "2013-11-15T12:00:00.000" and "2013-11-15T12:00:00.000+01:00" would both
     * parse to noon. As of 4.1 the first one parse to noon, the second one to 11 AM.
     * 
     * @return <code>true</code> if the time zone part should be ignored.
     */
    static public synchronized final boolean isTimeZoneIgnored() {
        return DATE_CONFIG.isTimeZoneIgnored();
    }

    /**
     * Parse an OpenDocument date value with the framework defaults.
     * 
     * @param date the string formatted value.
     * @return a calendar with the local time of the passed date.
     * @throws ParseException if the value couldn't be parsed.
     * @see #parseDateValue(String, TimeZone, Locale, Boolean)
     */
    static public synchronized Calendar parseDateValue(final String date) throws ParseException {
        return parseDateValue(date, null, null, null);
    }

    /**
     * Parse an OpenDocument date value with the passed parameters.
     * 
     * @param date the string formatted value.
     * @param tz the time zone of the returned calendar, <code>null</code> meaning
     *        {@link #getTimeZone(boolean)}.
     * @param locale the locale of the returned calendar, <code>null</code> meaning
     *        {@link #getLocale(boolean)}.
     * @param ignoreTZ whether to ignore the time zone part of <code>part</code>, <code>null</code>
     *        meaning {@link #isTimeZoneIgnored()}.
     * @return a calendar with the local time of the passed date.
     * @throws ParseException if the value couldn't be parsed.
     */
    static public synchronized Calendar parseDateValue(final String date, final TimeZone tz, final Locale locale, final Boolean ignoreTZ) throws ParseException {
        final DateConfig conf = new DateConfig(DATE_CONFIG, tz, locale, ignoreTZ);
        final Object parsed = getFormatParser(conf, false).parseObject(date);
        if (parsed instanceof Calendar) {
            // XMLCalendarFormat
            return (Calendar) parsed;
        } else {
            final Calendar res = conf.getCalendar();
            if (conf.isTimeZoneIgnored()) {
                // SimpleDateFormat
                res.setTime((Date) parsed);
                return res;
            } else {
                // XMLDateFormat or SimpleDateFormat
                final Calendar cal = Calendar.getInstance(UTC_TZ);
                cal.setTime((Date) parsed);
                return TimeUtils.copyLocalTime(cal, res);
            }
        }
    }
}