1// BEGIN LICENSE BLOCK
2// Version: CMPL 1.1
3//
4// The contents of this file are subject to the Cisco-style Mozilla Public
5// License Version 1.1 (the "License"); you may not use this file except
6// in compliance with the License.  You may obtain a copy of the License
7// at www.eclipse-clp.org/license.
8//
9// Software distributed under the License is distributed on an "AS IS"
10// basis, WITHOUT WARRANTY OF ANY KIND, either express or implied.  See
11// the License for the specific language governing rights and limitations
12// under the License.
13//
14// The Original Code is  The ECLiPSe Constraint Logic Programming System.
15// The Initial Developer of the Original Code is  Cisco Systems, Inc.
16// Portions created by the Initial Developer are
17// Copyright (C) 2000 - 2006 Cisco Systems, Inc.  All Rights Reserved.
18//
19// Contributor(s): Stefano Novello / Josh Singer, Parc Technologies
20//
21// END LICENSE BLOCK
22
23//Title:        Java/ECLiPSe interface
24//Version:      $Id: EXDROutputStream.java,v 1.1 2006/09/23 01:54:10 snovello Exp $
25//Author:       Stefano Novello / Josh Singer
26//Company:      Parc Technologies
27//Description:  Output stream which can write EXDR format data.
28package com.parctechnologies.eclipse;
29import java.util.*;
30import java.io.*;
31import java.math.BigInteger;
32
33/**
34 * A stream which can write EXDR format.
35 *
36 * An <i>EXDROutputStream</i> can be constructed from any
37 * instance of the <i>OutputStream</i> class and extends it to be able to write
38 * outgoing data in the EXDR (ECLiPSe eXternal Data Representation) format.
39 * <p>
40 * Use the method {@link EXDROutputStream#write(Object)} to convert
41 * Java <i>CompoundTerm</i> objects and instances of other relevant Java classes into EXDR format
42 * so that data can be read in by ECLiPSe.
43 * <p>
44 * EXDROutputStream also uses a buffer: rather than writing to the underlying OutputStream
45 * byte-by-byte, EXDR data is written to the buffer and then copied to the OutputStream
46 * when a whole term has been completed.
47 * <p> Note that EXDROutputStream objects are often constructed using ToEclipseQueues.
48 * @see CompoundTerm
49 * @see ToEclipseQueue
50 *
51 */
52public class EXDROutputStream extends FilterOutputStream {
53
54    /**
55     * A data output stream is created to do most of the work since EXDR
56     * partially compatible with java's own format.
57     */
58    DataOutputStream out;
59
60    /**
61     * This ByteArrayOutputStream is used as a buffer so that data is not
62     * written to the underlying output stream byte by byte (this is inefficient).
63     * Instead it goes to the ByteArrayOutputStream, whose byte array grows as
64     * necessary. The byte array is copied onto the underlying output stream
65     * when a full term has been written, or when this stream is flushed.
66     */
67    ByteArrayOutputStream buf;
68
69    /**
70     * The following hash map is used to map EXDR strings to their
71     * EXDR string reference identifier. For strings that occur repeatedly
72     * throughout the message, there is a single EXDR string representation.
73     * The remainder of the occurrences are all references to the first
74     * occurrence of the string.
75     * The hash map is not synchronized. Multiple threads writing concurrently
76     * to the output stream is envisaged via the synchronized 'write()'.
77     * Should this change, the map should become a Hashtable or wrapped:
78     * Map m = Collections.synchronizedMap(new HashMap(...));
79     */
80    private HashMap stringReferenceMap = null;
81
82    private boolean compressStrings = false;
83
84    /**
85     * Construct an <i>EXDROutputStream</i> which will write
86     * EXDR to a named OutputStream.
87     */
88    public EXDROutputStream(OutputStream s)
89    {
90
91        // execute superclass's constructor, setting the underlying output
92        // stream to s. Later this is referenced as super.out.
93        super(s);
94        // create a byte array as an output stream
95        buf = new ByteArrayOutputStream();
96        // decorate byte array with a DataOutputStream (so we can write integers
97        // to it etc). This is where bytes will be written.
98        out = new DataOutputStream(buf);
99    }
100
101    /**
102     * Construct an <i>EXDROutputStream</i> which will write
103     * EXDR with optionally compressed strings to a named OutputStream.
104     */
105    public EXDROutputStream(OutputStream s, boolean compressStrings)
106    {
107        this(s);
108	this.compressStrings = compressStrings;
109    }
110
111    /**
112     * Write the version to the buffer as per EXDR format
113     */
114    private void writeVersion() throws IOException
115    {
116      out.writeByte('V');
117      out.writeByte(2);
118    }
119
120    /**
121     * Write an Object to the underlying stream in EXDR format. The Object (or if it is
122     * a compound term, its arguments, nested
123     * however deep) can be of any of the following acceptable EXDR output types:
124     * <ul>
125     * <li><code>null</code> - this is interpreted as a variable in ECLiPSe. </li>
126     * <li><i>String</i> objects.</li>
127     * <li>Anything implementing the <i>CompoundTerm</i> interface whose arguments are
128     * also instances of acceptable EXDR output types. </li>
129     * <li><i>Integer</i> objects.<li>
130     * <li><i>Double</i> or <i>Float</i> objects: these are interpreted as floats
131     * on the ECLiPSe side. Note that attempts to write Not-a-number (NaN) will raise an IllegalArgumentException.<li>
132     * <li>Any object implementing the <i>Collection</i> interface whose elements
133     * are all instances of acceptable EXDR output types. The collection is interpreted
134     * as an ECLiPSe list.</li>
135     * </ul>
136     * This is an atomic action, so that different threads
137     * writing to the same stream do not garble the data.
138     * @throws IllegalArgumentException if the parameter is of an unrecognised
139     * class, or if Not-a-number (NaN)-valued Floats or Doubles are
140     * supplied.
141     */
142    public synchronized void write(Object o) throws IOException
143    {
144       // write the version to the buffer
145       writeVersion();
146       // if compressing streams
147       if (compressStrings) {
148           // Write compression header byte
149           out.writeByte('C');
150           // Create the hash map used for EXDR string reference lookups
151           stringReferenceMap = new HashMap();
152       } else {
153           // Make sure the hash map is NULL just in case of previous exception etc
154           stringReferenceMap = null;
155       }
156       // write the term o to the buffer
157       writeSub(o);
158       // put all the bytes accumulated in the buffer on to the underlying
159       // output stream
160       //System.err.println("EXDROutStream "+this+" buf size="+buf.size());
161       flush_buffer();
162       // Dispose of the hash map (if created)
163       stringReferenceMap = null;
164    }
165
166  /**
167   * puts all the bytes accumulated in the buffer on to the underlying
168   * output stream.
169   */
170    private void flush_buffer() throws IOException
171    {
172      // convert the ByteArrayOutputStream to a byte array and write these
173      // bytes to the underlying OuputStream stored as an instance variable
174      // in the superclass
175      super.out.write(buf.toByteArray());
176      // reset (i.e. empty) the buffer
177      buf.reset();
178    }
179
180    /**
181     *  Write a term o's worth of bytes to the buffer.
182     */
183    void writeSub(Object o) throws IOException
184    {
185        if (o == null) // null means a variable
186            writeVar();
187        else if (o instanceof Collection) // any collection is a list
188            writeList((Collection) o);
189        else if (o instanceof CompoundTerm) // compound term
190            writeStructure((CompoundTerm) o);
191        else if (o instanceof Integer) // integer
192            writeInt(((Integer)o).intValue());
193        else if (o instanceof Long) // long
194            writeLong(((Long)o).longValue());
195        else if (o instanceof Double) // double
196            writeDouble(((Double)o).doubleValue());
197        else if (o instanceof Float) // float
198            // For floats, we convert a double, then write it
199            writeDouble(((Float) o).doubleValue());
200        else if (o instanceof String) // string
201            writeString(((String) o));
202        else // throw an IllegalArgument exception if the object is not of a
203             // recognised class
204            throw(new IllegalArgumentException("Attempt to write EXDR for non-"+
205                  "recognised class "+o.getClass().getName()));
206    }
207
208    /**
209     * Write a double to the buffer in EXDR format.
210     *
211     * @throws IllegalArgumentException if NaN (not-a-number) is passed in as
212     * the parameter.
213     */
214    void writeDouble(double d) throws IOException
215    {
216        if(Double.isNaN(d))
217        {
218          throw(new IllegalArgumentException("NaN cannot be written"+
219           " in EXDR format."));
220        }
221        out.writeByte('D');
222        out.writeDouble(d);
223    }
224
225    /**
226     * Write a string to the buffer in EXDR format
227     */
228    void writeString(String s) throws IOException
229    {
230        int val;
231        boolean wroteReference = false;
232
233        /**
234         * In EXDR version 2, we have added a form of string
235         * compression to reduce the message sizes. If a string
236         * has previously been written to the stream then we
237         * just write out a reference to it. In addition,
238         * if the length of the string < 128 we write an XDR_nat
239         * not an XDR_int;
240         */
241
242        if (stringReferenceMap != null) {
243            /**
244             * We don't allow mappings of keys to a 'null' value.
245             * The return of 'null' really does indicate that there
246             * is not a key-value pair in the map.
247             */
248            Integer strRefID = (Integer)stringReferenceMap.get(s);
249
250            if ( strRefID == null ) {
251                // No previous occurrences, write as a EXDR string
252                stringReferenceMap.put(s, new Integer(stringReferenceMap.size()));
253                out.writeByte('S');
254                val = s.length();
255            } else {
256                // Previous occurrences, write as a EXDR string reference
257                out.writeByte('R');
258                val = strRefID.intValue();
259                wroteReference = true;
260            }
261        } else {
262            out.writeByte('S');
263            val = s.length();
264        }
265
266        // Can we write the Length/Index as a XDR_nat?
267        if (val == (int)(byte)val) {
268            out.writeByte(val | 0x80);
269        } else {
270            out.writeInt(val);
271        }
272
273        // If writing an EXDR string, write the characters
274        if (!wroteReference) {
275            out.writeBytes(s);
276        }
277    }
278
279    /**
280     * Write an integer to the buffer in EXDR format
281     */
282    void writeInt(int i) throws IOException
283    {
284        if ( i == (int) (byte)i )
285        {
286            out.writeByte('B');
287            out.writeByte((byte)i);
288
289        } else {
290            out.writeByte('I');
291            out.writeInt(i);
292        }
293    }
294
295    /**
296     * Write a long to the buffer in EXDR format. However, if the number
297     * is small enough to be represented as a regular int, an int is
298     * written instead.
299     */
300    void writeLong(long l) throws IOException
301    {
302        if (l == (long) (int) l)
303                writeInt((int) l);
304        else
305        {
306            out.writeByte('J');
307            out.writeLong(l);
308        }
309    }
310
311    /**
312     * Write a variable to the buffer in EXDR format
313     */
314    void writeVar() throws IOException
315    {
316        out.writeByte('_');
317    }
318
319    /**
320     * Write a compound term to the buffer in EXDR format
321     */
322    void writeStructure(CompoundTerm term) throws IOException
323    {
324        out.writeByte('F');
325        // arity
326        out.writeInt(term.arity());
327        // functor
328        writeString(term.functor());
329        // and arguments
330        for( int i=1 ; i <= term.arity() ; i++)
331            writeSub(term.arg(i));
332    }
333
334    /**
335     * Write a list to the buffer in EXDR format. Order of list is as per
336     * iterator.
337     */
338    void writeList(Collection collection) throws IOException
339    {
340        // get an iterator
341        Iterator i = collection.iterator();
342        while (i.hasNext())
343        {
344            // write each element
345            out.writeByte('[');
346            writeSub(i.next());
347        }
348        out.writeByte(']');
349    }
350
351    /**
352     * Flushes the underlying <i>OutputStream</i>.
353     */
354    public void flush() throws IOException
355    {
356      // make sure any buffered data is already flushed on to super.out.
357      flush_buffer();
358      super.out.flush();
359    }
360
361    /**
362     * Enable / disable string compression for subsequently written terms.
363     */
364    public void enableCompression(boolean compress)
365    {
366	this.compressStrings = compress;
367    }
368}
369