001/* ===========================================================
002 * JFreeChart : a free chart library for the Java(tm) platform
003 * ===========================================================
004 *
005 * (C) Copyright 2000-2020, by Object Refinery Limited and Contributors.
006 *
007 * Project Info:  http://www.jfree.org/jfreechart/index.html
008 *
009 * This library is free software; you can redistribute it and/or modify it
010 * under the terms of the GNU Lesser General Public License as published by
011 * the Free Software Foundation; either version 2.1 of the License, or
012 * (at your option) any later version.
013 *
014 * This library is distributed in the hope that it will be useful, but
015 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
016 * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
017 * License for more details.
018 *
019 * You should have received a copy of the GNU Lesser General Public
020 * License along with this library; if not, write to the Free Software
021 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301,
022 * USA.
023 *
024 * [Oracle and Java are registered trademarks of Oracle and/or its affiliates. 
025 * Other names may be trademarks of their respective owners.]
026 *
027 * ----------------
028 * SerialUtils.java
029 * ----------------
030 * (C) Copyright 2000-2020, by Object Refinery Limited and Contributors.
031 *
032 * Original Author:  David Gilbert (for Object Refinery Limited);
033 * Contributors:     -;
034 */
035
036package org.jfree.chart.util;
037
038import java.awt.AlphaComposite;
039import java.awt.BasicStroke;
040import java.awt.Color;
041import java.awt.Composite;
042import java.awt.GradientPaint;
043import java.awt.Paint;
044import java.awt.Shape;
045import java.awt.Stroke;
046import java.awt.geom.Arc2D;
047import java.awt.geom.Ellipse2D;
048import java.awt.geom.GeneralPath;
049import java.awt.geom.Line2D;
050import java.awt.geom.PathIterator;
051import java.awt.geom.Point2D;
052import java.awt.geom.Rectangle2D;
053import java.io.IOException;
054import java.io.ObjectInputStream;
055import java.io.ObjectOutputStream;
056import java.io.Serializable;
057import java.text.AttributedCharacterIterator;
058import java.text.AttributedString;
059import java.text.CharacterIterator;
060import java.util.HashMap;
061import java.util.Map;
062
063/**
064 * A class containing useful utility methods relating to serialization.
065 */
066public class SerialUtils {
067
068    /**
069     * Private constructor prevents object creation.
070     */
071    private SerialUtils() {
072    }
073
074    /**
075     * Returns {@code true} if a class implements {@code Serializable}
076     * and {@code false} otherwise.
077     *
078     * @param c  the class.
079     *
080     * @return A boolean.
081     */
082    public static boolean isSerializable(Class c) {
083        return (Serializable.class.isAssignableFrom(c));
084    }
085
086    /**
087     * Reads a {@code Paint} object that has been serialised by the
088     * {@link #writePaint(Paint, ObjectOutputStream)} method.
089     *
090     * @param stream  the input stream ({@code null} not permitted).
091     *
092     * @return The paint object (possibly {@code null}).
093     *
094     * @throws IOException  if there is an I/O problem.
095     * @throws ClassNotFoundException  if there is a problem loading a class.
096     */
097    public static Paint readPaint(ObjectInputStream stream)
098        throws IOException, ClassNotFoundException {
099
100        if (stream == null) {
101            throw new IllegalArgumentException("Null 'stream' argument.");
102        }
103        Paint result = null;
104        boolean isNull = stream.readBoolean();
105        if (!isNull) {
106            final Class c = (Class) stream.readObject();
107            if (isSerializable(c)) {
108                result = (Paint) stream.readObject();
109            }
110            else if (c.equals(GradientPaint.class)) {
111                float x1 = stream.readFloat();
112                float y1 = stream.readFloat();
113                Color c1 = (Color) stream.readObject();
114                float x2 = stream.readFloat();
115                float y2 = stream.readFloat();
116                Color c2 = (Color) stream.readObject();
117                boolean isCyclic = stream.readBoolean();
118                result = new GradientPaint(x1, y1, c1, x2, y2, c2, isCyclic);
119            }
120        }
121        return result;
122
123    }
124
125    /**
126     * Serialises a {@code Paint} object.
127     *
128     * @param paint  the paint object ({@code null} permitted).
129     * @param stream  the output stream ({@code null} not permitted).
130     *
131     * @throws IOException if there is an I/O error.
132     */
133    public static void writePaint(Paint paint, ObjectOutputStream stream)
134        throws IOException {
135
136        if (stream == null) {
137            throw new IllegalArgumentException("Null 'stream' argument.");
138        }
139        if (paint != null) {
140            stream.writeBoolean(false);
141            stream.writeObject(paint.getClass());
142            if (paint instanceof Serializable) {
143                stream.writeObject(paint);
144            }
145            else if (paint instanceof GradientPaint) {
146                final GradientPaint gp = (GradientPaint) paint;
147                stream.writeFloat((float) gp.getPoint1().getX());
148                stream.writeFloat((float) gp.getPoint1().getY());
149                stream.writeObject(gp.getColor1());
150                stream.writeFloat((float) gp.getPoint2().getX());
151                stream.writeFloat((float) gp.getPoint2().getY());
152                stream.writeObject(gp.getColor2());
153                stream.writeBoolean(gp.isCyclic());
154            }
155        }
156        else {
157            stream.writeBoolean(true);
158        }
159
160    }
161
162    /**
163     * Reads a {@code Stroke} object that has been serialised by the
164     * {@link #writeStroke(Stroke, ObjectOutputStream)} method.
165     *
166     * @param stream  the input stream ({@code null} not permitted).
167     *
168     * @return The stroke object (possibly {@code null}).
169     *
170     * @throws IOException  if there is an I/O problem.
171     * @throws ClassNotFoundException  if there is a problem loading a class.
172     */
173    public static Stroke readStroke(ObjectInputStream stream)
174        throws IOException, ClassNotFoundException {
175
176        if (stream == null) {
177            throw new IllegalArgumentException("Null 'stream' argument.");
178        }
179        Stroke result = null;
180        boolean isNull = stream.readBoolean();
181        if (!isNull) {
182            Class c = (Class) stream.readObject();
183            if (c.equals(BasicStroke.class)) {
184                float width = stream.readFloat();
185                int cap = stream.readInt();
186                int join = stream.readInt();
187                float miterLimit = stream.readFloat();
188                float[] dash = (float[]) stream.readObject();
189                float dashPhase = stream.readFloat();
190                result = new BasicStroke(width, cap, join, miterLimit, dash, 
191                        dashPhase);
192            }
193            else {
194                result = (Stroke) stream.readObject();
195            }
196        }
197        return result;
198
199    }
200
201    /**
202     * Serialises a {@code Stroke} object.  This code handles the
203     * {@code BasicStroke} class which is the only {@code Stroke}
204     * implementation provided by the JDK (and isn't directly
205     * {@code Serializable}).
206     *
207     * @param stroke  the stroke object ({@code null} permitted).
208     * @param stream  the output stream ({@code null} not permitted).
209     *
210     * @throws IOException if there is an I/O error.
211     */
212    public static void writeStroke(Stroke stroke, ObjectOutputStream stream)
213            throws IOException {
214
215        if (stream == null) {
216            throw new IllegalArgumentException("Null 'stream' argument.");
217        }
218        if (stroke != null) {
219            stream.writeBoolean(false);
220            if (stroke instanceof BasicStroke) {
221                BasicStroke s = (BasicStroke) stroke;
222                stream.writeObject(BasicStroke.class);
223                stream.writeFloat(s.getLineWidth());
224                stream.writeInt(s.getEndCap());
225                stream.writeInt(s.getLineJoin());
226                stream.writeFloat(s.getMiterLimit());
227                stream.writeObject(s.getDashArray());
228                stream.writeFloat(s.getDashPhase());
229            } else {
230                stream.writeObject(stroke.getClass());
231                stream.writeObject(stroke);
232            }
233        } else {
234            stream.writeBoolean(true);
235        }
236    }
237
238    /**
239     * Reads a {@code Composite} object that has been serialised by the
240     * {@link #writeComposite(Composite, ObjectOutputStream)}
241     * method.
242     *
243     * @param stream  the input stream ({@code null} not permitted).
244     *
245     * @return The composite object (possibly {@code null}).
246     *
247     * @throws IOException  if there is an I/O problem.
248     * @throws ClassNotFoundException  if there is a problem loading a class.
249     * 
250     * @since 1.0.17
251     */
252    public static Composite readComposite(ObjectInputStream stream)
253            throws IOException, ClassNotFoundException {
254
255        if (stream == null) {
256            throw new IllegalArgumentException("Null 'stream' argument.");
257        }
258        Composite result = null;
259        boolean isNull = stream.readBoolean();
260        if (!isNull) {
261            Class c = (Class) stream.readObject();
262            if (isSerializable(c)) {
263                result = (Composite) stream.readObject();
264            }
265            else if (c.equals(AlphaComposite.class)) {
266                int rule = stream.readInt();
267                float alpha = stream.readFloat();
268                result = AlphaComposite.getInstance(rule, alpha);
269            }
270        }
271        return result;
272
273    }
274
275    /**
276     * Serialises a {@code Composite} object.
277     *
278     * @param composite  the composite object ({@code null} permitted).
279     * @param stream  the output stream ({@code null} not permitted).
280     *
281     * @throws IOException if there is an I/O error.
282     * 
283     * @since 1.0.17
284     */
285    public static void writeComposite(Composite composite, 
286            ObjectOutputStream stream) throws IOException {
287
288        if (stream == null) {
289            throw new IllegalArgumentException("Null 'stream' argument.");
290        }
291        if (composite != null) {
292            stream.writeBoolean(false);
293            stream.writeObject(composite.getClass());
294            if (composite instanceof Serializable) {
295                stream.writeObject(composite);
296            }
297            else if (composite instanceof AlphaComposite) {
298                AlphaComposite ac = (AlphaComposite) composite;
299                stream.writeInt(ac.getRule());
300                stream.writeFloat(ac.getAlpha());
301            }
302        } else {
303            stream.writeBoolean(true);
304        }
305    }
306
307    /**
308     * Reads a {@code Shape} object that has been serialised by the
309     * {@link #writeShape(Shape, ObjectOutputStream)} method.
310     *
311     * @param stream  the input stream ({@code null} not permitted).
312     *
313     * @return The shape object (possibly {@code null}).
314     *
315     * @throws IOException  if there is an I/O problem.
316     * @throws ClassNotFoundException  if there is a problem loading a class.
317     */
318    public static Shape readShape(ObjectInputStream stream)
319        throws IOException, ClassNotFoundException {
320
321        if (stream == null) {
322            throw new IllegalArgumentException("Null 'stream' argument.");
323        }
324        Shape result = null;
325        boolean isNull = stream.readBoolean();
326        if (!isNull) {
327            Class c = (Class) stream.readObject();
328            if (c.equals(Line2D.class)) {
329                double x1 = stream.readDouble();
330                double y1 = stream.readDouble();
331                double x2 = stream.readDouble();
332                double y2 = stream.readDouble();
333                result = new Line2D.Double(x1, y1, x2, y2);
334            }
335            else if (c.equals(Rectangle2D.class)) {
336                double x = stream.readDouble();
337                double y = stream.readDouble();
338                double w = stream.readDouble();
339                double h = stream.readDouble();
340                result = new Rectangle2D.Double(x, y, w, h);
341            }
342            else if (c.equals(Ellipse2D.class)) {
343                double x = stream.readDouble();
344                double y = stream.readDouble();
345                double w = stream.readDouble();
346                double h = stream.readDouble();
347                result = new Ellipse2D.Double(x, y, w, h);
348            }
349            else if (c.equals(Arc2D.class)) {
350                double x = stream.readDouble();
351                double y = stream.readDouble();
352                double w = stream.readDouble();
353                double h = stream.readDouble();
354                double as = stream.readDouble(); // Angle Start
355                double ae = stream.readDouble(); // Angle Extent
356                int at = stream.readInt();       // Arc type
357                result = new Arc2D.Double(x, y, w, h, as, ae, at);
358            }
359            else if (c.equals(GeneralPath.class)) {
360                GeneralPath gp = new GeneralPath();
361                float[] args = new float[6];
362                boolean hasNext = stream.readBoolean();
363                while (!hasNext) {
364                    int type = stream.readInt();
365                    for (int i = 0; i < 6; i++) {
366                        args[i] = stream.readFloat();
367                    }
368                    switch (type) {
369                        case PathIterator.SEG_MOVETO :
370                            gp.moveTo(args[0], args[1]);
371                            break;
372                        case PathIterator.SEG_LINETO :
373                            gp.lineTo(args[0], args[1]);
374                            break;
375                        case PathIterator.SEG_CUBICTO :
376                            gp.curveTo(args[0], args[1], args[2],
377                                    args[3], args[4], args[5]);
378                            break;
379                        case PathIterator.SEG_QUADTO :
380                            gp.quadTo(args[0], args[1], args[2], args[3]);
381                            break;
382                        case PathIterator.SEG_CLOSE :
383                            gp.closePath();
384                            break;
385                        default :
386                            throw new RuntimeException(
387                                    "JFreeChart - No path exists");
388                    }
389                    gp.setWindingRule(stream.readInt());
390                    hasNext = stream.readBoolean();
391                }
392                result = gp;
393            }
394            else {
395                result = (Shape) stream.readObject();
396            }
397        }
398        return result;
399
400    }
401
402    /**
403     * Serialises a {@code Shape} object.
404     *
405     * @param shape  the shape object ({@code null} permitted).
406     * @param stream  the output stream ({@code null} not permitted).
407     *
408     * @throws IOException if there is an I/O error.
409     */
410    public static void writeShape(Shape shape, ObjectOutputStream stream)
411            throws IOException {
412
413        if (stream == null) {
414            throw new IllegalArgumentException("Null 'stream' argument.");
415        }
416        if (shape != null) {
417            stream.writeBoolean(false);
418            if (shape instanceof Line2D) {
419                final Line2D line = (Line2D) shape;
420                stream.writeObject(Line2D.class);
421                stream.writeDouble(line.getX1());
422                stream.writeDouble(line.getY1());
423                stream.writeDouble(line.getX2());
424                stream.writeDouble(line.getY2());
425            }
426            else if (shape instanceof Rectangle2D) {
427                final Rectangle2D rectangle = (Rectangle2D) shape;
428                stream.writeObject(Rectangle2D.class);
429                stream.writeDouble(rectangle.getX());
430                stream.writeDouble(rectangle.getY());
431                stream.writeDouble(rectangle.getWidth());
432                stream.writeDouble(rectangle.getHeight());
433            }
434            else if (shape instanceof Ellipse2D) {
435                final Ellipse2D ellipse = (Ellipse2D) shape;
436                stream.writeObject(Ellipse2D.class);
437                stream.writeDouble(ellipse.getX());
438                stream.writeDouble(ellipse.getY());
439                stream.writeDouble(ellipse.getWidth());
440                stream.writeDouble(ellipse.getHeight());
441            }
442            else if (shape instanceof Arc2D) {
443                final Arc2D arc = (Arc2D) shape;
444                stream.writeObject(Arc2D.class);
445                stream.writeDouble(arc.getX());
446                stream.writeDouble(arc.getY());
447                stream.writeDouble(arc.getWidth());
448                stream.writeDouble(arc.getHeight());
449                stream.writeDouble(arc.getAngleStart());
450                stream.writeDouble(arc.getAngleExtent());
451                stream.writeInt(arc.getArcType());
452            }
453            else if (shape instanceof GeneralPath) {
454                stream.writeObject(GeneralPath.class);
455                final PathIterator pi = shape.getPathIterator(null);
456                final float[] args = new float[6];
457                stream.writeBoolean(pi.isDone());
458                while (!pi.isDone()) {
459                    final int type = pi.currentSegment(args);
460                    stream.writeInt(type);
461                    // TODO: could write this to only stream the values
462                    // required for the segment type
463                    for (int i = 0; i < 6; i++) {
464                        stream.writeFloat(args[i]);
465                    }
466                    stream.writeInt(pi.getWindingRule());
467                    pi.next();
468                    stream.writeBoolean(pi.isDone());
469                }
470            }
471            else {
472                stream.writeObject(shape.getClass());
473                stream.writeObject(shape);
474            }
475        }
476        else {
477            stream.writeBoolean(true);
478        }
479    }
480
481    /**
482     * Reads a {@code Point2D} object that has been serialised by the
483     * {@link #writePoint2D(Point2D, ObjectOutputStream)} method.
484     *
485     * @param stream  the input stream ({@code null} not permitted).
486     *
487     * @return The point object (possibly {@code null}).
488     *
489     * @throws IOException  if there is an I/O problem.
490     */
491    public static Point2D readPoint2D(ObjectInputStream stream)
492            throws IOException {
493
494        if (stream == null) {
495            throw new IllegalArgumentException("Null 'stream' argument.");
496        }
497        Point2D result = null;
498        boolean isNull = stream.readBoolean();
499        if (!isNull) {
500            double x = stream.readDouble();
501            double y = stream.readDouble();
502            result = new Point2D.Double(x, y);
503        }
504        return result;
505
506    }
507
508    /**
509     * Serialises a {@code Point2D} object.
510     *
511     * @param p  the point object ({@code null} permitted).
512     * @param stream  the output stream ({@code null} not permitted).
513     *
514     * @throws IOException if there is an I/O error.
515     */
516    public static void writePoint2D(Point2D p, ObjectOutputStream stream)
517            throws IOException {
518
519        if (stream == null) {
520            throw new IllegalArgumentException("Null 'stream' argument.");
521        }
522        if (p != null) {
523            stream.writeBoolean(false);
524            stream.writeDouble(p.getX());
525            stream.writeDouble(p.getY());
526        }
527        else {
528            stream.writeBoolean(true);
529        }
530    }
531
532    /**
533     * Reads a {@code AttributedString} object that has been serialised by
534     * the {@link #writeAttributedString(AttributedString,
535     * ObjectOutputStream)} method.
536     *
537     * @param stream  the input stream ({@code null} not permitted).
538     *
539     * @return The attributed string object (possibly {@code null}).
540     *
541     * @throws IOException  if there is an I/O problem.
542     * @throws ClassNotFoundException  if there is a problem loading a class.
543     */
544    public static AttributedString readAttributedString(
545            ObjectInputStream stream)
546            throws IOException, ClassNotFoundException {
547
548        if (stream == null) {
549            throw new IllegalArgumentException("Null 'stream' argument.");
550        }
551        AttributedString result = null;
552        final boolean isNull = stream.readBoolean();
553        if (!isNull) {
554            // read string and attributes then create result
555            String plainStr = (String) stream.readObject();
556            result = new AttributedString(plainStr);
557            char c = stream.readChar();
558            int start = 0;
559            while (c != CharacterIterator.DONE) {
560                int limit = stream.readInt();
561                Map atts = (Map) stream.readObject();
562                result.addAttributes(atts, start, limit);
563                start = limit;
564                c = stream.readChar();
565            }
566        }
567        return result;
568    }
569
570    /**
571     * Serialises an {@code AttributedString} object.
572     *
573     * @param as  the attributed string object ({@code null} permitted).
574     * @param stream  the output stream ({@code null} not permitted).
575     *
576     * @throws IOException if there is an I/O error.
577     */
578    public static void writeAttributedString(AttributedString as,
579            ObjectOutputStream stream) throws IOException {
580
581        if (stream == null) {
582            throw new IllegalArgumentException("Null 'stream' argument.");
583        }
584        if (as != null) {
585            stream.writeBoolean(false);
586            AttributedCharacterIterator aci = as.getIterator();
587            // build a plain string from aci
588            // then write the string
589            StringBuffer plainStr = new StringBuffer();
590            char current = aci.first();
591            while (current != CharacterIterator.DONE) {
592                plainStr = plainStr.append(current);
593                current = aci.next();
594            }
595            stream.writeObject(plainStr.toString());
596
597            // then write the attributes and limits for each run
598            current = aci.first();
599            int begin = aci.getBeginIndex();
600            while (current != CharacterIterator.DONE) {
601                // write the current character - when the reader sees that this
602                // is not CharacterIterator.DONE, it will know to read the
603                // run limits and attributes
604                stream.writeChar(current);
605
606                // now write the limit, adjusted as if beginIndex is zero
607                int limit = aci.getRunLimit();
608                stream.writeInt(limit - begin);
609
610                // now write the attribute set
611                Map atts = new HashMap(aci.getAttributes());
612                stream.writeObject(atts);
613                current = aci.setIndex(limit);
614            }
615            // write a character that signals to the reader that all runs
616            // are done...
617            stream.writeChar(CharacterIterator.DONE);
618        }
619        else {
620            // write a flag that indicates a null
621            stream.writeBoolean(true);
622        }
623
624    }
625
626}