OpenConcerto

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

svn://code.openconcerto.org/openconcerto

Rev

Rev 156 | 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.sql.view.search;

import org.openconcerto.utils.FormatGroup;

import java.text.Format;
import java.text.Normalizer;
import java.text.Normalizer.Form;
import java.text.ParsePosition;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Pattern;

/**
 * Search a text in an object (first in toString() then using formats).
 * 
 * @author Sylvain
 */
public class TextSearchSpec implements SearchSpec {

    public static enum Mode {
        CONTAINS, CONTAINS_STRICT, LESS_THAN, EQUALS, EQUALS_STRICT, GREATER_THAN
    }

    // cannot use Collator : it doesn't works for CONTAINS
    static private final Pattern thoroughPattern = Pattern.compile("(\\p{Punct}|\\p{InCombiningDiacriticalMarks})+");
    static private final Pattern multipleSpacesPattern = Pattern.compile("\\p{Space}+");

    private final Mode mode;
    private final String filterString, normalizedFilterString;
    private final Map<Class<?>, FormatGroup> formats;
    // parsing of filterString for each format
    private final Map<Format, Object> parsedFilter;
    private Double parsedFilterD;
    private boolean parsedFilterD_tried = false;

    public TextSearchSpec(String filterString) {
        this(filterString, Mode.CONTAINS);
    }

    public TextSearchSpec(String filterString, final Mode mode) {
        this.mode = mode;
        this.filterString = filterString;
        this.normalizedFilterString = normalize(filterString);
        this.formats = new HashMap<>();
        this.parsedFilter = new HashMap<>();
    }

    private String normalize(String s) {
        if (this.mode == Mode.CONTAINS_STRICT || this.mode == Mode.EQUALS_STRICT) {
            return s.trim();
        } else {
            final String sansAccents = thoroughPattern.matcher(Normalizer.normalize(s.trim(), Form.NFD)).replaceAll("");
            return multipleSpacesPattern.matcher(sansAccents).replaceAll(" ").toLowerCase();
        }
    }

    private final Object getParsed(final Format fmt) {
        Object res;
        if (this.parsedFilter.containsKey(fmt)) {
            res = this.parsedFilter.get(fmt);
        } else {
            final ParsePosition pp = new ParsePosition(0);
            res = fmt.parseObject(this.filterString, pp);
            // don't allow "25/12/05foobar" or worse "25/12/05 13:00" (parsing to "25/12/05 00:00")
            if (pp.getErrorIndex() >= 0 || pp.getIndex() < this.filterString.length())
                res = null;
            else
                assert res != null : "Cannot tell apart parsing failed from parsed to null";
            this.parsedFilter.put(fmt, res);
        }
        return res;
    }

    private final String format(final Format fmt, final Object cell) {
        try {
            return fmt.format(cell);
        } catch (final Exception e) {
            throw new IllegalStateException("Couldn't format " + cell + '(' + getClass(cell) + ") with " + fmt, e);
        }
    }

    static private String getClass(Object cell) {
        return cell == null ? "<null>" : cell.getClass().getName();
    }

    private final Double getDouble() {
        if (!this.parsedFilterD_tried) {
            try {
                this.parsedFilterD = Double.valueOf(this.filterString);
            } catch (final NumberFormatException e) {
                this.parsedFilterD = null;
            }
            this.parsedFilterD_tried = true;
        }
        return this.parsedFilterD;
    }

    private boolean matchWithFormats(Object cell) {
        if (cell == null)
            return false;

        // return now since only the toString() of strings can be sorted (it makes no sense to sort
        // 12/25/2010)
        if (cell.getClass() == String.class)
            return test(cell.toString());

        final boolean containsOrEquals = isContainsOrEquals();
        final boolean isContains = isContains();

        // first an inexpensive comparison
        if (containsOrEquals && containsOrEquals(cell.toString()))
            return true;

        // then try to format the cell
        final FormatGroup fg = getFormat(cell);
        if (fg != null) {
            final List<? extends Format> fmts = fg.getFormats();
            final int stop = fmts.size();
            for (int i = 0; i < stop; i++) {
                final Format fmt = fmts.get(i);
                // e.g. test if "2006" is contained in "25 déc. 2010"

                // for the equals mode we can either format the cell value and compare it to the
                // user typed string, or we can parse the user typed string and compare it to the
                // cell value.
                // The problem with the first approach is that some formats lose information (e.g. a
                // date format for a timestamp loses the time part : 25/12/2010 13:00 would be
                // formatted to 25/12/2010 thus "= 25/12/2010" would select all times of that day,
                // which is better achieved with contains)
                // PS: the date format is useful for parsing since "> 25/12/2010" means
                // "> 25/12/2010 00:00" which is understandable and concise.
                if (isContains && containsOrEquals(format(fmt, cell)))
                    return true;
                // e.g. test if "01/01/2006" is before "25 déc. 2010"
                else if (!isContains && test(getParsed(fmt), cell))
                    return true;
            }
        } else if (!isContains && cell instanceof Number) {
            final Number n = (Number) cell;
            if (test(this.getDouble(), n.doubleValue()))
                return true;
        }
        return false;
    }

    private boolean test(final String searched) {
        final String normalized = normalize(searched);
        if (isContains())
            return normalized.indexOf(this.normalizedFilterString) >= 0;
        else if (this.mode == Mode.EQUALS || this.mode == Mode.EQUALS_STRICT)
            return normalized.equals(this.normalizedFilterString);
        else if (this.mode == Mode.LESS_THAN)
            return normalized.compareTo(this.normalizedFilterString) <= 0;
        else if (this.mode == Mode.GREATER_THAN)
            return normalized.compareTo(this.normalizedFilterString) >= 0;
        else
            throw new IllegalArgumentException("unknown mode " + this.mode);
    }

    private boolean isContainsOrEquals() {
        // only real Strings can be sorted (it makes no sense to sort 12/25/2010)
        return this.mode != Mode.LESS_THAN && this.mode != Mode.GREATER_THAN;
    }

    private boolean isContains() {
        return this.mode == Mode.CONTAINS || this.mode == Mode.CONTAINS_STRICT;
    }

    private boolean containsOrEquals(final String searched) {
        // don't normalize otherwise 2005-06-20 matches 2006 :
        // searched is first formatted to 20/06/2005 then normalized to 20062005
        if (isContains()) {
            return searched.indexOf(this.filterString) >= 0;
        } else {
            assert this.mode == Mode.EQUALS || this.mode == Mode.EQUALS_STRICT : "Only call contains() if isContainsOrEquals()";
            return searched.equals(this.filterString);
        }
    }

    @SuppressWarnings("unchecked")
    private boolean test(Object search, final Object cell) {
        assert !(this.mode == Mode.CONTAINS || this.mode == Mode.CONTAINS_STRICT) : "Only call test() if not isContains()";
        if (search == null)
            return false;

        if (cell instanceof Comparable) {
            final Comparable c = (Comparable<?>) cell;
            if (this.mode == Mode.EQUALS || this.mode == Mode.EQUALS_STRICT) {
                // allow to compare Timestamp & Date
                return c.compareTo(search) == 0;
            } else if (this.mode == Mode.LESS_THAN) {
                return c.compareTo(search) <= 0;
            } else {
                assert this.mode == Mode.GREATER_THAN;
                return c.compareTo(search) >= 0;
            }
        } else {
            if (this.mode == Mode.EQUALS || this.mode == Mode.EQUALS_STRICT)
                return cell.equals(search);
            else
                return false;
        }
    }

    private FormatGroup getFormat(Object cell) {
        final Class<?> clazz = cell.getClass();
        if (!this.formats.containsKey(clazz)) {
            // cache the findings (eg sql.Date can be formatted like util.Date)
            this.formats.put(clazz, findFormat(clazz));
        }
        return this.formats.get(clazz);
    }

    // find if there's a format for cell
    // 1st tries its class, then goes up the hierarchy
    private FormatGroup findFormat(final Class<?> clazz) {
        Class<?> c = clazz;
        FormatGroup res = null;
        while (res == null && c != Object.class) {
            res = this.formats.get(c);
            c = c.getSuperclass();
        }
        return res;
    }

    @Override
    public boolean match(Object line) {
        return this.isEmpty() || matchWithFormats(line);
    }

    @Override
    public String toString() {
        return this.getClass().getSimpleName() + ":" + this.filterString;
    }

    @Override
    public boolean isEmpty() {
        return this.filterString == null || this.filterString.length() == 0;
    }

    public void setFormats(Map<Class<?>, FormatGroup> formats) {
        this.formats.clear();
        this.formats.putAll(formats);
    }
}