OpenConcerto

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

svn://code.openconcerto.org/openconcerto

Rev

Rev 132 | Details | Compare with Previous | Last modification | View Log | RSS feed

Rev Author Line No. Line
17 ilm 1
/*
2
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
3
 *
4
 * Copyright 2011 OpenConcerto, by ILM Informatique. All rights reserved.
5
 *
6
 * The contents of this file are subject to the terms of the GNU General Public License Version 3
7
 * only ("GPL"). You may not use this file except in compliance with the License. You can obtain a
8
 * copy of the License at http://www.gnu.org/licenses/gpl-3.0.html See the License for the specific
9
 * language governing permissions and limitations under the License.
10
 *
11
 * When distributing the software, include this License Header Notice in each file.
12
 */
13
 
14
 package org.openconcerto.openoffice.generation;
15
 
16
import org.openconcerto.openoffice.ODSingleXMLDocument;
132 ilm 17
import org.openconcerto.openoffice.XMLFormatVersion;
17 ilm 18
import org.openconcerto.openoffice.generation.desc.ReportPart;
19
import org.openconcerto.openoffice.generation.desc.ReportType;
65 ilm 20
import org.openconcerto.openoffice.generation.desc.part.CaseReportPart;
17 ilm 21
import org.openconcerto.openoffice.generation.desc.part.ConditionalPart;
22
import org.openconcerto.openoffice.generation.desc.part.ForkReportPart;
23
import org.openconcerto.openoffice.generation.desc.part.GeneratorReportPart;
24
import org.openconcerto.openoffice.generation.desc.part.InsertReportPart;
25
import org.openconcerto.openoffice.generation.desc.part.SubReportPart;
93 ilm 26
import org.openconcerto.utils.RTInterruptedException;
17 ilm 27
import org.openconcerto.utils.Tuple2;
28
 
29
import java.beans.PropertyChangeEvent;
30
import java.beans.PropertyChangeListener;
31
import java.io.IOException;
32
import java.text.DateFormat;
33
import java.util.ArrayList;
34
import java.util.Collections;
35
import java.util.HashMap;
36
import java.util.Iterator;
37
import java.util.List;
38
import java.util.Map;
83 ilm 39
import java.util.Map.Entry;
17 ilm 40
import java.util.Stack;
41
import java.util.concurrent.Callable;
42
import java.util.concurrent.FutureTask;
180 ilm 43
import java.util.concurrent.TimeoutException;
17 ilm 44
 
132 ilm 45
import org.jdom.Element;
46
import org.jdom.JDOMException;
47
import org.jdom.filter.Filter;
48
 
180 ilm 49
import net.jcip.annotations.GuardedBy;
17 ilm 50
import ognl.Ognl;
51
import ognl.OgnlException;
52
import ognl.OgnlRuntime;
53
import ognl.PropertyAccessor;
54
 
55
/**
56
 * Représente la génération d'un rapport.
57
 *
58
 * @author Sylvain Cuaz
59
 * @param <C> type of GenerationCommon
60
 */
61
public class ReportGeneration<C extends GenerationCommon> {
62
 
63
    static {
64
        OgnlRuntime.setPropertyAccessor(Element.class, new PropertyAccessor() {
65
            public Object getProperty(Map context, Object target, Object name) {
66
                Element elem = (Element) target;
67
                String n = (String) name;
68
                // retourne le premier, TODO collections, attributes
69
                return elem.getChild(n);
70
            }
71
 
72
            public void setProperty(Map context, Object target, Object name, Object value) throws OgnlException {
73
                // impossible
74
                throw new OgnlException("", new UnsupportedOperationException("setProperty not supported on XML elements"));
75
            }
76
        });
77
    }
78
 
93 ilm 79
    static private final boolean isInterruptedExn(final Throwable e) {
80
        return e instanceof InterruptedException || e instanceof RTInterruptedException;
81
    }
82
 
17 ilm 83
    // instance members
84
 
85
    private final ReportType type;
86
    private C common;
132 ilm 87
    private XMLFormatVersion formatVersion;
17 ilm 88
    // Inheritable to allow generators to spawn threads
89
    private final InheritableThreadLocal<ReportPart> currentParts;
90
    private final InheritableThreadLocal<DocumentGenerator> currentGenerator;
180 ilm 91
    @GuardedBy("this")
17 ilm 92
    private Throwable interruptCause;
93
    // tous les générateurs s'exécuter dans ce groupe
180 ilm 94
    @GuardedBy("this")
95
    private List<Thread> thg;
17 ilm 96
    private final List<PropertyChangeListener> taskListeners;
97
    private final PropertyChangeListener taskListener;
98
    private Map<String, Object> commonData;
99
 
100
    /**
101
     * Crée une nouvelle instance pour générer un rapport.
102
     *
103
     * @param type le type de rapport à générer.
104
     */
105
    public ReportGeneration(ReportType type) {
106
        this.type = new ReportType(type, this);
107
        // ne pas créer common tout de suite car il peut faire appel à des propriétés initialisées
108
        // seulement après dans le constructeur d'1 sous classe
109
        this.common = null;
110
        this.commonData = null;
111
 
112
        this.currentParts = new InheritableThreadLocal<ReportPart>();
113
        this.currentGenerator = new InheritableThreadLocal<DocumentGenerator>();
114
        this.interruptCause = null;
180 ilm 115
        this.thg = null;
17 ilm 116
 
117
        this.taskListeners = new ArrayList<PropertyChangeListener>();
118
        this.taskListener = new PropertyChangeListener() {
119
            public void propertyChange(PropertyChangeEvent evt) {
120
                for (final PropertyChangeListener l : ReportGeneration.this.taskListeners) {
121
                    l.propertyChange(evt);
122
                }
123
            }
124
        };
125
    }
126
 
127
    protected final ODSingleXMLDocument createTaskAndGenerate(GeneratorReportPart part) throws IOException, InterruptedException {
128
        final GenerationTask task = new GenerationTask(part.getName(), this.getCommon().createGenerator(part));
129
        task.addPropertyChangeListener(this.taskListener);
130
        try {
131
            synchronized (this) {
132
                this.currentGenerator.set(task.getGenerator());
133
            }
134
            final ODSingleXMLDocument res = task.generate();
135
            synchronized (this) {
136
                this.currentGenerator.set(null);
137
            }
138
            return res;
139
        } catch (IOException exn) {
140
            throw new IOException("Impossible de générer '" + part + "'", exn);
141
        }
142
    }
143
 
144
    public void addTaskListener(PropertyChangeListener l) {
145
        this.taskListeners.add(l);
146
    }
147
 
148
    /**
149
     * A new document generation has just begun.
150
     */
151
    protected void beginGeneration() {
152
    }
153
 
154
    /**
155
     * Whether to insert a page break between report parts. This implementation always return
156
     * <code>true</code>.
157
     *
158
     * @return <code>true</code> if a page break should be inserted.
159
     */
160
    protected boolean pageBreak() {
161
        return true;
162
    }
163
 
164
    /**
165
     * The GenerationCommon needed for this generation. This implementation just returns a
166
     * {@link GenerationCommon}.
167
     *
168
     * @param name name of the requested common.
169
     * @return the corresponding common.
170
     */
171
    @SuppressWarnings("unchecked")
172
    protected C createCommon(String name) {
20 ilm 173
        return (C) new GenerationCommon<ReportGeneration<?>>(this);
17 ilm 174
    }
175
 
176
    /**
177
     * Génére le rapport.
178
     *
179
     * @return le fichier généré, ou <code>null</code> si interruption.
180
     * @throws Throwable si erreur lors de la génération.
181
     */
182
    public final ODSingleXMLDocument generate() throws Throwable {
183
        final Map<String, ODSingleXMLDocument> res = this.generateMulti();
184
        if (res.size() != 1)
185
            throw new IllegalStateException("more than one document: " + res);
186
        else
187
            return res.get(null);
188
    }
189
 
190
    /**
191
     * Generate a report with multiple documents. The main document has the <code>null</code> ID.
192
     *
193
     * @return the generated documents, indexed by ID, or <code>null</code> if interrupted.
194
     * @throws Throwable if an error occurs.
195
     * @see #generate()
196
     */
197
    public final Map<String, ODSingleXMLDocument> generateMulti() throws Throwable {
198
        synchronized (this) {
199
            this.interruptCause = null;
180 ilm 200
            this.thg = new ArrayList<>();
17 ilm 201
        }
202
 
203
        Map<String, ODSingleXMLDocument> f = null;
204
        final FutureTask<Map<String, ODSingleXMLDocument>> future = new FutureTask<Map<String, ODSingleXMLDocument>>(new Callable<Map<String, ODSingleXMLDocument>>() {
205
            public Map<String, ODSingleXMLDocument> call() throws Exception {
206
                return createDocument();
207
            }
208
        });
180 ilm 209
        // Don't pass a ThreadGroup, since every thread created, including "cache timeout", JDBC
210
        // threads will be in it. But these threads must out-live this generation.
211
        final Thread thr = new Thread(null, future);
212
        this.registerThread(thr);
17 ilm 213
        thr.start();
214
        try {
215
            f = future.get();
216
        } catch (Exception e) {
180 ilm 217
            assert f == null;
218
            // If one thread was interrupted (or failed), interrupt the others
219
            this.interrupt(e);
220
        } finally {
221
            // Make sure all threads are stopped and don't prevent this from being garbage
222
            // collected.
223
            final List<Thread> toJoin;
224
            synchronized (this) {
225
                toJoin = this.thg;
226
                this.thg = null;
227
            }
228
            for (final Thread t : toJoin) {
229
                // If no exception occurred, then should already be finished but if there was one in
230
                // a thread, another thread might be stuck on some I/O for a while before it can
231
                // process the interrupt.
232
                t.join(4500);
233
                if (t.isAlive())
234
                    throw new TimeoutException("Thread still not terminated : " + t);
235
            }
17 ilm 236
        }
237
 
238
        final Map<String, ODSingleXMLDocument> res;
239
        synchronized (this) {
93 ilm 240
            if (this.interruptCause != null && !isInterruptedExn(this.interruptCause)) {
17 ilm 241
                throw this.interruptCause;
93 ilm 242
            } else if (Thread.currentThread().isInterrupted()) {
17 ilm 243
                res = null;
244
            } else {
245
                res = f;
246
            }
247
        }
83 ilm 248
        if (res != null) {
249
            for (final Entry<String, ODSingleXMLDocument> e : res.entrySet()) {
250
                this.getCommon().postProcessDocument(e.getValue());
251
            }
252
        }
17 ilm 253
        return res;
254
    }
255
 
180 ilm 256
    public final synchronized void registerThread(final Thread thr) {
257
        this.thg.add(thr);
258
    }
259
 
17 ilm 260
    protected final void interrupt(Throwable cause) {
261
        synchronized (this) {
262
            if (this.interruptCause == null) {
263
                this.interruptCause = cause;
180 ilm 264
                for (final Thread thr : this.thg) {
265
                    thr.interrupt();
266
                }
17 ilm 267
            }
268
        }
269
    }
270
 
271
    private Map<String, ODSingleXMLDocument> createDocument() throws IOException, OgnlException, InterruptedException {
272
        // recompute common data for each run
273
        this.commonData = null;
274
        this.beginGeneration();
275
 
132 ilm 276
        final ODSingleXMLDocument emptyDocument = this.createEmptyDocument();
277
        synchronized (this) {
278
            this.formatVersion = emptyDocument.getFormatVersion();
279
        }
280
 
17 ilm 281
        final Map<String, ODSingleXMLDocument> res = new HashMap<String, ODSingleXMLDocument>();
132 ilm 282
        res.put(null, emptyDocument);
17 ilm 283
 
284
        // les threads
285
        final Map<String, GenThread> forked = new HashMap<String, GenThread>();
286
        // a stack to handle SubReportPart (and their optional document)
287
        final Stack<Tuple2<Iterator<ReportPart>, ODSingleXMLDocument>> s = new Stack<Tuple2<Iterator<ReportPart>, ODSingleXMLDocument>>();
288
        s.push(Tuple2.create(this.type.getParts().iterator(), res.get(null)));
289
        while (hasNext(s) && !Thread.currentThread().isInterrupted()) {
290
            final Iterator<ReportPart> i = s.peek().get0();
291
            final ODSingleXMLDocument currentDoc = s.peek().get1();
292
            final ReportPart part = i.next();
293
 
294
            // always set current part, so that the condition can use it.
295
            synchronized (this) {
296
                this.currentParts.set(part);
297
            }
298
            if (this.mustGenerate(part)) {
299
                if (part instanceof ForkReportPart) {
300
                    GenThread thread = new GenThread(part.getName(), ((ForkReportPart) part).getChildren());
301
                    forked.put(part.getName(), thread);
180 ilm 302
                    this.registerThread(thread);
17 ilm 303
                    thread.start();
304
                } else if (part instanceof SubReportPart) {
305
                    final SubReportPart subReportPart = (SubReportPart) part;
306
                    // the document for <sub>
307
                    final ODSingleXMLDocument newDoc;
308
                    final String docID = subReportPart.getDocumentID();
73 ilm 309
                    if (docID == null) {
17 ilm 310
                        newDoc = currentDoc;
73 ilm 311
                    } else if (res.containsKey(docID)) {
17 ilm 312
                        newDoc = res.get(docID);
73 ilm 313
                    } else if (subReportPart.isSinglePart()) {
314
                        newDoc = null;
315
                        res.put(docID, this.createTaskAndGenerate(((GeneratorReportPart) subReportPart.getChildren().iterator().next())));
17 ilm 316
                    } else {
317
                        newDoc = this.createEmptyDocument();
318
                        res.put(docID, newDoc);
319
                    }
320
                    // ajoute ses enfants
73 ilm 321
                    if (newDoc != null)
322
                        s.push(Tuple2.create(subReportPart.getChildren().iterator(), newDoc));
17 ilm 323
                } else if (part instanceof InsertReportPart) {
324
                    final GenThread thread = forked.get(part.getName());
325
                    if (thread == null)
326
                        throw new IllegalStateException(part.getName() + " has not been forked previously.");
327
                    final List<ODSingleXMLDocument> forkedList = thread.getRes();
328
                    if (forkedList == null) {
329
                        Thread.currentThread().interrupt();
330
                    } else {
331
                        for (final ODSingleXMLDocument doc : forkedList) {
332
                            add(currentDoc, doc);
333
                        }
334
                    }
65 ilm 335
                } else if (part instanceof CaseReportPart) {
336
                    s.push(Tuple2.create(((CaseReportPart) part).evaluate(this).iterator(), currentDoc));
17 ilm 337
                } else {
338
                    add(currentDoc, this.createTaskAndGenerate(((GeneratorReportPart) part)));
339
                }
340
            }
341
            synchronized (this) {
342
                this.currentParts.set(null);
343
            }
344
        }
132 ilm 345
 
346
        synchronized (this) {
347
            this.formatVersion = null;
348
        }
349
 
17 ilm 350
        return res;
351
    }
352
 
353
    private boolean hasNext(final Stack<Tuple2<Iterator<ReportPart>, ODSingleXMLDocument>> s) {
354
        if (s.peek().get0().hasNext())
355
            return true;
356
        else {
357
            // the current iterator is done, so remove it
358
            s.pop();
359
            if (s.isEmpty())
360
                return false;
361
            else
362
                return this.hasNext(s);
363
        }
364
    }
365
 
366
    private ODSingleXMLDocument createEmptyDocument() throws IOException, InterruptedException {
367
        final ODSingleXMLDocument f;
368
        final DocumentGenerator templateGenerator = this.getCommon().getStyleTemplateGenerator(this.type.getTemplate());
369
        if (templateGenerator == null)
370
            try {
57 ilm 371
                f = ODSingleXMLDocument.createFromPackage(this.type.getTemplate());
17 ilm 372
            } catch (JDOMException e) {
373
                throw new IOException("invalid template " + this.type.getTemplate(), e);
374
            }
375
        else
376
            f = templateGenerator.generate();
377
 
378
        // seulement intéressé par les styles et les user fields
379
        // TODO y passer dans fwk OO
380
        f.getBody().removeContent(new Filter() {
381
            public boolean matches(Object obj) {
382
                if (obj instanceof Element) {
383
                    final Element elem = (Element) obj;
384
                    final boolean isUserField = elem.getNamespace().equals(f.getVersion().getTEXT()) && elem.getName().equals("user-field-decls");
385
                    return !isUserField;
386
                } else
387
                    return true;
388
            }
389
        });
390
 
391
        this.getCommon().preProcessDocument(f);
392
 
73 ilm 393
        assert f != null;
17 ilm 394
        return f;
395
    }
396
 
397
    private final void add(final ODSingleXMLDocument f, final ODSingleXMLDocument toAdd) {
398
        // whether the added document is the first following the style template
399
        f.add(toAdd, f.getNumero() > 0 && pageBreak());
400
    }
401
 
402
    private final boolean mustGenerate(ReportPart part) throws OgnlException {
403
        if (part instanceof ConditionalPart) {
404
            final ConditionalPart p = (ConditionalPart) part;
65 ilm 405
            return p.getCondition() == null || evaluatePredicate(p.getCondition());
17 ilm 406
        } else
407
            return true;
408
    }
409
 
65 ilm 410
    public final boolean evaluatePredicate(String p) throws OgnlException {
411
        return ((Boolean) Ognl.getValue(p, getCommonData())).booleanValue();
412
    }
413
 
17 ilm 414
    /**
415
     * Une thread qui pour une liste de générateurs.
416
     */
417
    private class GenThread extends Thread {
418
 
419
        private final List m;
420
        private final List<ODSingleXMLDocument> res;
421
 
422
        public GenThread(String name, List generators) {
423
            super(name);
424
            this.m = generators;
425
            this.res = new ArrayList<ODSingleXMLDocument>(generators.size());
426
        }
427
 
428
        public void run() {
429
            final Iterator i = this.m.iterator();
430
            try {
431
                while (i.hasNext() && !Thread.currentThread().isInterrupted()) {
432
                    // ATTN les fork ne peuvent être imbriqués
433
                    final GeneratorReportPart part = (GeneratorReportPart) i.next();
434
                    this.res.add(ReportGeneration.this.createTaskAndGenerate(part));
435
                }
436
            } catch (Exception e) {
437
                ReportGeneration.this.interrupt(e);
438
            }
439
        }
440
 
441
        /**
442
         * Retourne la liste des documents générés.
443
         *
444
         * @return la liste, ou bien <code>null</code> s'il y a interruption.
445
         */
446
        public final List<ODSingleXMLDocument> getRes() {
447
            // on s'assure d'avoir fini
448
            try {
449
                if (!this.isInterrupted()) {
450
                    this.join();
451
                    return this.res;
452
                }
453
            } catch (InterruptedException exn) {
454
                // justement on fait rien
455
            }
456
            return null;
457
        }
458
    }
459
 
460
    // *** getter
461
 
462
    /**
463
     * Ognl data used in the evaluation of conditions among other things.
464
     *
465
     * @return a map of objects.
466
     */
467
    public final Map<String, Object> getCommonData() {
468
        if (this.commonData == null) {
469
            // set it before initializing it, that way even if initCommonData() needs a previous
470
            // value stored it won't loop infinitely (eg initCommonData() has
471
            // { put("a", "A") ; put("bPlus", getReportType().getParam("b")+"Plus"); }
472
            // and "b" is defined as a + "B" )
473
            this.commonData = new HashMap<String, Object>();
474
            this.initCommonData(this.commonData);
475
        }
476
        return Collections.unmodifiableMap(this.commonData);
477
    }
478
 
479
    protected void initCommonData(final Map<String, Object> res) {
480
        res.put("rg", this);
481
        res.put("variante", this.getReportType().getParam("variante"));
482
        res.put("dateFmt", DateFormat.getDateInstance(DateFormat.LONG));
483
        try {
484
            res.put("join", Ognl.getValue(":[@org.openconcerto.utils.CollectionUtils@join( #this, #sep == null ? ', ' : #sep )]", null));
485
            res.put("silentFirst", Ognl.getValue(":[#this.size == 0 ? null : #this[0]]", null));
486
        } catch (OgnlException exn) {
487
            // n'arrive jamais, la syntaxe est correcte
488
            exn.printStackTrace();
489
        }
490
    }
491
 
132 ilm 492
    public synchronized final XMLFormatVersion getFormatVersion() {
493
        return this.formatVersion;
494
    }
495
 
496
    @Override
17 ilm 497
    public String toString() {
498
        return "generation of '" + this.getReportType() + "'";
499
    }
500
 
501
    public final C getCommon() {
502
        if (this.common == null)
503
            this.common = this.createCommon(this.type.getCommon());
504
        return this.common;
505
    }
506
 
507
    public final ReportType getReportType() {
508
        return this.type;
509
    }
510
 
511
    public synchronized final ReportPart getCurrentPart() {
512
        return this.currentParts.get();
513
    }
514
 
515
    public synchronized final DocumentGenerator getCurrentGenerator() {
516
        return this.currentGenerator.get();
517
    }
518
}