View Javadoc

1   /**
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements. See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership. The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License. You may obtain a copy of the License at
9    *
10   * http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing,
13   * software distributed under the License is distributed on an
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   * KIND, either express or implied. See the License for the
16   * specific language governing permissions and limitations
17   * under the License.
18   */
19  
20  package org.apache.ws.security.util;
21  
22  import java.text.DateFormat;
23  import java.text.FieldPosition;
24  import java.text.ParsePosition;
25  import java.text.SimpleDateFormat;
26  import java.text.ParseException;
27  import java.util.Date;
28  import java.util.TimeZone;
29  
30  /**
31   * A {@link DateFormat} for the format of the dateTime simpleType as specified in the
32   * XML Schema specification. See <a href="http://www.w3.org/TR/xmlschema-2/#dateTime">
33   * XML Schema Part 2: Datatypes, W3C Recommendation 02 May 2001, Section 3.2.7.1</a>.
34   *
35   * @author Ian P. Springer
36   * @author Werner Dittmann
37   */
38  public class XmlSchemaDateFormat extends DateFormat {
39      /**
40       * 
41       */
42      private static final long serialVersionUID = 5152684993503882396L;
43  
44      /**
45       * Logger.
46       */
47      private static final org.apache.commons.logging.Log LOG = 
48          org.apache.commons.logging.LogFactory.getLog(XmlSchemaDateFormat.class);
49  
50      /**
51       * DateFormat for Zulu (UTC) form of an XML Schema dateTime string.
52       */
53      private static final DateFormat DATEFORMAT_XSD_ZULU = new SimpleDateFormat(
54              "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'");
55  
56      static {
57          DATEFORMAT_XSD_ZULU.setTimeZone(TimeZone.getTimeZone("UTC"));
58      }
59      
60      @Override
61      public void setLenient(boolean lenient) {
62          DATEFORMAT_XSD_ZULU.setLenient(lenient);
63      }
64  
65      /**
66       * This method was snarfed from <tt>org.apache.axis.encoding.ser.CalendarDeserializer</tt>,
67       * which was written by Sam Ruby (rubys@us.ibm.com) and Rich Scheuerle (scheu@us.ibm.com).
68       * Better error reporting was added.
69       *
70       * @see DateFormat#parse(java.lang.String)
71       */
72      public Date parse(String src, ParsePosition parsePos) {
73          Date date;
74  
75          // validate fixed portion of format
76          int index = 0;
77          try {
78              if (src != null) {
79                  if ((src.charAt(0) == '+') || (src.charAt(0) == '-')) {
80                      src = src.substring(1);
81                  }
82  
83                  if (src.length() < 19) {
84                      parsePos.setIndex(src.length() - 1);
85                      handleParseError(parsePos, "TOO_FEW_CHARS");
86                  }
87                  validateChar(src, parsePos, index = 4, '-', "EXPECTED_DASH");
88                  validateChar(src, parsePos, index = 7, '-', "EXPECTED_DASH");
89                  validateChar(src, parsePos, index = 10, 'T', "EXPECTED_CAPITAL_T");
90                  validateChar(src, parsePos, index = 13, ':', "EXPECTED_COLON_IN_TIME");
91                  validateChar(src, parsePos, index = 16, ':', "EXPECTED_COLON_IN_TIME");
92              }
93  
94              // convert what we have validated so far
95              synchronized (DATEFORMAT_XSD_ZULU) {
96                  date = DATEFORMAT_XSD_ZULU.parse((src == null) ? null
97                      : (src.substring(0, 19) + ".000Z"));
98              }
99  
100             index = 19;
101 
102             // parse optional milliseconds
103             if (src != null) {
104                 if ((index < src.length()) && (src.charAt(index) == '.')) {
105                     int milliseconds = 0;
106                     int start = ++index;
107 
108                     while ((index < src.length())
109                             && Character.isDigit(src.charAt(index))) {
110                         index++;
111                     }
112 
113                     String decimal = src.substring(start, index);
114 
115                     if (decimal.length() == 3) {
116                         milliseconds = Integer.parseInt(decimal);
117                     } else if (decimal.length() < 3) {
118                         milliseconds = Integer.parseInt((decimal + "000")
119                                 .substring(0, 3));
120                     } else {
121                         milliseconds = Integer
122                                 .parseInt(decimal.substring(0, 3));
123 
124                         if (decimal.charAt(3) >= '5') {
125                             ++milliseconds;
126                         }
127                     }
128 
129                     // add milliseconds to the current date
130                     date.setTime(date.getTime() + milliseconds);
131                 }
132 
133                 // parse optional timezone
134                 if (((index + 5) < src.length())
135                         && ((src.charAt(index) == '+') || (src.charAt(index) == '-'))) {
136                     validateCharIsDigit(src, parsePos, index + 1, "EXPECTED_NUMERAL");
137                     validateCharIsDigit(src, parsePos, index + 2, "EXPECTED_NUMERAL");
138                     validateChar(src, parsePos, index + 3, ':', "EXPECTED_COLON_IN_TIMEZONE");
139                     validateCharIsDigit(src, parsePos, index + 4, "EXPECTED_NUMERAL");
140                     validateCharIsDigit(src, parsePos, index + 5, "EXPECTED_NUMERAL");
141 
142                     final int hours = (((src.charAt(index + 1) - '0') * 10) + src
143                             .charAt(index + 2)) - '0';
144                     final int mins = (((src.charAt(index + 4) - '0') * 10) + src
145                             .charAt(index + 5)) - '0';
146                     int millisecs = ((hours * 60) + mins) * 60 * 1000;
147 
148                     // subtract millisecs from current date to obtain GMT
149                     if (src.charAt(index) == '+') {
150                         millisecs = -millisecs;
151                     }
152 
153                     date.setTime(date.getTime() + millisecs);
154                     index += 6;
155                 }
156 
157                 if ((index < src.length()) && (src.charAt(index) == 'Z')) {
158                     index++;
159                 }
160 
161                 if (index < src.length()) {
162                     handleParseError(parsePos, "TOO_MANY_CHARS");
163                 }
164             }
165         } catch (ParseException pe) {
166             LOG.error(pe.toString(), pe);
167             index = 0; // IMPORTANT: this tells DateFormat.parse() to throw a ParseException
168             parsePos.setErrorIndex(index);
169             date = null;
170         }
171         parsePos.setIndex(index);
172         return date;
173     }
174 
175     /**
176      * @see DateFormat#format(java.util.Date)
177      */
178     public StringBuffer format(Date date, StringBuffer appendBuf,
179             FieldPosition fieldPos) {
180         String str;
181 
182         synchronized (DATEFORMAT_XSD_ZULU) {
183             str = DATEFORMAT_XSD_ZULU.format(date);
184         }
185 
186         if (appendBuf == null) {
187             appendBuf = new StringBuffer();
188         }
189 
190         appendBuf.append(str);
191 
192         return appendBuf;
193     }
194 
195     private void validateChar(String str, ParsePosition parsePos, int index,
196             char expected, String errorReason) throws ParseException {
197         if (str.charAt(index) != expected) {
198             handleParseError(parsePos, errorReason);
199         }
200     }
201 
202     private void validateCharIsDigit(String str, ParsePosition parsePos,
203             int index, String errorReason) throws ParseException {
204         if (!Character.isDigit(str.charAt(index))) {
205             handleParseError(parsePos, errorReason);
206         }
207     }
208 
209     private void handleParseError(ParsePosition parsePos, String errorReason)
210             throws ParseException {
211         throw new ParseException(
212             "INVALID_XSD_DATETIME: " + errorReason, 
213             parsePos.getErrorIndex()
214         );
215     }
216 
217 }