001    /*
002     * Zmanim Java API
003     * Copyright (C) 2004-2011 Eliyahu Hershfeld
004     * 
005     * This program is free software; you can redistribute it and/or modify it under the terms of the
006     * GNU General Public License as published by the Free Software Foundation; either version 2 of the
007     * License, or (at your option) any later version.
008     * 
009     * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without
010     * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
011     * General Public License for more details.
012     * 
013     * You should have received a copy of the GNU General Public License along with this program; if
014     * not, write to the Free Software Foundation, Inc. 59 Temple Place - Suite 330, Boston, MA
015     * 02111-1307, USA or connect to: http://www.fsf.org/copyleft/gpl.html
016     */
017    package net.sourceforge.zmanim.util;
018    
019    import java.lang.reflect.Method;
020    import java.text.DateFormat;
021    import java.text.DecimalFormat;
022    import java.util.ArrayList;
023    import java.util.Collections;
024    import java.util.Date;
025    import java.util.Calendar;
026    import java.util.List;
027    import java.text.SimpleDateFormat;
028    import net.sourceforge.zmanim.*;
029    
030    
031    /**
032     * A class used to format non {@link java.util.Date} times generated by the
033     * Zmanim package. For example the
034     * {@link net.sourceforge.zmanim.AstronomicalCalendar#getTemporalHour()} returns
035     * the length of the hour in milliseconds. This class can format this time.
036     * 
037     * @author © Eliyahu Hershfeld 2004 - 2011
038     * @version 1.2
039     */
040    public class ZmanimFormatter {
041            private boolean prependZeroHours;
042    
043            private boolean useSeconds;
044    
045            private boolean useMillis;
046    
047            boolean useDecimal;
048    
049            private static DecimalFormat minuteSecondNF = new DecimalFormat("00");
050    
051            private DecimalFormat hourNF;
052    
053            private static DecimalFormat milliNF = new DecimalFormat("000");
054    
055            private SimpleDateFormat dateFormat;
056    
057            // private DecimalFormat decimalNF;
058    
059            /**
060             * Format using hours, minutes, seconds and milliseconds using the xsd:time
061             * format. This format will return 00.00.00.0 when formatting 0.
062             */
063            public static final int SEXAGESIMAL_XSD_FORMAT = 0;
064    
065            private int timeFormat = SEXAGESIMAL_XSD_FORMAT;
066    
067            /**
068             * Format using standard decimal format with 5 positions after the decimal.
069             */
070            public static final int DECIMAL_FORMAT = 1;
071    
072            /** Format using hours and minutes. */
073            public static final int SEXAGESIMAL_FORMAT = 2;
074    
075            /** Format using hours, minutes and seconds. */
076            public static final int SEXAGESIMAL_SECONDS_FORMAT = 3;
077    
078            /** Format using hours, minutes, seconds and milliseconds. */
079            public static final int SEXAGESIMAL_MILLIS_FORMAT = 4;
080            
081            /** constant for milliseconds in a minute (60,000) */
082            static final long MINUTE_MILLIS = 60 * 1000;
083            
084            /** constant for milliseconds in an hour (3,600,000) */
085            public static final long HOUR_MILLIS = MINUTE_MILLIS * 60;
086    
087            /**
088             * Format using the XSD Duration format. This is in the format of
089             * PT1H6M7.869S (P for period (duration), T for time, H, M and S indicate
090             * hours, minutes and seconds.
091             */
092            public static final int XSD_DURATION_FORMAT = 5;
093    
094            public ZmanimFormatter() {
095                    this(0, new SimpleDateFormat("h:mm:ss"));
096            }
097    
098            /**
099             * ZmanimFormatter constructor using a formatter
100             * 
101             * @param format
102             *            int The formatting style to use. Using
103             *            ZmanimFormatter.SEXAGESIMAL_SECONDS_FORMAT will format the
104             *            time time of 90*60*1000 + 1 as 1:30:00
105             */
106            public ZmanimFormatter(int format, SimpleDateFormat dateFormat) {
107                    String hourFormat = "0";
108                    if (this.prependZeroHours) {
109                            hourFormat = "00";
110                    }
111                    this.hourNF = new DecimalFormat(hourFormat);
112                    // decimalNF = new DecimalFormat("0.0####");
113                    setTimeFormat(format);
114                    this.setDateFormat(dateFormat);
115            }
116    
117            /**
118             * Sets the format to use for formatting.
119             * 
120             * @param format
121             *            int the format constant to use.
122             */
123            public void setTimeFormat(int format) {
124                    this.timeFormat = format;
125                    switch (format) {
126                    case SEXAGESIMAL_XSD_FORMAT:
127                            setSettings(true, true, true);
128                            break;
129                    case SEXAGESIMAL_FORMAT:
130                            setSettings(false, false, false);
131                            break;
132                    case SEXAGESIMAL_SECONDS_FORMAT:
133                            setSettings(false, true, false);
134                            break;
135                    case SEXAGESIMAL_MILLIS_FORMAT:
136                            setSettings(false, true, true);
137                            break;
138                    case DECIMAL_FORMAT:
139                    default:
140                            this.useDecimal = true;
141                    }
142            }
143    
144            public void setDateFormat(SimpleDateFormat sdf) {
145                    this.dateFormat = sdf;
146            }
147    
148            public SimpleDateFormat getDateFormat() {
149                    return this.dateFormat;
150            }
151    
152            private void setSettings(boolean prependZeroHours, boolean useSeconds,
153                            boolean useMillis) {
154                    this.prependZeroHours = prependZeroHours;
155                    this.useSeconds = useSeconds;
156                    this.useMillis = useMillis;
157            }
158    
159            /**
160             * A method that formats milliseconds into a time format.
161             * 
162             * @param milliseconds
163             *            The time in milliseconds.
164             * @return String The formatted <code>String</code>
165             */
166            public String format(double milliseconds) {
167                    return format((int) milliseconds);
168            }
169    
170            /**
171             * A method that formats milliseconds into a time format.
172             * 
173             * @param millis
174             *            The time in milliseconds.
175             * @return String The formatted <code>String</code>
176             */
177            public String format(int millis) {
178                    return format(new Time(millis));
179            }
180    
181            /**
182             * A method that formats {@link Time}objects.
183             * 
184             * @param time
185             *            The time <code>Object</code> to be formatted.
186             * @return String The formatted <code>String</code>
187             */
188            public String format(Time time) {
189                    if (this.timeFormat == XSD_DURATION_FORMAT) {
190                            return formatXSDDurationTime(time);
191                    }
192                    StringBuffer sb = new StringBuffer();
193                    sb.append(this.hourNF.format(time.getHours()));
194                    sb.append(":");
195                    sb.append(minuteSecondNF.format(time.getMinutes()));
196                    if (this.useSeconds) {
197                            sb.append(":");
198                            sb.append(minuteSecondNF.format(time.getSeconds()));
199                    }
200                    if (this.useMillis) {
201                            sb.append(".");
202                            sb.append(milliNF.format(time.getMilliseconds()));
203                    }
204                    return sb.toString();
205            }
206    
207            /**
208             * Formats a date using this classe's {@link #getDateFormat() date format}.
209             * 
210             * @param dateTime
211             *            the date to format
212             * @param calendar
213             *            the {@link java.util.Calendar Calendar} used to help format
214             *            based on the Calendar's DST and other settings.
215             * @return the formatted String
216             */
217            public String formatDateTime(Date dateTime, Calendar calendar) {
218                    this.dateFormat.setCalendar(calendar);
219                    if (this.dateFormat.toPattern().equals("yyyy-MM-dd'T'HH:mm:ss")) {
220                            return getXSDateTime(dateTime, calendar);
221                    } else {
222                            return this.dateFormat.format(dateTime);
223                    }
224    
225            }
226    
227            /**
228             * The date:date-time function returns the current date and time as a
229             * date/time string. The date/time string that's returned must be a string
230             * in the format defined as the lexical representation of xs:dateTime in <a
231             * href="http://www.w3.org/TR/xmlschema11-2/#dateTime">[3.3.8 dateTime]</a>
232             * of <a href="http://www.w3.org/TR/xmlschema11-2/">[XML Schema 1.1 Part 2:
233             * Datatypes]</a>. The date/time format is basically CCYY-MM-DDThh:mm:ss,
234             * although implementers should consult <a
235             * href="http://www.w3.org/TR/xmlschema11-2/">[XML Schema 1.1 Part 2:
236             * Datatypes]</a> and <a href="http://www.iso.ch/markete/8601.pdf">[ISO
237             * 8601]</a> for details. The date/time string format must include a time
238             * zone, either a Z to indicate Coordinated Universal Time or a + or -
239             * followed by the difference between the difference from UTC represented as
240             * hh:mm.
241             */
242            public String getXSDateTime(Date dateTime, Calendar cal) {
243                    String xsdDateTimeFormat = "yyyy-MM-dd'T'HH:mm:ss";
244                    /*
245                     * if (xmlDateFormat == null || xmlDateFormat.trim().equals("")) {
246                     * xmlDateFormat = xsdDateTimeFormat; }
247                     */
248                    SimpleDateFormat dateFormat = new SimpleDateFormat(xsdDateTimeFormat);
249    
250                    StringBuffer buff = new StringBuffer(dateFormat.format(dateTime));
251                    // Must also include offset from UTF.
252                    // Get the offset (in milliseconds).
253                    int offset = cal.get(Calendar.ZONE_OFFSET)
254                                    + cal.get(Calendar.DST_OFFSET);
255                    // If there is no offset, we have "Coordinated
256                    // Universal Time."
257                    if (offset == 0)
258                            buff.append("Z");
259                    else {
260                            // Convert milliseconds to hours and minutes
261                            int hrs = offset / (60 * 60 * 1000);
262                            // In a few cases, the time zone may be +/-hh:30.
263                            int min = offset % (60 * 60 * 1000);
264                            char posneg = hrs < 0 ? '-' : '+';
265                            buff.append(posneg + formatDigits(hrs) + ':' + formatDigits(min));
266                    }
267                    return buff.toString();
268            }
269    
270            /**
271             * Represent the hours and minutes with two-digit strings.
272             * 
273             * @param digits hours or minutes.
274             * @return two-digit String representation of hrs or minutes.
275             */
276            private static String formatDigits(int digits) {
277                    String dd = String.valueOf(Math.abs(digits));
278                    return dd.length() == 1 ? '0' + dd : dd;
279            }
280    
281            /**
282             * This returns the xml representation of an xsd:duration object.
283             * 
284             * @param millis the duration in milliseconds
285             * @return the xsd:duration formatted String
286             */
287            public String formatXSDDurationTime(long millis) {
288                    return formatXSDDurationTime(new Time(millis));
289            }
290    
291            /**
292             * This returns the xml representation of an xsd:duration object.
293             * 
294             * @param time the duration as a Time object 
295             * @return the xsd:duration formatted String
296             */
297            public String formatXSDDurationTime(Time time) {
298                    StringBuffer duration = new StringBuffer();
299                    if (time.getHours() != 0 || time.getMinutes() != 0
300                                    || time.getSeconds() != 0 || time.getMilliseconds() != 0) {
301                            duration.append("P");
302                            duration.append("T");
303    
304                            if (time.getHours() != 0)
305                                    duration.append(time.getHours() + "H");
306    
307                            if (time.getMinutes() != 0)
308                                    duration.append(time.getMinutes() + "M");
309    
310                            if (time.getSeconds() != 0 || time.getMilliseconds() != 0) {
311                                    duration.append(time.getSeconds() + "."
312                                                    + milliNF.format(time.getMilliseconds()));
313                                    duration.append("S");
314                            }
315                            if (duration.length() == 1) // zero seconds
316                                    duration.append("T0S");
317                            if (time.isNegative())
318                                    duration.insert(0, "-");
319                    }
320                    return duration.toString();
321            }
322            
323            /**
324             * A method that returns an XML formatted <code>String</code> representing
325             * the serialized <code>Object</code>. The format used is:
326             * 
327             * <pre>
328             *  &lt;AstronomicalTimes date=&quot;1969-02-08&quot; type=&quot;net.sourceforge.zmanim.AstronomicalCalendar algorithm=&quot;US Naval Almanac Algorithm&quot; location=&quot;Lakewood, NJ&quot; latitude=&quot;40.095965&quot; longitude=&quot;-74.22213&quot; elevation=&quot;31.0&quot; timeZoneName=&quot;Eastern Standard Time&quot; timeZoneID=&quot;America/New_York&quot; timeZoneOffset=&quot;-5&quot;&gt;
329             *     &lt;Sunrise&gt;2007-02-18T06:45:27-05:00&lt;/Sunrise&gt;
330             *     &lt;TemporalHour&gt;PT54M17.529S&lt;/TemporalHour&gt;
331             *     ...
332             *   &lt;/AstronomicalTimes&gt;
333             * </pre>
334             * 
335             * Note that the output uses the <a
336             * href="http://www.w3.org/TR/xmlschema11-2/#dateTime">xsd:dateTime</a>
337             * format for times such as sunrise, and <a
338             * href="http://www.w3.org/TR/xmlschema11-2/#duration">xsd:duration</a>
339             * format for times that are a duration such as the length of a
340             * {@link net.sourceforge.zmanim.AstronomicalCalendar#getTemporalHour() temporal hour}. The output of this method is
341             * returned by the {@link #toString() toString} }.
342             * 
343             * @return The XML formatted <code>String</code>. The format will be:
344             * 
345             * <pre>
346             *  &lt;AstronomicalTimes date=&quot;1969-02-08&quot; type=&quot;net.sourceforge.zmanim.AstronomicalCalendar algorithm=&quot;US Naval Almanac Algorithm&quot; location=&quot;Lakewood, NJ&quot; latitude=&quot;40.095965&quot; longitude=&quot;-74.22213&quot; elevation=&quot;31.0&quot; timeZoneName=&quot;Eastern Standard Time&quot; timeZoneID=&quot;America/New_York&quot; timeZoneOffset=&quot;-5&quot;&gt;
347             *     &lt;Sunrise&gt;2007-02-18T06:45:27-05:00&lt;/Sunrise&gt;
348             *     &lt;TemporalHour&gt;PT54M17.529S&lt;/TemporalHour&gt;
349             *     ...
350             *  &lt;/AstronomicalTimes&gt;
351             * </pre>
352             * TODO: add proper schema, and support for nulls. XSD duration (for solar hours), should probably return nil and not P
353             */
354            public static String toXML(AstronomicalCalendar ac) {
355                    ZmanimFormatter formatter = new ZmanimFormatter(
356                                    ZmanimFormatter.XSD_DURATION_FORMAT, new SimpleDateFormat(
357                                                    "yyyy-MM-dd'T'HH:mm:ss"));
358                    DateFormat df = new SimpleDateFormat("yyyy-MM-dd");
359                    
360                    String output = "<";
361                    if (ac.getClass().getName().equals("net.sourceforge.zmanim.AstronomicalCalendar")) {
362                            output += "AstronomicalTimes";
363                            //TODO: use proper schema ref, and maybe build a real schema.
364                            //output += "xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" ";
365                            //output += xsi:schemaLocation="http://www.kosherjava.com/zmanim astronomical.xsd"
366                    } else if (ac.getClass().getName().equals("net.sourceforge.zmanim.ComplexZmanimCalendar")) {
367                            output += "Zmanim";
368                            //TODO: use proper schema ref, and maybe build a real schema.
369                            //output += "xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" ";
370                            //output += xsi:schemaLocation="http://www.kosherjava.com/zmanim zmanim.xsd"
371                    } else if (ac.getClass().getName().equals("net.sourceforge.zmanim.ZmanimCalendar")) {
372                            output += "BasicZmanim";
373                            //TODO: use proper schema ref, and maybe build a real schema.
374                            //output += "xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" ";
375                            //output += xsi:schemaLocation="http://www.kosherjava.com/zmanim basicZmanim.xsd"
376                    }  
377                    output += " date=\"" + df.format(ac.getCalendar().getTime()) + "\"";
378                    output += " type=\"" + ac.getClass().getName() + "\"";
379                    output += " algorithm=\""
380                                    + ac.getAstronomicalCalculator().getCalculatorName() + "\"";
381                    output += " location=\"" + ac.getGeoLocation().getLocationName() + "\"";
382                    output += " latitude=\"" + ac.getGeoLocation().getLatitude() + "\"";
383                    output += " longitude=\"" + ac.getGeoLocation().getLongitude() + "\"";
384                    output += " elevation=\"" + ac.getGeoLocation().getElevation() + "\"";
385                    output += " timeZoneName=\""
386                                    + ac.getGeoLocation().getTimeZone().getDisplayName() + "\"";
387                    output += " timeZoneID=\"" + ac.getGeoLocation().getTimeZone().getID()
388                                    + "\"";
389                    output += " timeZoneOffset=\""
390                                    + (ac.getGeoLocation().getTimeZone().getOffset(
391                                                    ac.getCalendar().getTimeInMillis()) / ((double)HOUR_MILLIS))
392                                    + "\"";
393    
394                    output += ">\n";
395    
396                    Method[] theMethods = ac.getClass().getMethods();
397                    String tagName = "";
398                    Object value = null;
399                    List dateList = new ArrayList();
400                    List durationList = new ArrayList();
401                    List otherList = new ArrayList();
402                    for (int i = 0; i < theMethods.length; i++) {
403                            if (includeMethod(theMethods[i])) {
404                                    tagName = theMethods[i].getName().substring(3);
405                                    //String returnType = theMethods[i].getReturnType().getName();
406                                    try {
407                                            value = theMethods[i].invoke(ac, (Object[]) null);
408                                            if (value == null) {//FIXME: use reflection to determine what the return type is, not the value
409                                                    otherList.add("<" + tagName + ">N/A</" + tagName + ">");
410                                                    //TODO: instead of N/A, return proper nil.
411                                                    //otherList.add("<" + tagName + " xs:nil=\"true\" />");
412                                            } else if (value instanceof Date) {
413                                                    dateList.add(new Zman((Date) value, tagName));
414                                            } else if (value instanceof Long || value instanceof Integer) {// shaah zmanis
415                                                    if(((Long)value).longValue()== Long.MIN_VALUE){
416                                                            otherList.add("<" + tagName + ">N/A</" + tagName + ">");
417                                                            //TODO: instead of N/A, return proper nil.
418                                                            //otherList.add("<" + tagName + " xs:nil=\"true\" />");
419                                                    } else {
420                                                            durationList.add(new Zman((int) ((Long) value).longValue(), tagName));
421                                                    }
422                                            } else { // will probably never enter this block, but is
423                                                    // present to be future proof
424                                                    otherList.add("<" + tagName + ">" + value + "</" + tagName + ">");
425                                            }
426                                    } catch (Exception e) {
427                                            e.printStackTrace();
428                                    }
429                            }
430                    }
431                    Zman zman;
432                    Collections.sort(dateList, Zman.DATE_ORDER);
433                    for (int i = 0; i < dateList.size(); i++) {
434                            zman = (Zman) dateList.get(i);
435                            output += "\t<" + zman.getZmanLabel();
436                            output += ">";
437                            output += formatter.formatDateTime(zman.getZman(), ac.getCalendar())
438                                            + "</" + zman.getZmanLabel() + ">\n";
439                    }
440                    Collections.sort(durationList, Zman.DURATION_ORDER);
441                    for (int i = 0; i < durationList.size(); i++) {
442                            zman = (Zman) durationList.get(i);
443                            output += "\t<" + zman.getZmanLabel();
444                            output += ">";
445                            output += formatter.format((int) zman.getDuration()) + "</"
446                                            + zman.getZmanLabel() + ">\n";
447                    }
448    
449                    for (int i = 0; i < otherList.size(); i++) {// will probably never enter
450                            // this block
451                            output += "\t" + otherList.get(i) + "\n";
452                    }
453    
454                    if (ac.getClass().getName().equals("net.sourceforge.zmanim.AstronomicalCalendar")) {
455                            output += "</AstronomicalTimes>";
456                    } else if (ac.getClass().getName().equals("net.sourceforge.zmanim.ComplexZmanimCalendar")) {
457                            output += "</Zmanim>";
458                    } else if (ac.getClass().getName().equals("net.sourceforge.zmanim.ZmanimCalendar")) {
459                            output += "</Basic>";
460                    }
461                    return output;
462            }
463            
464            /**
465             * Determines if a method should be output by the {@link #toXML(AstronomicalCalendar)}
466             * 
467             * @param method
468             * @return
469             */
470            private static boolean includeMethod(Method method) {
471                    List methodWhiteList = new ArrayList();
472                    // methodWhiteList.add("getName");
473    
474                    List methodBlackList = new ArrayList();
475                    // methodBlackList.add("getGregorianChange");
476    
477                    if (methodWhiteList.contains(method.getName()))
478                            return true;
479                    if (methodBlackList.contains(method.getName()))
480                            return false;
481    
482                    if (method.getParameterTypes().length > 0)
483                            return false; // Skip get methods with parameters since we do not
484                    // know what value to pass
485                    if (!method.getName().startsWith("get"))
486                            return false;
487    
488                    if (method.getReturnType().getName().endsWith("Date")
489                                    || method.getReturnType().getName().endsWith("long")) {
490                            return true;
491                    }
492                    return false;
493            }
494    }