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