View Javadoc

1   /**
2    *  BlueCove - Java library for Bluetooth
3    *  Copyright (C) 2007-2008 Vlad Skarzhevskyy
4    *
5    *  Licensed to the Apache Software Foundation (ASF) under one
6    *  or more contributor license agreements.  See the NOTICE file
7    *  distributed with this work for additional information
8    *  regarding copyright ownership.  The ASF licenses this file
9    *  to you under the Apache License, Version 2.0 (the
10   *  "License"); you may not use this file except in compliance
11   *  with the License.  You may obtain a copy of the License at
12   *
13   *    http://www.apache.org/licenses/LICENSE-2.0
14   *
15   *  Unless required by applicable law or agreed to in writing,
16   *  software distributed under the License is distributed on an
17   *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
18   *  KIND, either express or implied.  See the License for the
19   *  specific language governing permissions and limitations
20   *  under the License.
21   *
22   *  @version $Id: OBEXHeaderSetImpl.java 2641 2008-12-22 23:28:52Z skarzhevskyy $
23   */
24  package com.intel.bluetooth.obex;
25  
26  import java.io.ByteArrayOutputStream;
27  import java.io.IOException;
28  import java.io.OutputStream;
29  import java.util.Calendar;
30  import java.util.Date;
31  import java.util.Enumeration;
32  import java.util.Hashtable;
33  import java.util.TimeZone;
34  import java.util.Vector;
35  
36  import javax.obex.HeaderSet;
37  
38  import com.intel.bluetooth.DebugLog;
39  
40  class OBEXHeaderSetImpl implements HeaderSet {
41  
42  	/** Number of objects (used by connect) (0xC0) */
43  	static final int OBEX_HDR_COUNT = HeaderSet.COUNT;
44  
45  	/** Name of the object (0x01) */
46  	static final int OBEX_HDR_NAME = HeaderSet.NAME;
47  
48  	/** Type of the object (0x42) */
49  	static final int OBEX_HDR_TYPE = HeaderSet.TYPE;
50  
51  	/** Total lenght of object (0xC3) */
52  	static final int OBEX_HDR_LENGTH = HeaderSet.LENGTH;
53  
54  	/** Last modification time of (ISO8601) (0x44) */
55  	static final int OBEX_HDR_TIME = HeaderSet.TIME_ISO_8601;
56  
57  	/** Deprecated use HDR_TIME instead (0xC4) */
58  	static final int OBEX_HDR_TIME2 = HeaderSet.TIME_4_BYTE;
59  
60  	/** Description of object (0x05) */
61  	static final int OBEX_HDR_DESCRIPTION = HeaderSet.DESCRIPTION;
62  
63  	/** name of service that operation is targeted to (0x46) */
64  	static final int OBEX_HDR_TARGET = HeaderSet.TARGET;
65  
66  	/** An HTTP 1.x header (0x47) */
67  	static final int OBEX_HDR_HTTP = HeaderSet.HTTP;
68  
69  	/** Data part of the object (0x48) */
70  	static final int OBEX_HDR_BODY = 0x48;
71  
72  	/** Last data part of the object (0x49) */
73  	static final int OBEX_HDR_BODY_END = 0x49;
74  
75  	/** Identifies the sender of the object (0x4A) */
76  	static final int OBEX_HDR_WHO = HeaderSet.WHO;
77  
78  	/** Connection identifier used for OBEX connection multiplexing (0xCB) */
79  	static final int OBEX_HDR_CONNECTION = 0xCB;
80  
81  	/** Application parameters (0x4C) */
82  	static final int OBEX_HDR_APP_PARAM = HeaderSet.APPLICATION_PARAMETER;
83  
84  	/** Authentication digest-challenge (0x4D) */
85  	static final int OBEX_HDR_AUTH_CHALLENGE = 0x4D;
86  
87  	/** Authentication digest-response (0x4E) */
88  	static final int OBEX_HDR_AUTH_RESPONSE = 0x4E;
89  
90  	/** OBEX Object class of object (0x51) */
91  	static final int OBEX_HDR_OBJECTCLASS = HeaderSet.OBJECT_CLASS;
92  
93  	/** indicates the creator of an object (0xCF) */
94  	static final int OBEX_HDR_CREATOR = 0xCF;
95  
96  	/** uniquely identifies the network client (OBEX server) (0x50) */
97  	static final int OBEX_HDR_WANUUID = 0x50;
98  
99  	// /** OBEX Object class of object (0x51)*/
100 	// static final int OBEX_HDR_OBJECTCLASS = 0x51;
101 
102 	/** Parameters used in sessioncommands/responses (0x52) */
103 	static final int OBEX_HDR_SESSIONPARAM = 0x52;
104 
105 	/** Sequence number used in each OBEX packet for reliability (0x93) */
106 	static final int OBEX_HDR_SESSIONSEQ = 0x93;
107 
108 	// 0x30 to 0x3F user defined - this range includes all combinations of the
109 	// upper 2 bits
110 	static final int OBEX_HDR_USER = 0x30;
111 
112 	static final int OBEX_HDR_HI_MASK = 0xC0;
113 
114 	static final int OBEX_HDR_ID_MASK = 0x3F;
115 
116 	/**
117 	 * null terminated Unicode text, length prefixed with 2 byte unsigned integer
118 	 */
119 	static final int OBEX_STRING = 0x00;
120 
121 	/** byte sequence, length prefixed with 2 byte unsigned integer */
122 	static final int OBEX_BYTE_STREAM = 0x40;
123 
124 	/** 1 byte quantity */
125 	static final int OBEX_BYTE = 0x80;
126 
127 	/** 4 byte quantity – transmitted in network byte order (high byte first) */
128 	static final int OBEX_INT = 0xC0;
129 
130 	private static final int OBEX_MAX_FIELD_LEN = 0xFF;
131 
132 	private int responseCode;
133 
134 	private Hashtable headerValues;
135 
136 	private Vector authResponses;
137 
138 	private Vector authChallenges;
139 
140 	private static final int NO_RESPONSE_CODE = Integer.MIN_VALUE;
141 
142 	OBEXHeaderSetImpl() {
143 		this(NO_RESPONSE_CODE);
144 	}
145 
146 	private OBEXHeaderSetImpl(int responseCode) {
147 		this.headerValues = new Hashtable();
148 		this.responseCode = responseCode;
149 		this.authResponses = null;
150 		this.authChallenges = null;
151 	}
152 
153 	static void validateCreatedHeaderSet(HeaderSet headers) {
154 		if (headers == null) {
155 			return;
156 		}
157 		if (!(headers instanceof OBEXHeaderSetImpl)) {
158 			throw new IllegalArgumentException("Illegal HeaderSet type");
159 		}
160 		if (((OBEXHeaderSetImpl) headers).responseCode != NO_RESPONSE_CODE) {
161 			throw new IllegalArgumentException("Illegal HeaderSet");
162 		}
163 	}
164 
165 	private void validateHeaderID(int headerID) throws IllegalArgumentException {
166 		if (headerID < 0 || headerID > 0xff) {
167 			throw new IllegalArgumentException("Expected header ID in range 0 to 255");
168 		}
169 		int identifier = headerID & OBEX_HDR_ID_MASK;
170 		if (identifier >= 0x10 && identifier < 0x2F) {
171 			throw new IllegalArgumentException("Reserved header ID");
172 		}
173 	}
174 
175 	public void setHeader(int headerID, Object headerValue) {
176 		validateHeaderID(headerID);
177 		if (headerValue == null) {
178 			headerValues.remove(new Integer(headerID));
179 		} else {
180 			// Validate Java value Type
181 			if ((headerID == OBEX_HDR_TIME) || (headerID == OBEX_HDR_TIME2)) {
182 				if (!(headerValue instanceof Calendar)) {
183 					throw new IllegalArgumentException("Expected java.util.Calendar");
184 				}
185 			} else if (headerID == OBEX_HDR_TYPE) {
186 				if (!(headerValue instanceof String)) {
187 					throw new IllegalArgumentException("Expected java.lang.String");
188 				}
189 			} else {
190 				switch (headerID & OBEX_HDR_HI_MASK) {
191 				case OBEX_STRING:
192 					if (!(headerValue instanceof String)) {
193 						throw new IllegalArgumentException("Expected java.lang.String");
194 					}
195 					break;
196 				case OBEX_BYTE_STREAM:
197 					if (!(headerValue instanceof byte[])) {
198 						throw new IllegalArgumentException("Expected byte[]");
199 					}
200 					break;
201 				case OBEX_BYTE:
202 					if (!(headerValue instanceof Byte)) {
203 						throw new IllegalArgumentException("Expected java.lang.Byte");
204 					}
205 					break;
206 				case OBEX_INT:
207 					if (!(headerValue instanceof Long)) {
208 						throw new IllegalArgumentException("Expected java.lang.Long");
209 					}
210 					long v = ((Long) headerValue).longValue();
211 					if (v < 0 || v > 0xffffffffl) {
212 						throw new IllegalArgumentException("Expected long in range 0 to 2^32-1");
213 					}
214 					break;
215 				default:
216 					throw new IllegalArgumentException("Unsupported encoding " + (headerID & OBEX_HDR_HI_MASK));
217 				}
218 			}
219 			headerValues.put(new Integer(headerID), headerValue);
220 		}
221 	}
222 
223 	public Object getHeader(int headerID) throws IOException {
224 		validateHeaderID(headerID);
225 		return headerValues.get(new Integer(headerID));
226 	}
227 
228 	/*
229 	 * (non-Javadoc)
230 	 * 
231 	 * @see javax.obex.HeaderSet#getHeaderList()
232 	 */
233 	public int[] getHeaderList() throws IOException {
234 		if (headerValues.size() == 0) {
235 			// Spec: null if no headers are available
236 			return null;
237 		}
238 		int[] headerIDArray = new int[headerValues.size()];
239 		int i = 0;
240 		for (Enumeration e = headerValues.keys(); e.hasMoreElements();) {
241 			headerIDArray[i++] = ((Integer) e.nextElement()).intValue();
242 		}
243 		return headerIDArray;
244 	}
245 
246 	public int getResponseCode() throws IOException {
247 		if (this.responseCode == NO_RESPONSE_CODE) {
248 			throw new IOException();
249 		}
250 		return this.responseCode;
251 	}
252 
253 	boolean hasIncommingData() {
254 		return headerValues.contains(new Integer(OBEX_HDR_BODY))
255 				|| headerValues.contains(new Integer(OBEX_HDR_BODY_END));
256 	}
257 
258 	static OBEXHeaderSetImpl cloneHeaders(HeaderSet headers) throws IOException {
259 		if (headers == null) {
260 			return null;
261 		}
262 		if (!(headers instanceof OBEXHeaderSetImpl)) {
263 			throw new IllegalArgumentException("Illegal HeaderSet type");
264 		}
265 		OBEXHeaderSetImpl hs = new OBEXHeaderSetImpl(((OBEXHeaderSetImpl) headers).responseCode);
266 
267 		int[] headerIDArray = headers.getHeaderList();
268 		for (int i = 0; (headerIDArray != null) && (i < headerIDArray.length); i++) {
269 			int headerID = headerIDArray[i];
270 			// Body is not accessible by the client
271 			if ((headerID == OBEX_HDR_BODY) || (headerID == OBEX_HDR_BODY_END)) {
272 				continue;
273 			}
274 			hs.setHeader(headerID, headers.getHeader(headerID));
275 		}
276 		return hs;
277 	}
278 
279 	static HeaderSet appendHeaders(HeaderSet dst, HeaderSet src) throws IOException {
280 		int[] headerIDArray = src.getHeaderList();
281 		for (int i = 0; (headerIDArray != null) && (i < headerIDArray.length); i++) {
282 			int headerID = headerIDArray[i];
283 			if ((headerID == OBEX_HDR_BODY) || (headerID == OBEX_HDR_BODY_END)) {
284 				continue;
285 			}
286 			dst.setHeader(headerID, src.getHeader(headerID));
287 		}
288 		return dst;
289 	}
290 
291 	public synchronized void createAuthenticationChallenge(String realm, boolean isUserIdRequired, boolean isFullAccess) {
292 		if (authChallenges == null) {
293 			authChallenges = new Vector();
294 		}
295 		authChallenges.addElement(OBEXAuthentication.createChallenge(realm, isUserIdRequired, isFullAccess));
296 	}
297 
298 	synchronized void addAuthenticationResponse(byte[] authResponse) {
299 		if (authResponses == null) {
300 			authResponses = new Vector();
301 		}
302 		authResponses.addElement(authResponse);
303 	}
304 
305 	boolean hasAuthenticationChallenge() {
306 		if (authChallenges == null) {
307 			return false;
308 		}
309 		return !authChallenges.isEmpty();
310 	}
311 
312 	Enumeration getAuthenticationChallenges() {
313 		return authChallenges.elements();
314 	}
315 
316 	boolean hasAuthenticationResponses() {
317 		if (authResponses == null) {
318 			return false;
319 		}
320 		return !authResponses.isEmpty();
321 	}
322 
323 	Enumeration getAuthenticationResponses() {
324 		return authResponses.elements();
325 	}
326 
327 	static long readObexInt(byte[] data, int off) throws IOException {
328 		long l = 0;
329 		for (int i = 0; i < 4; i++) {
330 			l = l << 8;
331 			l += (int) (data[off + i] & 0xFF);
332 		}
333 		return l;
334 	}
335 
336 	static void writeObexInt(OutputStream out, int headerID, long data) throws IOException {
337 		byte[] b = new byte[5];
338 		b[0] = (byte) headerID;
339 		b[1] = (byte) ((data >>> 24) & 0xFF);
340 		b[2] = (byte) ((data >>> 16) & 0xFF);
341 		b[3] = (byte) ((data >>> 8) & 0xFF);
342 		b[4] = (byte) ((data >>> 0) & 0xFF);
343 		out.write(b);
344 	}
345 
346 	static void writeObexLen(OutputStream out, int headerID, int len) throws IOException {
347 		byte[] b = new byte[3];
348 		b[0] = (byte) headerID;
349 		if ((len < 0) || len > 0xFFFF) {
350 			throw new IOException("very large data" + len);
351 		}
352 		b[1] = OBEXUtils.hiByte(len);
353 		b[2] = OBEXUtils.loByte(len);
354 		out.write(b);
355 	}
356 
357 	static void writeObexASCII(OutputStream out, int headerID, String value) throws IOException {
358 		writeObexLen(out, headerID, 3 + value.length() + 1);
359 		out.write(value.getBytes("iso-8859-1"));
360 		out.write(0);
361 	}
362 
363 	static void writeObexUnicode(OutputStream out, int headerID, String value) throws IOException {
364 		// null terminated Unicode text, length prefixed with 2 byte unsigned
365 		// integer
366 		// the length field includes the 2 bytes of the null
367 		// terminator (0x00, 0x00). Therefore the length of the string ”Jumar”
368 		// would be 12 bytes; 5 visible
369 		// characters plus the null terminator, each two bytes in length.
370 		if (value.length() == 0) {
371 			writeObexLen(out, headerID, 3);
372 			return;
373 		}
374 		byte[] b = OBEXUtils.getUTF16Bytes(value);
375 		writeObexLen(out, headerID, 3 + b.length + 2);
376 		out.write(b);
377 		out.write(new byte[] { 0, 0 });
378 	}
379 
380 	static byte[] toByteArray(HeaderSet headers) throws IOException {
381 		if (headers == null) {
382 			return new byte[0];
383 		}
384 		ByteArrayOutputStream buf = new ByteArrayOutputStream();
385 		int[] headerIDArray = headers.getHeaderList();
386 		for (int i = 0; (headerIDArray != null) && (i < headerIDArray.length); i++) {
387 			int hi = headerIDArray[i];
388 			if (hi == OBEX_HDR_TIME) {
389 				Calendar c = (Calendar) headers.getHeader(hi);
390 				writeObexLen(buf, hi, 19);
391 				writeTimeISO8601(buf, c);
392 			} else if (hi == OBEX_HDR_TIME2) {
393 				Calendar c = (Calendar) headers.getHeader(hi);
394 				writeObexInt(buf, hi, c.getTime().getTime() / 1000);
395 			} else if (hi == OBEX_HDR_TYPE) {
396 				// ASCII string
397 				writeObexASCII(buf, hi, (String) headers.getHeader(hi));
398 			} else {
399 				switch (hi & OBEX_HDR_HI_MASK) {
400 				case OBEX_STRING:
401 					writeObexUnicode(buf, hi, (String) headers.getHeader(hi));
402 					break;
403 				case OBEX_BYTE_STREAM:
404 					byte data[] = (byte[]) headers.getHeader(hi);
405 					writeObexLen(buf, hi, 3 + data.length);
406 					buf.write(data);
407 					break;
408 				case OBEX_BYTE:
409 					buf.write(hi);
410 					buf.write(((Byte) headers.getHeader(hi)).byteValue());
411 					break;
412 				case OBEX_INT:
413 					writeObexInt(buf, hi, ((Long) headers.getHeader(hi)).longValue());
414 					break;
415 				default:
416 					throw new IOException("Unsupported encoding " + (hi & OBEX_HDR_HI_MASK));
417 				}
418 			}
419 		}
420 		if ((headerIDArray != null) && (headerIDArray.length != 0)) {
421 			DebugLog.debug("written headers", headerIDArray.length);
422 		}
423 		if (((OBEXHeaderSetImpl) headers).hasAuthenticationChallenge()) {
424 			for (Enumeration iter = ((OBEXHeaderSetImpl) headers).authChallenges.elements(); iter.hasMoreElements();) {
425 				byte[] authChallenge = (byte[]) iter.nextElement();
426 				writeObexLen(buf, OBEX_HDR_AUTH_CHALLENGE, 3 + authChallenge.length);
427 				buf.write(authChallenge);
428 				DebugLog.debug("written AUTH_CHALLENGE");
429 			}
430 		}
431 		if (((OBEXHeaderSetImpl) headers).hasAuthenticationResponses()) {
432 			for (Enumeration iter = ((OBEXHeaderSetImpl) headers).authResponses.elements(); iter.hasMoreElements();) {
433 				byte[] authResponse = (byte[]) iter.nextElement();
434 				writeObexLen(buf, OBEX_HDR_AUTH_RESPONSE, 3 + authResponse.length);
435 				buf.write(authResponse);
436 				DebugLog.debug("written AUTH_RESPONSE");
437 			}
438 		}
439 		return buf.toByteArray();
440 	}
441 
442 	/*
443 	 * Read by server
444 	 */
445 	static OBEXHeaderSetImpl readHeaders(byte[] buf, int off) throws IOException {
446 		return readHeaders(new OBEXHeaderSetImpl(NO_RESPONSE_CODE), buf, off);
447 	}
448 
449 	static OBEXHeaderSetImpl readHeaders(byte responseCode, byte[] buf, int off) throws IOException {
450 		return readHeaders(new OBEXHeaderSetImpl(0xFF & responseCode), buf, off);
451 	}
452 
453 	private static OBEXHeaderSetImpl readHeaders(OBEXHeaderSetImpl hs, byte[] buf, int off) throws IOException {
454 		int count = 0;
455 		while (off < buf.length) {
456 			int hi = 0xFF & buf[off];
457 			int len = 0;
458 			switch (hi & OBEX_HDR_HI_MASK) {
459 			case OBEX_STRING:
460 				len = OBEXUtils.bytesToShort(buf[off + 1], buf[off + 2]);
461 				if (len == 3) {
462 					hs.setHeader(hi, "");
463 				} else {
464 					byte data[] = new byte[len - 5];
465 					System.arraycopy(buf, off + 3, data, 0, data.length);
466 					hs.setHeader(hi, OBEXUtils.newStringUTF16(data));
467 				}
468 				break;
469 			case OBEX_BYTE_STREAM:
470 				len = OBEXUtils.bytesToShort(buf[off + 1], buf[off + 2]);
471 				byte data[] = new byte[len - 3];
472 				System.arraycopy(buf, off + 3, data, 0, data.length);
473 				if (hi == OBEX_HDR_TYPE) {
474 					if (data[data.length - 1] != 0) {
475 						hs.setHeader(hi, new String(data, "iso-8859-1"));
476 					} else {
477 						hs.setHeader(hi, new String(data, 0, data.length - 1, "iso-8859-1"));
478 					}
479 				} else if (hi == OBEX_HDR_TIME) {
480 					hs.setHeader(hi, readTimeISO8601(data));
481 				} else if (hi == OBEX_HDR_AUTH_CHALLENGE) {
482 					synchronized (hs) {
483 						if (hs.authChallenges == null) {
484 							hs.authChallenges = new Vector();
485 						}
486 					}
487 					hs.authChallenges.addElement(data);
488 					DebugLog.debug("received AUTH_CHALLENGE");
489 				} else if (hi == OBEX_HDR_AUTH_RESPONSE) {
490 					synchronized (hs) {
491 						if (hs.authResponses == null) {
492 							hs.authResponses = new Vector();
493 						}
494 					}
495 					hs.authResponses.addElement(data);
496 					DebugLog.debug("received AUTH_RESPONSE");
497 				} else {
498 					hs.setHeader(hi, data);
499 				}
500 				break;
501 			case OBEX_BYTE:
502 				len = 2;
503 				hs.setHeader(hi, new Byte(buf[off + 1]));
504 				break;
505 			case OBEX_INT:
506 				len = 5;
507 				long intValue = readObexInt(buf, off + 1);
508 				if (hi == OBEX_HDR_TIME2) {
509 					Calendar cal = Calendar.getInstance(TimeZone.getTimeZone("UTC"));
510 					cal.setTime(new Date(intValue * 1000));
511 					hs.setHeader(hi, cal);
512 				} else {
513 					hs.setHeader(hi, new Long(intValue));
514 				}
515 				break;
516 			default:
517 				throw new IOException("Unsupported encoding " + (hi & OBEX_HDR_HI_MASK));
518 			}
519 			off += len;
520 			count++;
521 		}
522 		if (count != 0) {
523 			DebugLog.debug("read headers", count);
524 		}
525 		return hs;
526 	}
527 
528 	private static byte[] d4(int i) {
529 		byte[] b = new byte[4];
530 		int d = 1000;
531 		for (int k = 0; k < 4; k++) {
532 			b[k] = (byte) (i / d + '0');
533 			i %= d;
534 			d /= 10;
535 		}
536 		return b;
537 	}
538 
539 	private static byte[] d2(int i) {
540 		byte[] b = new byte[2];
541 		b[0] = (byte) (i / 10 + '0');
542 		b[1] = (byte) (i % 10 + '0');
543 		return b;
544 	}
545 
546 	/**
547 	 * ISO-8601 UTC YYYYMMDDTHHMMSSZ
548 	 */
549 	static void writeTimeISO8601(OutputStream out, Calendar c) throws IOException {
550 		Calendar cal = Calendar.getInstance(TimeZone.getTimeZone("UTC"));
551 		cal.setTime(c.getTime());
552 		out.write(d4(cal.get(Calendar.YEAR)));
553 		out.write(d2(cal.get(Calendar.MONTH) + 1));
554 		out.write(d2(cal.get(Calendar.DAY_OF_MONTH)));
555 		out.write('T');
556 		out.write(d2(cal.get(Calendar.HOUR_OF_DAY)));
557 		out.write(d2(cal.get(Calendar.MINUTE)));
558 		out.write(d2(cal.get(Calendar.SECOND)));
559 		out.write('Z');
560 	}
561 
562 	/**
563 	 * ISO-8601 UTC YYYYMMDDTHHMMSS(Z) Z for UTC time
564 	 */
565 	static Calendar readTimeISO8601(byte data[]) throws IOException {
566 		boolean utc = false;
567 		if ((data.length != 16) && (data.length != 15)) {
568 			throw new IOException("Invalid ISO-8601 date length " + new String(data) + " length " + data.length);
569 		} else if (data[8] != 'T') {
570 			throw new IOException("Invalid ISO-8601 date " + new String(data));
571 		} else if (data.length == 16) {
572 			if (data[15] != 'Z') {
573 				throw new IOException("Invalid ISO-8601 date " + new String(data));
574 			} else {
575 				utc = true;
576 			}
577 		}
578 		Calendar cal = utc ? Calendar.getInstance(TimeZone.getTimeZone("UTC")) : Calendar.getInstance();
579 		cal.set(Calendar.YEAR, Integer.valueOf(new String(data, 0, 4)).intValue());
580 		cal.set(Calendar.MONTH, Integer.valueOf(new String(data, 4, 2)).intValue() - 1);
581 		cal.set(Calendar.DAY_OF_MONTH, Integer.valueOf(new String(data, 6, 2)).intValue());
582 		cal.set(Calendar.HOUR_OF_DAY, Integer.valueOf(new String(data, 9, 2)).intValue());
583 		cal.set(Calendar.MINUTE, Integer.valueOf(new String(data, 11, 2)).intValue());
584 		cal.set(Calendar.SECOND, Integer.valueOf(new String(data, 13, 2)).intValue());
585 		return cal;
586 	}
587 
588 }