Source code: com/synchrona/jred/IrOBEX.java
1 /*
2 **************************************************************************
3 ** $Header: /cvsroot/jred/jred/src/com/synchrona/jred/IrOBEX.java,v 1.1.1.1 2000/07/05 04:41:52 mpatters Exp $
4 **
5 ** Copyright (C) 2000 Synchrona, Inc. All rights reserved.
6 **
7 ** This file is part of JRed, a 100% Java implementation of the IrDA
8 ** infrared communications protocols.
9 **
10 ** This file may be distributed under the terms of the Synchrona Public
11 ** License as defined by Synchrona, Inc. and appearing in the file
12 ** LICENSE included in the packaging of this file. The Synchrona Public
13 ** License is based on the Q Public License as defined by Troll Tech AS
14 ** of Norway; it differs only in its use of the courts of Florida, USA
15 ** rather than those of Oslo, Norway.
16 **************************************************************************
17 */
18 package com.synchrona.jred;
19
20 import com.synchrona.util.Log;
21 import java.io.ByteArrayInputStream;
22 import java.io.ByteArrayOutputStream;
23 import java.io.DataInputStream;
24 import java.io.File;
25 import java.io.FileOutputStream;
26 import java.io.IOException;
27 import java.util.Enumeration;
28 import java.util.Hashtable;
29
30 /**
31 * Implements the Infrared Data Association (IrDA) Object Exchange (OBEX)
32 * protocol.
33 */
34 public class IrOBEX {
35 public static final byte IROBEX_FLAGS = (byte) 0x00;
36 public static final byte IROBEX_VERSION = (byte) 0x10;
37 public static final byte OPCODE_CONNECT_REQUEST = (byte) 0x00;
38 public static final byte OPCODE_DISCONNECT_REQUEST = (byte) 0x01;
39 public static final byte OPCODE_PUT_REQUEST = (byte) 0x02;
40 public static final int STATE_READY = 37;
41 public static final int STATE_RECEIVING = 51;
42
43 private byte [] m_ayPacket = null;
44 private boolean m_bDone = false;
45 private File m_inbox = null;
46 private Log m_log = null;
47 private int m_nPacketLength = 0;
48 private int m_nState = STATE_READY;
49 private ByteArrayOutputStream m_outStream = null;
50 private Hashtable m_params = new Hashtable();
51
52 public IrOBEX(Log log) {
53 m_log = log;
54
55 // We hope the user defined an inbox directory. If not, we'll
56 // try to use the current directory. If that doesn't work, we
57 // won't be able to save anything.
58 String strInbox = System.getProperty("IrOBEX.InboxDirectory");
59 if ( null == strInbox ) {
60 m_log.warn("IrOBEX", "IrOBEX.InboxDirectory was not defined, using ./inbox instead.");
61 strInbox = "." + File.separator + "inbox";
62 }
63
64 m_inbox = new File(strInbox);
65
66 boolean bUseCurrent = false;
67 if ( m_inbox.exists() ) {
68 if ( !m_inbox.isDirectory() ) {
69 m_log.error("IrOBEX", "IrOBEX.InboxDirectory (" + strInbox + ") exists but is not a directory...using current instead.");
70 bUseCurrent = true;
71 }
72 } else {
73 // If mkdir() fails (most likely because of permissions), we'll
74 // have to use the current directory.
75 m_log.warn("IrOBEX", "Creating a new inbox directory: \"" + strInbox + "\"");
76 bUseCurrent = !m_inbox.mkdir();
77 if ( bUseCurrent ) {
78 m_log.error("IrOBEX", "Could not create IrOBEX.InboxDirectory (" + strInbox + ")...using current instead.");
79 } else if ( !m_inbox.canWrite() ) {
80 m_log.error("IrOBEX", "Cannot write to IrOBEX.InboxDirectory (" + strInbox + ")...using current instead.");
81 }
82 }
83
84 if ( bUseCurrent ) {
85 m_inbox = new File(".");
86 if ( m_inbox.canWrite() ) {
87 // If we weren't able to write to the current directory,
88 // we just can't save anymore.
89 m_log.error("IrOBEX", "Cannot write to current directory. IrOBEX will not be able to save any beamed objects.");
90 }
91 }
92 }
93
94 public void serviceRequest(TinyTP tinyTP, IrLMPConnection conn, byte [] ayData, int nOffset, int nLength) throws Exception {
95 if ( STATE_READY == m_nState ) {
96 m_outStream = new ByteArrayOutputStream();
97 m_nPacketLength = (0x0000FF00 & (ayData[nOffset + 1] << 8)) | (0x000000FF & ayData[nOffset + 2]);
98 m_log.debug("IrOBEX", "(serviceRequest) starting new packet: opcode=" + ayData[nOffset] + " length=" + m_nPacketLength);
99 } else {
100 m_log.debug("IrOBEX", "(serviceRequest) appending to current packet");
101 }
102
103 m_outStream.write(ayData, nOffset, nLength);
104 m_bDone = (m_nPacketLength == m_outStream.size());
105
106 if ( m_bDone ) {
107 m_log.debug("IrOBEX", "(serviceRequest) packet complete " + m_outStream.size());
108 m_nState = STATE_READY;
109 m_ayPacket = m_outStream.toByteArray();
110 processPacket(tinyTP);
111 } else {
112 m_nState = STATE_RECEIVING;
113 }
114 }
115
116 /**
117 * Save a beamed object as a file in the inbox directory.
118 */
119 private void doPut() {
120 m_log.debug("IrOBEX", "(doPut)");
121
122 String strName = (String) m_params.get("Name");
123 m_log.debug("IrOBEX", "\tName: " + strName);
124
125 String strDescription = (String) m_params.get("Description");
126 m_log.debug("IrOBEX", "\tDescription: " + strDescription);
127
128 ByteArrayOutputStream body = (ByteArrayOutputStream) m_params.get("Body");
129 m_log.debug("IrOBEX", "\tBody: " + body.toString());
130
131 //
132 // Object names can contain characters with special meanings to the
133 // receiver. For instance, Palm application names can have '/' characters
134 // in them, which Unix platforms use to denote directories. We need to
135 // replace path separators with something, and here we use '_' characters.
136 //
137 String newName = strName.replace(File.separatorChar, '_');
138 File beamedObject = new File(m_inbox.getName() + File.separator + newName);
139
140 m_log.debug("IrOBEX", "\tsaving to " + beamedObject.getName());
141
142 try {
143 FileOutputStream out = new FileOutputStream(beamedObject);
144 body.writeTo(out);
145 out.close();
146 } catch ( IOException ioException ) {
147 m_log.error("IrOBEX", ioException.getMessage());
148 }
149 }
150
151 private void processPacket(TinyTP tinyTP) {
152 m_log.debug("IrOBEX", "(processPacket)");
153
154 boolean bFinal = (0 != (m_ayPacket[0] & 0x80));
155 byte yOpcode = (byte) (m_ayPacket[0] & 0x7F);
156
157 switch ( yOpcode ) {
158 case OPCODE_CONNECT_REQUEST:
159 handleConnectRequest(tinyTP, bFinal);
160 break;
161
162 case OPCODE_DISCONNECT_REQUEST:
163 handleDisconnectRequest(tinyTP, bFinal);
164 break;
165
166 case OPCODE_PUT_REQUEST:
167 handlePutRequest(tinyTP, bFinal);
168 break;
169
170 default:
171 m_log.warn("IrOBEX", "Unimplemented opcode: " + yOpcode);
172 break;
173 }
174 }
175
176 private void handleConnectRequest(TinyTP tinyTP, boolean bFinal) {
177 m_log.debug("IrOBEX", "Connect-Request");
178
179 int nPacketLength = (m_ayPacket[1] << 8) | m_ayPacket[2];
180 byte yVersion = m_ayPacket[3];
181 byte yFlags = m_ayPacket[4];
182 int nMaxPacketLength = (m_ayPacket[5] << 8) | m_ayPacket[6];
183
184 byte [] ayResult = new byte[255];
185 int nLength = 0;
186
187 ayResult[nLength++] = (byte) 0xA0; // 0: success
188 nLength += 2; // 1 & 2: leave space for packet length
189 ayResult[nLength++] = (byte) IROBEX_VERSION; // 3: IrOBEX version
190 ayResult[nLength++] = (byte) IROBEX_FLAGS; // 4: flags
191
192 // bytes 5 & 6: maximum packet length
193 ayResult[nLength++] = (byte) ((nMaxPacketLength & 0x0000FF00) >> 8);
194 ayResult[nLength++] = (byte) (nMaxPacketLength & 0x000000FF);
195
196 ayResult[1] = (byte) ((nLength & 0x0000FF00) >> 8);
197 ayResult[2] = (byte) (nLength & 0x0000000FF);
198
199 tinyTP.dataRequest(ayResult, 0, nLength);
200 }
201
202 private void handleDisconnectRequest(TinyTP tinyTP, boolean bFinal) {
203 m_log.debug("IrOBEX", "Disconnect-Request");
204
205 byte [] ayResponse = new byte[3];
206 int nResponseLength = 0;
207
208 ayResponse[nResponseLength++] = (byte) 0xA0;
209 ayResponse[nResponseLength++] = (byte) 0x00;
210 ayResponse[nResponseLength++] = (byte) 0x03;
211
212 tinyTP.dataRequest(ayResponse, 0, nResponseLength);
213 }
214
215 private void handlePutRequest(TinyTP tinyTP, boolean bFinal) {
216 m_log.debug("IrOBEX", "Put-Request packet-length=" + m_nPacketLength);
217
218 try {
219 parseHeaders();
220 if ( bFinal ) {
221 doPut();
222 m_params.clear();
223 }
224 } catch ( Exception e ) {
225 m_log.error("IrOBEX", e.toString());
226 }
227
228 byte [] ayResponse = new byte[3];
229 int nResponseLength = 0;
230
231 ayResponse[nResponseLength++] = (byte) (bFinal ? 0xA0 : 0x90);
232 ayResponse[nResponseLength++] = (byte) 0x00;
233 ayResponse[nResponseLength++] = (byte) 0x03;
234
235 tinyTP.dataRequest(ayResponse, 0, nResponseLength);
236 }
237
238 private void parseHeaders() throws Exception {
239 int nLength = 0;
240 DataInputStream data = new DataInputStream(new ByteArrayInputStream(m_ayPacket));
241
242 // Packets begin with 1-byte opcode, but we already know it's
243 // a PUT request (although we should assert that here)
244 data.readByte();
245
246 // Next is a 16-bit packet length
247 short nPacketLength = data.readShort();
248 m_log.debug("IrOBEX", "(parseHeaders) nPacketLength: " + nPacketLength);
249
250 while ( data.available() > 0 ) {
251 // Figure out datatype
252 String strName = null;
253 Object value = null;
254 byte [] ayBody = null;
255 boolean bBody = false; // reinitialize each time through
256
257 // Each header begins with a 1-byte type.
258 int yHeader = data.readByte();
259 m_log.debug("IrOBEX", "yHeader=" + yHeader);
260
261 switch ( (byte) (0x000000FF & yHeader) ) {
262 case (byte) 0xC0: strName = "Count"; break;
263 case (byte) 0x01: strName = "Name"; break;
264 case (byte) 0x42: strName = "Type"; break;
265 case (byte) 0xC3: strName = "Length"; break;
266 case (byte) 0x44: strName = "Time"; break;
267 case (byte) 0x05: strName = "Description"; break;
268 case (byte) 0x46: strName = "Target"; break;
269 case (byte) 0x47: strName = "HTTP"; break;
270
271 // See below for special handling of Body & End-Of-Body
272 // chunks.
273 case (byte) 0x48: // fall through
274 case (byte) 0x49:
275 strName = "Body";
276 bBody = true;
277 break;
278
279 case (byte) 0x4A: strName = "Who"; break;
280 case (byte) 0xCB: strName = "Connection-ID"; break;
281 case (byte) 0x4C: strName = "App-Parameters"; break;
282 case (byte) 0x4D: strName = "Challenge"; break;
283 case (byte) 0x4E: strName = "Response"; break;
284 case (byte) 0x4F: strName = "Object-Class"; break;
285
286 // Not part of the IrOBEX specification, and outside
287 // the range of ID's that are supposed to be recognized
288 // by IrOBEX services. It's a 4-byte ASCII string disguised
289 // as an int. Is this considered intellectual property
290 // of Palm Computing? If so, we need to ignore it.
291 case (byte) 0xCF: strName = "Palm-Creator-ID"; break;
292
293 default:
294 strName = "Unknown (" + yHeader + ")";
295 break;
296 }
297
298 m_log.debug("IrOBEX", strName);
299
300 switch ( 0x000000C0 & yHeader ) {
301 case 0x00000000:
302 // Unicode text, null terminated, prefixed by 2-byte length
303 // Length includes header byte and 2-byte length, so we need
304 // to subtract 3 before continuing
305 nLength = data.readShort() - 3;
306 m_log.debug("IrOBEX", "reading string length=" + nLength);
307 // Java uses modified UTF-8, while OBEX specifies
308 // UTF-16. We can convert by reading 16-bit characters
309 // and constructing a string from them.
310 char [] acText = new char[nLength / 2];
311 for ( int i = 0; i < acText.length; i++ ) {
312 acText[i] = data.readChar();
313 }
314 // Convert to string, minus the trailing null
315 String strValue = new String(acText, 0, acText.length - 1);
316 m_log.debug("IrOBEX", "strValue: #" + strValue + "#");
317 value = strValue;
318 break;
319
320 case 0x00000040:
321 // Byte sequence prefaced by 2-byte length
322 // Length includes header byte and 2-byte length, so we need
323 // to subtract 3 before continuing
324 nLength = data.readShort() - 3;
325 m_log.debug("IrOBEX", "reading binary sequence length=" + nLength);
326 ayBody = new byte[nLength];
327 data.read(ayBody);
328 // If this sequence is a Body or End-Of-Body chunk,
329 // it will be contenated into the preceding body
330 // segments below.
331 String strBody = new String(ayBody);
332 m_log.debug("IrOBEX", "body: #" + strBody + "#");
333 break;
334
335 case 0x00000080: // 1 byte
336 m_log.debug("IrOBEX", "reading byte");
337 value = new Byte(data.readByte());
338 break;
339
340 case 0x000000C0: // 4-byte integer
341 m_log.debug("IrOBEX", "reading int");
342 value = new Integer(data.readInt());
343 break;
344
345 default:
346 throw new Exception("Unhandled datatype: " + yHeader);
347 }
348
349 m_log.debug("IrOBEX", "(parseHeaders) " + strName);
350
351 // We need to handle Body and End-Of-Body chunks differently,
352 // because they need to be concatenated into a single stream
353 // of bytes.
354 if ( bBody ) {
355 m_log.debug("IrOBEX", "bBody was true");
356 if ( (null != ayBody) && (ayBody.length > 0 ) ) {
357 ByteArrayOutputStream body =
358 (ByteArrayOutputStream) m_params.get("Body");
359
360 if ( null == body ) {
361 m_log.debug("IrOBEX", "allocating new ByteArrayOutputStream");
362 body = new ByteArrayOutputStream();
363 }
364
365 body.write(ayBody, 0, ayBody.length);
366 m_log.debug("IrOBEX", "putting body into hash table");
367 m_params.put("Body", body);
368 }
369 } else {
370 m_log.debug("IrOBEX", "putting key [" + strName + "] value [" + value + "]");
371 m_params.put(strName, value);
372 }
373 }
374 }
375
376 private void connectRequest(TinyTP tinyTP) {
377 byte [] ayRequest = new byte[255];
378 int nRequestLength = 0;
379
380 ayRequest[nRequestLength++] = (byte) 0x80; // 0: opcode
381 nRequestLength += 2; // 1 & 2: leave space for packet length
382 ayRequest[nRequestLength++] = (byte) IROBEX_VERSION; // 3: IrOBEX version
383 ayRequest[nRequestLength++] = (byte) IROBEX_FLAGS; // 4: flags
384
385 // bytes 5 & 6: maximum packet length
386 ayRequest[nRequestLength++] = (byte) 0xFF;
387 ayRequest[nRequestLength++] = (byte) 0xFF;
388
389 ayRequest[1] = (byte) ((nRequestLength & 0x0000FF00) >> 8);
390 ayRequest[2] = (byte) (nRequestLength & 0x0000000FF);
391
392 tinyTP.dataRequest(ayRequest, 0, nRequestLength);
393 }
394 }