OpenConcerto

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

svn://code.openconcerto.org/openconcerto

Rev

Rev 93 | Go to most recent revision | Blame | Compare with Previous | Last modification | View Log | RSS feed

/*
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
 * 
 * Copyright 2011 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.generation;

import org.openconcerto.openoffice.ODSingleXMLDocument;
import org.openconcerto.openoffice.XMLFormatVersion;
import org.openconcerto.openoffice.generation.desc.ReportPart;
import org.openconcerto.openoffice.generation.desc.ReportType;
import org.openconcerto.openoffice.generation.desc.part.CaseReportPart;
import org.openconcerto.openoffice.generation.desc.part.ConditionalPart;
import org.openconcerto.openoffice.generation.desc.part.ForkReportPart;
import org.openconcerto.openoffice.generation.desc.part.GeneratorReportPart;
import org.openconcerto.openoffice.generation.desc.part.InsertReportPart;
import org.openconcerto.openoffice.generation.desc.part.SubReportPart;
import org.openconcerto.utils.RTInterruptedException;
import org.openconcerto.utils.Tuple2;

import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.io.IOException;
import java.text.DateFormat;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Stack;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;

import org.jdom.Element;
import org.jdom.JDOMException;
import org.jdom.filter.Filter;

import ognl.Ognl;
import ognl.OgnlException;
import ognl.OgnlRuntime;
import ognl.PropertyAccessor;

/**
 * Représente la génération d'un rapport.
 * 
 * @author Sylvain Cuaz
 * @param <C> type of GenerationCommon
 */
public class ReportGeneration<C extends GenerationCommon> {

    static {
        OgnlRuntime.setPropertyAccessor(Element.class, new PropertyAccessor() {
            public Object getProperty(Map context, Object target, Object name) {
                Element elem = (Element) target;
                String n = (String) name;
                // retourne le premier, TODO collections, attributes
                return elem.getChild(n);
            }

            public void setProperty(Map context, Object target, Object name, Object value) throws OgnlException {
                // impossible
                throw new OgnlException("", new UnsupportedOperationException("setProperty not supported on XML elements"));
            }
        });
    }

    static private final boolean isInterruptedExn(final Throwable e) {
        return e instanceof InterruptedException || e instanceof RTInterruptedException;
    }

    // instance members

    private final ReportType type;
    private C common;
    private XMLFormatVersion formatVersion;
    // Inheritable to allow generators to spawn threads
    private final InheritableThreadLocal<ReportPart> currentParts;
    private final InheritableThreadLocal<DocumentGenerator> currentGenerator;
    private Throwable interruptCause;
    // tous les générateurs s'exécuter dans ce groupe
    private final ThreadGroup thg;
    private final List<PropertyChangeListener> taskListeners;
    private final PropertyChangeListener taskListener;
    private Map<String, Object> commonData;

    /**
     * Crée une nouvelle instance pour générer un rapport.
     * 
     * @param type le type de rapport à générer.
     */
    public ReportGeneration(ReportType type) {
        this.type = new ReportType(type, this);
        // ne pas créer common tout de suite car il peut faire appel à des propriétés initialisées
        // seulement après dans le constructeur d'1 sous classe
        this.common = null;
        this.commonData = null;

        this.currentParts = new InheritableThreadLocal<ReportPart>();
        this.currentGenerator = new InheritableThreadLocal<DocumentGenerator>();
        this.interruptCause = null;
        this.thg = new ThreadGroup("Generateurs") {
            public void uncaughtException(Thread t, Throwable e) {
                ReportGeneration.this.interrupt(e);
            }
        };

        this.taskListeners = new ArrayList<PropertyChangeListener>();
        this.taskListener = new PropertyChangeListener() {
            public void propertyChange(PropertyChangeEvent evt) {
                for (final PropertyChangeListener l : ReportGeneration.this.taskListeners) {
                    l.propertyChange(evt);
                }
            }
        };
    }

    protected final ODSingleXMLDocument createTaskAndGenerate(GeneratorReportPart part) throws IOException, InterruptedException {
        final GenerationTask task = new GenerationTask(part.getName(), this.getCommon().createGenerator(part));
        task.addPropertyChangeListener(this.taskListener);
        try {
            synchronized (this) {
                this.currentGenerator.set(task.getGenerator());
            }
            final ODSingleXMLDocument res = task.generate();
            synchronized (this) {
                this.currentGenerator.set(null);
            }
            return res;
        } catch (IOException exn) {
            throw new IOException("Impossible de générer '" + part + "'", exn);
        }
    }

    public void addTaskListener(PropertyChangeListener l) {
        this.taskListeners.add(l);
    }

    /**
     * A new document generation has just begun.
     */
    protected void beginGeneration() {
    }

    /**
     * Whether to insert a page break between report parts. This implementation always return
     * <code>true</code>.
     * 
     * @return <code>true</code> if a page break should be inserted.
     */
    protected boolean pageBreak() {
        return true;
    }

    /**
     * The GenerationCommon needed for this generation. This implementation just returns a
     * {@link GenerationCommon}.
     * 
     * @param name name of the requested common.
     * @return the corresponding common.
     */
    @SuppressWarnings("unchecked")
    protected C createCommon(String name) {
        return (C) new GenerationCommon<ReportGeneration<?>>(this);
    }

    /**
     * Génére le rapport.
     * 
     * @return le fichier généré, ou <code>null</code> si interruption.
     * @throws Throwable si erreur lors de la génération.
     */
    public final ODSingleXMLDocument generate() throws Throwable {
        final Map<String, ODSingleXMLDocument> res = this.generateMulti();
        if (res.size() != 1)
            throw new IllegalStateException("more than one document: " + res);
        else
            return res.get(null);
    }

    /**
     * Generate a report with multiple documents. The main document has the <code>null</code> ID.
     * 
     * @return the generated documents, indexed by ID, or <code>null</code> if interrupted.
     * @throws Throwable if an error occurs.
     * @see #generate()
     */
    public final Map<String, ODSingleXMLDocument> generateMulti() throws Throwable {
        synchronized (this) {
            this.interruptCause = null;
        }

        Map<String, ODSingleXMLDocument> f = null;
        final FutureTask<Map<String, ODSingleXMLDocument>> future = new FutureTask<Map<String, ODSingleXMLDocument>>(new Callable<Map<String, ODSingleXMLDocument>>() {
            public Map<String, ODSingleXMLDocument> call() throws Exception {
                return createDocument();
            }
        });
        final Thread thr = new Thread(this.thg, future);
        thr.start();
        try {
            thr.join();
            f = future.get();
        } catch (Exception e) {
            if (isInterruptedExn(e) || (e instanceof ExecutionException && isInterruptedExn(e.getCause())))
                f = null;
            else
                this.interrupt(e);
        }

        final Map<String, ODSingleXMLDocument> res;
        synchronized (this) {
            if (this.interruptCause != null && !isInterruptedExn(this.interruptCause)) {
                throw this.interruptCause;
            } else if (Thread.currentThread().isInterrupted()) {
                res = null;
            } else {
                res = f;
            }
        }
        if (res != null) {
            for (final Entry<String, ODSingleXMLDocument> e : res.entrySet()) {
                this.getCommon().postProcessDocument(e.getValue());
            }
        }
        return res;
    }

    protected final void interrupt(Throwable cause) {
        synchronized (this) {
            if (this.interruptCause == null) {
                this.interruptCause = cause;
                this.thg.interrupt();
            }
        }
    }

    private Map<String, ODSingleXMLDocument> createDocument() throws IOException, OgnlException, InterruptedException {
        // recompute common data for each run
        this.commonData = null;
        this.beginGeneration();

        final ODSingleXMLDocument emptyDocument = this.createEmptyDocument();
        synchronized (this) {
            this.formatVersion = emptyDocument.getFormatVersion();
        }

        final Map<String, ODSingleXMLDocument> res = new HashMap<String, ODSingleXMLDocument>();
        res.put(null, emptyDocument);

        // les threads
        final Map<String, GenThread> forked = new HashMap<String, GenThread>();
        // a stack to handle SubReportPart (and their optional document)
        final Stack<Tuple2<Iterator<ReportPart>, ODSingleXMLDocument>> s = new Stack<Tuple2<Iterator<ReportPart>, ODSingleXMLDocument>>();
        s.push(Tuple2.create(this.type.getParts().iterator(), res.get(null)));
        while (hasNext(s) && !Thread.currentThread().isInterrupted()) {
            final Iterator<ReportPart> i = s.peek().get0();
            final ODSingleXMLDocument currentDoc = s.peek().get1();
            final ReportPart part = i.next();

            // always set current part, so that the condition can use it.
            synchronized (this) {
                this.currentParts.set(part);
            }
            if (this.mustGenerate(part)) {
                if (part instanceof ForkReportPart) {
                    GenThread thread = new GenThread(part.getName(), ((ForkReportPart) part).getChildren());
                    forked.put(part.getName(), thread);
                    thread.start();
                } else if (part instanceof SubReportPart) {
                    final SubReportPart subReportPart = (SubReportPart) part;
                    // the document for <sub>
                    final ODSingleXMLDocument newDoc;
                    final String docID = subReportPart.getDocumentID();
                    if (docID == null) {
                        newDoc = currentDoc;
                    } else if (res.containsKey(docID)) {
                        newDoc = res.get(docID);
                    } else if (subReportPart.isSinglePart()) {
                        newDoc = null;
                        res.put(docID, this.createTaskAndGenerate(((GeneratorReportPart) subReportPart.getChildren().iterator().next())));
                    } else {
                        newDoc = this.createEmptyDocument();
                        res.put(docID, newDoc);
                    }
                    // ajoute ses enfants
                    if (newDoc != null)
                        s.push(Tuple2.create(subReportPart.getChildren().iterator(), newDoc));
                } else if (part instanceof InsertReportPart) {
                    final GenThread thread = forked.get(part.getName());
                    if (thread == null)
                        throw new IllegalStateException(part.getName() + " has not been forked previously.");
                    final List<ODSingleXMLDocument> forkedList = thread.getRes();
                    if (forkedList == null) {
                        Thread.currentThread().interrupt();
                    } else {
                        for (final ODSingleXMLDocument doc : forkedList) {
                            add(currentDoc, doc);
                        }
                    }
                } else if (part instanceof CaseReportPart) {
                    s.push(Tuple2.create(((CaseReportPart) part).evaluate(this).iterator(), currentDoc));
                } else {
                    add(currentDoc, this.createTaskAndGenerate(((GeneratorReportPart) part)));
                }
            }
            synchronized (this) {
                this.currentParts.set(null);
            }
        }

        synchronized (this) {
            this.formatVersion = null;
        }

        return res;
    }

    private boolean hasNext(final Stack<Tuple2<Iterator<ReportPart>, ODSingleXMLDocument>> s) {
        if (s.peek().get0().hasNext())
            return true;
        else {
            // the current iterator is done, so remove it
            s.pop();
            if (s.isEmpty())
                return false;
            else
                return this.hasNext(s);
        }
    }

    private ODSingleXMLDocument createEmptyDocument() throws IOException, InterruptedException {
        final ODSingleXMLDocument f;
        final DocumentGenerator templateGenerator = this.getCommon().getStyleTemplateGenerator(this.type.getTemplate());
        if (templateGenerator == null)
            try {
                f = ODSingleXMLDocument.createFromPackage(this.type.getTemplate());
            } catch (JDOMException e) {
                throw new IOException("invalid template " + this.type.getTemplate(), e);
            }
        else
            f = templateGenerator.generate();

        // seulement intéressé par les styles et les user fields
        // TODO y passer dans fwk OO
        f.getBody().removeContent(new Filter() {
            public boolean matches(Object obj) {
                if (obj instanceof Element) {
                    final Element elem = (Element) obj;
                    final boolean isUserField = elem.getNamespace().equals(f.getVersion().getTEXT()) && elem.getName().equals("user-field-decls");
                    return !isUserField;
                } else
                    return true;
            }
        });

        this.getCommon().preProcessDocument(f);

        assert f != null;
        return f;
    }

    private final void add(final ODSingleXMLDocument f, final ODSingleXMLDocument toAdd) {
        // whether the added document is the first following the style template
        f.add(toAdd, f.getNumero() > 0 && pageBreak());
    }

    private final boolean mustGenerate(ReportPart part) throws OgnlException {
        if (part instanceof ConditionalPart) {
            final ConditionalPart p = (ConditionalPart) part;
            return p.getCondition() == null || evaluatePredicate(p.getCondition());
        } else
            return true;
    }

    public final boolean evaluatePredicate(String p) throws OgnlException {
        return ((Boolean) Ognl.getValue(p, getCommonData())).booleanValue();
    }

    /**
     * Une thread qui pour une liste de générateurs.
     */
    private class GenThread extends Thread {

        private final List m;
        private final List<ODSingleXMLDocument> res;

        public GenThread(String name, List generators) {
            super(name);
            this.m = generators;
            this.res = new ArrayList<ODSingleXMLDocument>(generators.size());
        }

        public void run() {
            final Iterator i = this.m.iterator();
            try {
                while (i.hasNext() && !Thread.currentThread().isInterrupted()) {
                    // ATTN les fork ne peuvent être imbriqués
                    final GeneratorReportPart part = (GeneratorReportPart) i.next();
                    this.res.add(ReportGeneration.this.createTaskAndGenerate(part));
                }
            } catch (Exception e) {
                ReportGeneration.this.interrupt(e);
            }
        }

        /**
         * Retourne la liste des documents générés.
         * 
         * @return la liste, ou bien <code>null</code> s'il y a interruption.
         */
        public final List<ODSingleXMLDocument> getRes() {
            // on s'assure d'avoir fini
            try {
                if (!this.isInterrupted()) {
                    this.join();
                    return this.res;
                }
            } catch (InterruptedException exn) {
                // justement on fait rien
            }
            return null;
        }
    }

    // *** getter

    /**
     * Ognl data used in the evaluation of conditions among other things.
     * 
     * @return a map of objects.
     */
    public final Map<String, Object> getCommonData() {
        if (this.commonData == null) {
            // set it before initializing it, that way even if initCommonData() needs a previous
            // value stored it won't loop infinitely (eg initCommonData() has
            // { put("a", "A") ; put("bPlus", getReportType().getParam("b")+"Plus"); }
            // and "b" is defined as a + "B" )
            this.commonData = new HashMap<String, Object>();
            this.initCommonData(this.commonData);
        }
        return Collections.unmodifiableMap(this.commonData);
    }

    protected void initCommonData(final Map<String, Object> res) {
        res.put("rg", this);
        res.put("variante", this.getReportType().getParam("variante"));
        res.put("dateFmt", DateFormat.getDateInstance(DateFormat.LONG));
        try {
            res.put("join", Ognl.getValue(":[@org.openconcerto.utils.CollectionUtils@join( #this, #sep == null ? ', ' : #sep )]", null));
            res.put("silentFirst", Ognl.getValue(":[#this.size == 0 ? null : #this[0]]", null));
        } catch (OgnlException exn) {
            // n'arrive jamais, la syntaxe est correcte
            exn.printStackTrace();
        }
    }

    public synchronized final XMLFormatVersion getFormatVersion() {
        return this.formatVersion;
    }

    @Override
    public String toString() {
        return "generation of '" + this.getReportType() + "'";
    }

    public final C getCommon() {
        if (this.common == null)
            this.common = this.createCommon(this.type.getCommon());
        return this.common;
    }

    public final ReportType getReportType() {
        return this.type;
    }

    public synchronized final ReportPart getCurrentPart() {
        return this.currentParts.get();
    }

    public synchronized final DocumentGenerator getCurrentGenerator() {
        return this.currentGenerator.get();
    }
}