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.sql.element;

import org.openconcerto.sql.Log;
import org.openconcerto.sql.ShowAs;
import org.openconcerto.sql.model.DBRoot;
import org.openconcerto.sql.model.DBStructureItemNotFound;
import org.openconcerto.sql.model.DBSystemRoot;
import org.openconcerto.sql.model.SQLName;
import org.openconcerto.sql.model.SQLTable;
import org.openconcerto.sql.request.SQLFieldTranslator;
import org.openconcerto.utils.CollectionMap2;
import org.openconcerto.utils.CollectionUtils;
import org.openconcerto.utils.SetMap;
import org.openconcerto.utils.cc.Cookies;
import org.openconcerto.utils.cc.ITransformer;

import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;

import net.jcip.annotations.GuardedBy;

/**
 * Directory of SQLElement by table. <code>final</code> so that {@link SQLElement} can't depend on a
 * subclass and can be shared between applications (e.g. using
 * {@link #putAll(SQLElementDirectory)}). If some elements needs shared state, use
 * {@link #getCookies()}.
 * 
 * @author Sylvain CUAZ
 */
public final class SQLElementDirectory {

    private final DBSystemRoot systemRoot;
    private final Map<SQLTable, SQLElement> elements;
    private final SetMap<String, SQLTable> tableNames;
    private final SetMap<String, SQLTable> byCode;
    private final SetMap<Class<? extends SQLElement>, SQLTable> byClass;
    private final List<DirectoryListener> listeners;

    private String phrasesPkgName;

    @GuardedBy("this")
    private SQLFieldTranslator translator;
    private final ShowAs showAs;

    private final Cookies cookies;

    public SQLElementDirectory(DBSystemRoot systemRoot) {
        this.systemRoot = systemRoot;
        this.elements = new HashMap<SQLTable, SQLElement>();
        // to mimic elements behaviour, if we add twice the same table
        // the second one should replace the first one
        this.tableNames = new SetMap<String, SQLTable>();
        this.byCode = new SetMap<String, SQLTable>();
        this.byClass = new SetMap<Class<? extends SQLElement>, SQLTable>();

        this.listeners = new ArrayList<DirectoryListener>();

        this.phrasesPkgName = null;

        this.showAs = new ShowAs((DBRoot) null);
        this.cookies = new Cookies();
    }

    public synchronized final void destroy() {
        for (final SQLElement elem : this.elements.values()) {
            elem.destroy();
        }
    }

    public final ShowAs getShowAs() {
        return this.showAs;
    }

    public final synchronized void setTranslator(SQLFieldTranslator translator) {
        if (translator.getDirectory() != this)
            throw new IllegalArgumentException("Not for this : " + translator);
        this.translator = translator;
    }

    public synchronized final SQLFieldTranslator getTranslator() {
        return this.translator;
    }

    public final Cookies getCookies() {
        return this.cookies;
    }

    private static <K> SQLTable getSoleTable(SetMap<K, SQLTable> m, K key) throws IllegalArgumentException {
        final Collection<SQLTable> res = m.getNonNull(key);
        if (res.size() > 1)
            throw new IllegalArgumentException(key + " is not unique: " + CollectionUtils.join(res, ",", new ITransformer<SQLTable, SQLName>() {
                @Override
                public SQLName transformChecked(SQLTable input) {
                    return input.getSQLName();
                }
            }));
        return CollectionUtils.getSole(res);
    }

    public synchronized final void putAll(SQLElementDirectory o) {
        // Cookies are needed in addSQLElement() (e.g. by directoryChanged() and getShowAs())
        this.cookies.putAll(o.getCookies());
        // copy since addSQLElement() removes elements from their previous directory
        for (final SQLElement elem : new ArrayList<>(o.getElements())) {
            if (!this.contains(elem.getTable()))
                this.addSQLElement(elem);
        }
        this.translator.putAll(o.getTranslator());
    }

    /**
     * Add an element by creating it with the no-arg constructor. If the element cannot find its
     * table and thus raise DBStructureItemNotFound, the exception is logged.
     * 
     * @param element the element to add.
     */
    public final void addSQLElement(final Class<? extends SQLElement> element) {
        this.addSQLElement(element, null);
    }

    public final void addSQLElement(final Class<? extends SQLElement> element, final DBRoot root) {
        final SQLElement newInstance;
        try {
            if (root == null)
                newInstance = element.getConstructor().newInstance();
            else
                newInstance = element.getConstructor(DBRoot.class).newInstance(root);
        } catch (InvocationTargetException e) {
            if (e.getCause() instanceof DBStructureItemNotFound) {
                Log.get().config("ignore inexistent tables: " + e.getCause().getLocalizedMessage());
                return;
            }
            throw new IllegalArgumentException("Constructor failed", e);
        } catch (Exception e) {
            throw new IllegalArgumentException("Couldn't use constructor", e);
        }
        this.addSQLElement(newInstance);
    }

    /**
     * Adds an already instantiated element.
     * 
     * @param elem the SQLElement to add.
     * @return the previously added element.
     */
    public synchronized final SQLElement addSQLElement(SQLElement elem) {
        if (elem.getTable().getDBSystemRoot() != this.systemRoot) {
            System.err.println("SQLElementDirectory.addSQLElement() warning : Not in this system root : " + elem + " " + elem.getTable().getDBSystemRoot() + "  != " + this.systemRoot);
        }
        final SQLElement res = this.removeSQLElement(elem.getTable());
        this.elements.put(elem.getTable(), elem);
        this.tableNames.add(elem.getTable().getName(), elem.getTable());
        this.byCode.add(elem.getCode(), elem.getTable());
        this.byClass.add(elem.getClass(), elem.getTable());

        final CollectionMap2<String, ? extends List<String>, String> sa = elem.getShowAs();
        if (sa != null) {
            for (final Entry<String, ? extends List<String>> e : sa.entrySet()) {
                try {
                    if (e.getKey() == null)
                        this.showAs.show(elem.getTable(), e.getValue());
                    else
                        this.showAs.show(elem.getTable().getField(e.getKey()), e.getValue());
                } catch (RuntimeException exn) {
                    throw new IllegalStateException("Couldn't add showAs for " + elem + " : " + e, exn);
                }
            }
        }

        for (final DirectoryListener dl : this.listeners) {
            dl.elementAdded(elem);
        }
        elem.setDirectory(this);
        return res;
    }

    public synchronized final boolean contains(SQLTable t) {
        return this.elements.containsKey(t);
    }

    public synchronized final SQLElement getElement(SQLTable t) {
        return this.elements.get(t);
    }

    /**
     * Search for a table whose name is <code>tableName</code>.
     * 
     * @param tableName a table name, e.g. "ADRESSE".
     * @return the corresponding SQLElement, or <code>null</code> if there is no table named
     *         <code>tableName</code>.
     * @throws IllegalArgumentException if more than one table match.
     */
    public synchronized final SQLElement getElement(String tableName) {
        return this.getElement(getSoleTable(this.tableNames, tableName));
    }

    /**
     * Search for an SQLElement which is an {@link Class#isInstance(Object) instance of}
     * <code>clazz</code>.
     * 
     * @param <S> type of SQLElement
     * @param clazz the class.
     * @return the corresponding SQLElement, or <code>null</code> if none can be found.
     * @throws IllegalArgumentException if there's more than one match.
     * @see #getElementsOfClass(Class, boolean)
     */
    public final <S extends SQLElement> S getElement(Class<S> clazz) {
        return this.getElementOfClass(clazz, true);
    }

    public final <S extends SQLElement> S getElementOfClass(Class<S> clazz, final boolean allowSubclass) {
        final List<S> res = getElementsOfClass(clazz, allowSubclass);
        if (res.size() > 1)
            throw new IllegalArgumentException(clazz + " is not unique: " + res);
        return res.isEmpty() ? null : res.get(0);
    }

    /**
     * Search for SQLElement whose class is <code>clazz</code>.
     * 
     * @param clazz the class.
     * @param allowSubclass <code>true</code> to include {@link Class#isInstance(Object)
     *        subclasses}, <code>false</code> to only return exact match.
     * @return the corresponding elements.
     */
    public final <S extends SQLElement> List<S> getElementsOfClass(final Class<S> clazz, final boolean allowSubclass) {
        final List<S> res;
        synchronized (this) {
            if (allowSubclass) {
                res = new ArrayList<>();
                for (final SQLElement elem : this.elements.values()) {
                    if (clazz.isInstance(elem))
                        res.add(clazz.cast(elem));
                }
            } else {
                final Set<SQLTable> tables = this.byClass.get(clazz);
                if (tables != null) {
                    res = new ArrayList<>(tables.size());
                    for (final SQLTable t : tables) {
                        res.add(clazz.cast(this.getElement(t)));
                    }
                } else {
                    res = Collections.emptyList();
                }
            }
        }
        if (res.isEmpty()) {
            return Collections.emptyList();
        } else if (res.size() == 1) {
            return Collections.singletonList(res.get(0));
        } else {
            return Collections.unmodifiableList(res);
        }
    }

    public synchronized final SQLElement getElementForCode(String code) {
        return this.getElement(getSoleTable(this.byCode, code));
    }

    public synchronized final Set<SQLTable> getTables() {
        return this.getElementsMap().keySet();
    }

    public synchronized final Collection<SQLElement> getElements() {
        return this.getElementsMap().values();
    }

    public final Map<SQLTable, SQLElement> getElementsMap() {
        return Collections.unmodifiableMap(this.elements);
    }

    /**
     * Remove the passed instance. NOTE: this method only remove the specific instance passed, so
     * it's a conditional <code>removeSQLElement(elem.getTable())</code>.
     * 
     * @param elem the instance to remove.
     * @see #removeSQLElement(SQLTable)
     */
    public synchronized void removeSQLElement(SQLElement elem) {
        if (this.getElement(elem.getTable()) == elem)
            this.removeSQLElement(elem.getTable());
    }

    /**
     * Remove the element for the passed table.
     * 
     * @param t the table to remove.
     * @return the removed element, can be <code>null</code>.
     */
    public synchronized SQLElement removeSQLElement(SQLTable t) {
        final SQLElement elem = this.elements.remove(t);
        if (elem != null) {
            this.tableNames.removeOne(elem.getTable().getName(), elem.getTable());
            this.byCode.removeOne(elem.getCode(), elem.getTable());
            this.byClass.removeOne(elem.getClass(), elem.getTable());
            // MAYBE only reset neighbours.
            for (final SQLElement otherElem : this.elements.values())
                otherElem.resetRelationships();
            elem.setDirectory(null);
            this.showAs.removeTable(elem.getTable());
            for (final DirectoryListener dl : this.listeners) {
                dl.elementRemoved(elem);
            }
        }
        return elem;
    }

    public synchronized final void initL18nPackageName(final String baseName) {
        if (this.phrasesPkgName != null)
            throw new IllegalStateException("Already initialized : " + this.getL18nPackageName());
        this.phrasesPkgName = baseName;
    }

    public synchronized final String getL18nPackageName() {
        return this.phrasesPkgName;
    }

    public synchronized final void addListener(DirectoryListener dl) {
        this.listeners.add(dl);
    }

    public synchronized final void removeListener(DirectoryListener dl) {
        this.listeners.remove(dl);
    }

    static public interface DirectoryListener {
        void elementAdded(SQLElement elem);

        void elementRemoved(SQLElement elem);
    }
}