001/**
002 * Licensed to the Apache Software Foundation (ASF) under one
003 * or more contributor license agreements.  See the NOTICE file
004 * distributed with this work for additional information
005 * regarding copyright ownership.  The ASF licenses this file
006 * to you under the Apache License, Version 2.0 (the
007 * "License"); you may not use this file except in compliance
008 * with the License.  You may obtain a copy of the License at
009 *
010 *      http://www.apache.org/licenses/LICENSE-2.0
011 *
012 * Unless required by applicable law or agreed to in writing, software
013 * distributed under the License is distributed on an "AS IS" BASIS,
014 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
015 * See the License for the specific language governing permissions and
016 * limitations under the License.
017 */
018
019package org.apache.oozie.coord;
020
021import com.google.common.collect.Lists;
022
023import org.apache.commons.lang.StringUtils;
024import org.apache.hadoop.conf.Configuration;
025import org.apache.oozie.ErrorCode;
026import org.apache.oozie.client.OozieClient;
027import org.apache.oozie.command.CommandException;
028import org.apache.oozie.coord.input.logic.CoordInputLogicEvaluatorUtil;
029import org.apache.oozie.dependency.URIHandler;
030import org.apache.oozie.dependency.URIHandler.Context;
031import org.apache.oozie.service.Services;
032import org.apache.oozie.service.URIHandlerService;
033import org.apache.oozie.util.DateUtils;
034import org.apache.oozie.util.ELEvaluator;
035import org.apache.oozie.util.ParamChecker;
036import org.apache.oozie.util.XLog;
037import org.jdom.JDOMException;
038
039import java.net.URI;
040import java.util.ArrayList;
041import java.util.Calendar;
042import java.util.Date;
043import java.util.GregorianCalendar;
044import java.util.List;
045import java.util.TimeZone;
046
047/**
048 * This class implements the EL function related to coordinator
049 */
050
051public class CoordELFunctions {
052    final public static String DATASET = "oozie.coord.el.dataset.bean";
053    final public static String COORD_ACTION = "oozie.coord.el.app.bean";
054    final public static String CONFIGURATION = "oozie.coord.el.conf";
055    final public static String LATEST_EL_USE_CURRENT_TIME = "oozie.service.ELService.latest-el.use-current-time";
056    // INSTANCE_SEPARATOR is used to separate multiple directories into one tag.
057    final public static String INSTANCE_SEPARATOR = "#";
058    final public static String DIR_SEPARATOR = ",";
059    // TODO: in next release, support flexibility
060    private static String END_OF_OPERATION_INDICATOR_FILE = "_SUCCESS";
061
062    public static final long MINUTE_MSEC = 60 * 1000L;
063    public static final long HOUR_MSEC = 60 * MINUTE_MSEC;
064    public static final long DAY_MSEC = 24 * HOUR_MSEC;
065    public static final long WEEK_MSEC = 7 * DAY_MSEC;
066
067    /**
068     * Used in defining the frequency in 'day' unit. <p> domain: <code> val &gt; 0</code> and should be integer.
069     *
070     * @param val frequency in number of days.
071     * @return number of days and also set the frequency timeunit to "day"
072     */
073    public static int ph1_coord_days(int val) {
074        val = ParamChecker.checkGTZero(val, "n");
075        ELEvaluator eval = ELEvaluator.getCurrent();
076        eval.setVariable("timeunit", TimeUnit.DAY);
077        eval.setVariable("endOfDuration", TimeUnit.NONE);
078        return val;
079    }
080
081    /**
082     * Used in defining the frequency in 'month' unit. <p> domain: <code> val &gt; 0</code> and should be integer.
083     *
084     * @param val frequency in number of months.
085     * @return number of months and also set the frequency timeunit to "month"
086     */
087    public static int ph1_coord_months(int val) {
088        val = ParamChecker.checkGTZero(val, "n");
089        ELEvaluator eval = ELEvaluator.getCurrent();
090        eval.setVariable("timeunit", TimeUnit.MONTH);
091        eval.setVariable("endOfDuration", TimeUnit.NONE);
092        return val;
093    }
094
095    /**
096     * Used in defining the frequency in 'hour' unit. <p> parameter value domain: <code> val &gt; 0</code> and should
097     * be integer.
098     *
099     * @param val frequency in number of hours.
100     * @return number of minutes and also set the frequency timeunit to "minute"
101     */
102    public static int ph1_coord_hours(int val) {
103        val = ParamChecker.checkGTZero(val, "n");
104        ELEvaluator eval = ELEvaluator.getCurrent();
105        eval.setVariable("timeunit", TimeUnit.MINUTE);
106        eval.setVariable("endOfDuration", TimeUnit.NONE);
107        return val * 60;
108    }
109
110    /**
111     * Used in defining the frequency in 'minute' unit. <p> domain: <code> val &gt; 0</code> and should be integer.
112     *
113     * @param val frequency in number of minutes.
114     * @return number of minutes and also set the frequency timeunit to "minute"
115     */
116    public static int ph1_coord_minutes(int val) {
117        val = ParamChecker.checkGTZero(val, "n");
118        ELEvaluator eval = ELEvaluator.getCurrent();
119        eval.setVariable("timeunit", TimeUnit.MINUTE);
120        eval.setVariable("endOfDuration", TimeUnit.NONE);
121        return val;
122    }
123
124    /**
125     * Used in defining the frequency in 'day' unit and specify the "end of day" property. <p> Every instance will
126     * start at 00:00 hour of each day. <p> domain: <code> val &gt; 0</code> and should be integer.
127     *
128     * @param val frequency in number of days.
129     * @return number of days and also set the frequency timeunit to "day" and end_of_duration flag to "day"
130     */
131    public static int ph1_coord_endOfDays(int val) {
132        val = ParamChecker.checkGTZero(val, "n");
133        ELEvaluator eval = ELEvaluator.getCurrent();
134        eval.setVariable("timeunit", TimeUnit.DAY);
135        eval.setVariable("endOfDuration", TimeUnit.END_OF_DAY);
136        return val;
137    }
138
139    /**
140     * Used in defining the frequency in 'week' unit and specify the "end of
141     * week" property.
142     * <p>
143     * Every instance will start at 00:00 hour of start of week
144     * <p>
145     * domain: <code> val &gt; 0</code> and should be integer.
146     *
147     * @param val frequency in number of weeks.
148     * @return number of weeks and also set the frequency timeunit to week of
149     *         the year and end_of_duration flag to week of the year
150     */
151    public static int ph1_coord_endOfWeeks(int val) {
152        val = ParamChecker.checkGTZero(val, "n");
153        ELEvaluator eval = ELEvaluator.getCurrent();
154        eval.setVariable("timeunit", TimeUnit.WEEK);
155        eval.setVariable("endOfDuration", TimeUnit.END_OF_WEEK);
156        return val;
157    }
158
159
160    /**
161     * Used in defining the frequency in 'month' unit and specify the "end of month" property. <p> Every instance will
162     * start at first day of each month at 00:00 hour. <p> domain: <code> val &gt; 0</code> and should be integer.
163     *
164     * @param val frequency in number of months.
165     * @return number of months and also set the frequency timeunit to "month" and end_of_duration flag to "month"
166     */
167    public static int ph1_coord_endOfMonths(int val) {
168        val = ParamChecker.checkGTZero(val, "n");
169        ELEvaluator eval = ELEvaluator.getCurrent();
170        eval.setVariable("timeunit", TimeUnit.MONTH);
171        eval.setVariable("endOfDuration", TimeUnit.END_OF_MONTH);
172        return val;
173    }
174
175    /**
176     * Calculate the difference of timezone offset in minutes between dataset and coordinator job. <p> Depends on: <p>
177     * 1. Timezone of both dataset and job <p> 2. Action creation Time
178     *
179     * @return difference in minutes (DataSet TZ Offset - Application TZ offset)
180     */
181    public static int ph2_coord_tzOffset() {
182        long actionCreationTime = getActionCreationtime().getTime();
183        TimeZone dsTZ = ParamChecker.notNull(getDatasetTZ(), "DatasetTZ");
184        TimeZone jobTZ = ParamChecker.notNull(getJobTZ(), "JobTZ");
185        return (dsTZ.getOffset(actionCreationTime) - jobTZ.getOffset(actionCreationTime)) / (1000 * 60);
186    }
187
188    public static int ph3_coord_tzOffset() {
189        return ph2_coord_tzOffset();
190    }
191
192    /**
193     * Returns a date string that is offset from 'strBaseDate' by the amount specified.  The unit can be one of
194     * DAY, MONTH, HOUR, MINUTE, MONTH.
195     *
196     * @param strBaseDate The base date
197     * @param offset any number
198     * @param unit one of DAY, MONTH, HOUR, MINUTE, MONTH
199     * @return the offset date string
200     * @throws Exception in case of error
201     */
202    public static String ph2_coord_dateOffset(String strBaseDate, int offset, String unit) throws Exception {
203        Calendar baseCalDate = DateUtils.getCalendar(strBaseDate);
204        StringBuilder buffer = new StringBuilder();
205        baseCalDate.add(TimeUnit.valueOf(unit).getCalendarUnit(), offset);
206        buffer.append(DateUtils.formatDateOozieTZ(baseCalDate));
207        return buffer.toString();
208    }
209
210    public static String ph3_coord_dateOffset(String strBaseDate, int offset, String unit) throws Exception {
211        return ph2_coord_dateOffset(strBaseDate, offset, unit);
212    }
213
214    /**
215     * Returns a date string that is offset from 'strBaseDate' by the difference from Oozie processing timezone to the given
216     * timezone. It will account for daylight saving time based on the given 'strBaseDate' and 'timezone'.
217     *
218     * @param strBaseDate The base date
219     * @param timezone the timezone
220     * @return the offset date string
221     * @throws Exception in case of error
222     */
223    public static String ph2_coord_dateTzOffset(String strBaseDate, String timezone) throws Exception {
224        Calendar baseCalDate = DateUtils.getCalendar(strBaseDate);
225        StringBuilder buffer = new StringBuilder();
226        baseCalDate.setTimeZone(DateUtils.getTimeZone(timezone));
227        buffer.append(DateUtils.formatDate(baseCalDate));
228        return buffer.toString();
229    }
230
231    public static String ph3_coord_dateTzOffset(String strBaseDate, String timezone) throws Exception{
232        return ph2_coord_dateTzOffset(strBaseDate, timezone);
233    }
234
235    /**
236     * Determine the date-time in Oozie processing timezone of n-th future available dataset instance
237     * from nominal Time but not beyond the instance specified as 'instance.
238     * <p>
239     * It depends on:
240     * <p>
241     * 1. Data set frequency
242     * <p>
243     * 2. Data set Time unit (day, month, minute)
244     * <p>
245     * 3. Data set Time zone/DST
246     * <p>
247     * 4. End Day/Month flag
248     * <p>
249     * 5. Data set initial instance
250     * <p>
251     * 6. Action Creation Time
252     * <p>
253     * 7. Existence of dataset's directory
254     *
255     * @param n :instance count
256     *        <p>
257     *        domain: n &gt;= 0, n is integer
258     * @param instance How many future instance it should check? value should
259     *        be &gt;=0
260     * @return date-time in Oozie processing timezone of the n-th instance
261     *         <p>
262     * @throws Exception if the dataset is asynchronous
263     */
264    public static String ph3_coord_future(int n, int instance) throws Exception {
265        ParamChecker.checkGEZero(n, "future:n");
266        ParamChecker.checkGTZero(instance, "future:instance");
267        if (isSyncDataSet()) {// For Sync Dataset
268            return coord_future_sync(n, instance);
269        }
270        else {
271            throw new UnsupportedOperationException("Asynchronous Dataset is not supported yet");
272        }
273    }
274
275    /**
276     * Determine the date-time in Oozie processing timezone of the future available dataset instances
277     * from start to end offsets from nominal Time but not beyond the instance specified as 'instance'.
278     * <p>
279     * It depends on:
280     * <p>
281     * 1. Data set frequency
282     * <p>
283     * 2. Data set Time unit (day, month, minute)
284     * <p>
285     * 3. Data set Time zone/DST
286     * <p>
287     * 4. End Day/Month flag
288     * <p>
289     * 5. Data set initial instance
290     * <p>
291     * 6. Action Creation Time
292     * <p>
293     * 7. Existence of dataset's directory
294     *
295     * @param start : start instance offset
296     *        <p>
297     *        domain: start &gt;= 0, start is integer
298     * @param end : end instance offset
299     *        <p>
300     *        domain: end &gt;= 0, end is integer
301     * @param instance How many future instance it should check? value should
302     *        be &gt;=0
303     * @return date-time in Oozie processing timezone of the instances from start to end offsets
304     *        delimited by comma.
305     * @throws Exception if the dataset is asynchronous
306     */
307    public static String ph3_coord_futureRange(int start, int end, int instance) throws Exception {
308        ParamChecker.checkGEZero(start, "future:n");
309        ParamChecker.checkGEZero(end, "future:n");
310        ParamChecker.checkGTZero(instance, "future:instance");
311        if (isSyncDataSet()) {// For Sync Dataset
312            return coord_futureRange_sync(start, end, instance);
313        }
314        else {
315            throw new UnsupportedOperationException("Asynchronous Dataset is not supported yet");
316        }
317    }
318
319    private static String coord_future_sync(int n, int instance) throws Exception {
320        return coord_futureRange_sync(n, n, instance);
321    }
322
323    private static String coord_futureRange_sync(int startOffset, int endOffset, int instance) throws Exception {
324        final XLog LOG = XLog.getLog(CoordELFunctions.class);
325        final Thread currentThread = Thread.currentThread();
326        ELEvaluator eval = ELEvaluator.getCurrent();
327        String retVal = "";
328        int datasetFrequency = (int) getDSFrequency();// in minutes
329        TimeUnit dsTimeUnit = getDSTimeUnit();
330        int[] instCount = new int[1];
331        Calendar nominalInstanceCal = getCurrentInstance(getActionCreationtime(), instCount);
332        StringBuilder resolvedInstances = new StringBuilder();
333        StringBuilder resolvedURIPaths = new StringBuilder();
334        if (nominalInstanceCal != null) {
335            Calendar initInstance = getInitialInstanceCal();
336            nominalInstanceCal = (Calendar) initInstance.clone();
337            nominalInstanceCal.add(dsTimeUnit.getCalendarUnit(), instCount[0] * datasetFrequency);
338
339            SyncCoordDataset ds = (SyncCoordDataset) eval.getVariable(DATASET);
340            if (ds == null) {
341                throw new RuntimeException("Associated Dataset should be defined with key " + DATASET);
342            }
343            String uriTemplate = ds.getUriTemplate();
344            Configuration conf = (Configuration) eval.getVariable(CONFIGURATION);
345            if (conf == null) {
346                throw new RuntimeException("Associated Configuration should be defined with key " + CONFIGURATION);
347            }
348            int available = 0, checkedInstance = 0;
349            boolean resolved = false;
350            String user = ParamChecker
351                    .notEmpty((String) eval.getVariable(OozieClient.USER_NAME), OozieClient.USER_NAME);
352            String doneFlag = ds.getDoneFlag();
353            URIHandlerService uriService = Services.get().get(URIHandlerService.class);
354            URIHandler uriHandler = null;
355            Context uriContext = null;
356            try {
357                while (instance >= checkedInstance && !currentThread.isInterrupted()) {
358                    ELEvaluator uriEval = getUriEvaluator(nominalInstanceCal);
359                    String uriPath = uriEval.evaluate(uriTemplate, String.class);
360                    if (uriHandler == null) {
361                        URI uri = new URI(uriPath);
362                        uriHandler = uriService.getURIHandler(uri);
363                        uriContext = uriHandler.getContext(uri, conf, user, true);
364                    }
365                    String uriWithDoneFlag = uriHandler.getURIWithDoneFlag(uriPath, doneFlag);
366                    if (uriHandler.exists(new URI(uriWithDoneFlag), uriContext)) {
367                        if (available == endOffset) {
368                            LOG.debug("Matched future(" + available + "): " + uriWithDoneFlag);
369                            resolved = true;
370                            resolvedInstances.append(DateUtils.formatDateOozieTZ(nominalInstanceCal));
371                            resolvedURIPaths.append(uriPath);
372                            retVal = resolvedInstances.toString();
373                            eval.setVariable(CoordELConstants.RESOLVED_PATH, resolvedURIPaths.toString());
374                            break;
375                        }
376                        else if (available >= startOffset) {
377                            LOG.debug("Matched future(" + available + "): " + uriWithDoneFlag);
378                            resolvedInstances.append(DateUtils.formatDateOozieTZ(nominalInstanceCal)).append(
379                                    INSTANCE_SEPARATOR);
380                            resolvedURIPaths.append(uriPath).append(INSTANCE_SEPARATOR);
381
382                        }
383                        available++;
384                    }
385                    // nominalInstanceCal.add(dsTimeUnit.getCalendarUnit(), datasetFrequency);
386                    nominalInstanceCal = (Calendar) initInstance.clone();
387                    instCount[0]++;
388                    nominalInstanceCal.add(dsTimeUnit.getCalendarUnit(), instCount[0] * datasetFrequency);
389                    checkedInstance++;
390                    // DateUtils.moveToEnd(nominalInstanceCal, getDSEndOfFlag());
391                }
392                if (!StringUtils.isEmpty(resolvedURIPaths.toString()) && eval.getVariable(CoordELConstants.RESOLVED_PATH) == null) {
393                    eval.setVariable(CoordELConstants.RESOLVED_PATH, resolvedURIPaths.toString());
394                }
395
396            }
397            finally {
398                if (uriContext != null) {
399                    uriContext.destroy();
400                }
401            }
402            if (!resolved) {
403                // return unchanged future function with variable 'is_resolved'
404                // to 'false'
405                eval.setVariable(CoordELConstants.IS_RESOLVED, Boolean.FALSE);
406                if (startOffset == endOffset) {
407                    retVal = "${coord:future(" + startOffset + ", " + instance + ")}";
408                }
409                else {
410                    retVal = "${coord:futureRange(" + startOffset + ", " + endOffset + ", " + instance + ")}";
411                }
412            }
413            else {
414                eval.setVariable(CoordELConstants.IS_RESOLVED, Boolean.TRUE);
415            }
416        }
417        else {// No feasible nominal time
418            eval.setVariable(CoordELConstants.IS_RESOLVED, Boolean.TRUE);
419            retVal = "";
420        }
421        return retVal;
422    }
423
424    /**
425     * Return nominal time or Action Creation Time.
426     *
427     * @return coordinator action creation or materialization date time
428     * @throws Exception if unable to format the Date object to String
429     */
430    public static String ph2_coord_nominalTime() throws Exception {
431        ELEvaluator eval = ELEvaluator.getCurrent();
432        SyncCoordAction action = ParamChecker.notNull((SyncCoordAction) eval.getVariable(COORD_ACTION),
433                "Coordinator Action");
434        return DateUtils.formatDateOozieTZ(action.getNominalTime());
435    }
436
437    public static String ph3_coord_nominalTime() throws Exception {
438        return ph2_coord_nominalTime();
439    }
440
441    /**
442     * Convert from standard date-time formatting to a desired format.
443     * @param dateTimeStr - A timestamp in standard (ISO8601) format.
444     * @param format - A string representing the desired format.
445     * @return coordinator action creation or materialization date time
446     * @throws Exception if unable to format the Date object to String
447     */
448    public static String ph2_coord_formatTime(String dateTimeStr, String format)
449            throws Exception {
450        Date dateTime = DateUtils.parseDateOozieTZ(dateTimeStr);
451        return DateUtils.formatDateCustom(dateTime, format);
452    }
453
454    public static String ph3_coord_formatTime(String dateTimeStr, String format)
455            throws Exception {
456        return ph2_coord_formatTime(dateTimeStr, format);
457    }
458
459    /**
460     * Convert from standard date-time formatting to a Unix epoch time.
461     * @param dateTimeStr - A timestamp in standard (ISO8601) format.
462     * @param millis - "true" to include millis; otherwise will only include seconds
463     * @return coordinator action creation or materialization date time
464     * @throws Exception if unable to format the Date object to String
465     */
466    public static String ph2_coord_epochTime(String dateTimeStr, String millis)
467            throws Exception {
468        Date dateTime = DateUtils.parseDateOozieTZ(dateTimeStr);
469        return DateUtils.formatDateEpoch(dateTime, Boolean.valueOf(millis));
470    }
471
472    public static String ph3_coord_epochTime(String dateTimeStr, String millis)
473            throws Exception {
474        return  ph2_coord_epochTime(dateTimeStr, millis);
475    }
476
477    /**
478     * Return Action Id.
479     *
480     * @return coordinator action Id
481     */
482    public static String ph2_coord_actionId() throws Exception {
483        ELEvaluator eval = ELEvaluator.getCurrent();
484        SyncCoordAction action = ParamChecker.notNull((SyncCoordAction) eval.getVariable(COORD_ACTION),
485                "Coordinator Action");
486        return action.getActionId();
487    }
488
489    public static String ph3_coord_actionId() throws Exception {
490        return ph2_coord_actionId();
491    }
492
493    /**
494     * Return Job Name. <p>
495     *
496     * @return coordinator name
497     */
498    public static String ph2_coord_name() throws Exception {
499        ELEvaluator eval = ELEvaluator.getCurrent();
500        SyncCoordAction action = ParamChecker.notNull((SyncCoordAction) eval.getVariable(COORD_ACTION),
501                "Coordinator Action");
502        return action.getName();
503    }
504
505    public static String ph3_coord_name() throws Exception {
506        return ph2_coord_name();
507    }
508
509    /**
510     * Return Action Start time. <p>
511     *
512     * @return coordinator action start time
513     * @throws Exception if unable to format the Date object to String
514     */
515    public static String ph2_coord_actualTime() throws Exception {
516        ELEvaluator eval = ELEvaluator.getCurrent();
517        SyncCoordAction coordAction = (SyncCoordAction) eval.getVariable(COORD_ACTION);
518        if (coordAction == null) {
519            throw new RuntimeException("Associated Application instance should be defined with key " + COORD_ACTION);
520        }
521        return DateUtils.formatDateOozieTZ(coordAction.getActualTime());
522    }
523
524    public static String ph3_coord_actualTime() throws Exception {
525        return ph2_coord_actualTime();
526    }
527
528    /**
529     * Used to specify a list of URI's that are used as input dir to the workflow job. <p> Look for two evaluator-level
530     * variables <p> A) .datain.&lt;DATAIN_NAME&gt; B) .datain.&lt;DATAIN_NAME&gt;.unresolved <p> A defines the current list of
531     * URI. <p> B defines whether there are any unresolved EL-function (i.e latest) <p> If there are something
532     * unresolved, this function will echo back the original function <p> otherwise it sends the uris.
533     *
534     * @param dataInName : Datain name
535     * @return the list of URI's separated by INSTANCE_SEPARATOR <p> if there are unresolved EL function (i.e. latest)
536     *         , echo back <p> the function without resolving the function.
537     */
538    public static String ph3_coord_dataIn(String dataInName) {
539        String uris = "";
540        ELEvaluator eval = ELEvaluator.getCurrent();
541        if (eval.getVariable(".datain." + dataInName) == null
542                && (eval.getVariable(".actionInputLogic") != null && !StringUtils.isEmpty(eval.getVariable(
543                        ".actionInputLogic").toString()))) {
544            try {
545                return new CoordInputLogicEvaluatorUtil().getInputDependencies(dataInName,
546                        (SyncCoordAction) eval.getVariable(COORD_ACTION));
547            }
548            catch (JDOMException e) {
549                XLog.getLog(CoordELFunctions.class).error(e);
550                throw new RuntimeException(e.getMessage());
551            }
552        }
553
554        uris = (String) eval.getVariable(".datain." + dataInName);
555        Object unResolvedObj = eval.getVariable(".datain." + dataInName + ".unresolved");
556        if (unResolvedObj == null) {
557            return uris;
558        }
559        Boolean unresolved = Boolean.parseBoolean(unResolvedObj.toString());
560        if (unresolved != null && unresolved.booleanValue() == true) {
561            return "${coord:dataIn('" + dataInName + "')}";
562        }
563        return uris;
564    }
565
566    /**
567     * Used to specify a list of URI's that are output dir of the workflow job. <p> Look for one evaluator-level
568     * variable <p> dataout.&lt;DATAOUT_NAME&gt; <p> It defines the current list of URI. <p> otherwise it sends the uris.
569     *
570     * @param dataOutName : Dataout name
571     * @return the list of URI's separated by INSTANCE_SEPARATOR
572     */
573    public static String ph3_coord_dataOut(String dataOutName) {
574        String uris = "";
575        ELEvaluator eval = ELEvaluator.getCurrent();
576        uris = (String) eval.getVariable(".dataout." + dataOutName);
577        return uris;
578    }
579
580    /**
581     * Determine the date-time in Oozie processing timezone of n-th dataset instance. <p> It depends on: <p> 1.
582     * Data set frequency <p> 2.
583     * Data set Time unit (day, month, minute) <p> 3. Data set Time zone/DST <p> 4. End Day/Month flag <p> 5. Data
584     * set initial instance <p> 6. Action Creation Time
585     *
586     * @param n instance count domain: n is integer
587     * @return date-time in Oozie processing timezone of the n-th instance returns 'null' means n-th instance is
588     * earlier than Initial-Instance of DS
589     * @throws Exception
590     */
591    public static String ph2_coord_current(int n) throws Exception {
592        if (isSyncDataSet()) { // For Sync Dataset
593            return coord_current_sync(n);
594        }
595        else {
596            throw new UnsupportedOperationException("Asynchronous Dataset is not supported yet");
597        }
598    }
599
600    /**
601     * Determine the date-time in Oozie processing timezone of current dataset instances
602     * from start to end offsets from the nominal time. <p> It depends
603     * on: <p> 1. Data set frequency <p> 2. Data set Time unit (day, month, minute) <p> 3. Data set Time zone/DST
604     * <p> 4. End Day/Month flag <p> 5. Data set initial instance <p> 6. Action Creation Time
605     *
606     * @param start :start instance offset <p> domain: start &lt;= 0, start is integer
607     * @param end :end instance offset <p> domain: end &lt;= 0, end is integer
608     * @return date-time in Oozie processing timezone of the instances from start to end offsets
609     *        delimited by comma. <p> If the current instance time of the dataset based on the Action Creation Time
610     *        is earlier than the Initial-Instance of DS an empty string is returned.
611     *        If an instance within the range is earlier than Initial-Instance of DS that instance is ignored
612     * @throws Exception
613     */
614    public static String ph2_coord_currentRange(int start, int end) throws Exception {
615        if (isSyncDataSet()) { // For Sync Dataset
616            return coord_currentRange_sync(start, end);
617        }
618        else {
619            throw new UnsupportedOperationException("Asynchronous Dataset is not supported yet");
620        }
621    }
622    /**
623     * Determine the date-time in Oozie processing timezone of the given offset from the dataset effective nominal time. <p> It
624     * depends on: <p> 1. Data set frequency <p> 2. Data set Time Unit <p> 3. Data set Time zone/DST
625     * <p> 4. Data set initial instance <p> 5. Action Creation Time
626     *
627     * @param n offset amount (integer)
628     * @param timeUnit TimeUnit for offset n ("MINUTE", "HOUR", "DAY", "MONTH", "YEAR")
629     * @return date-time in Oozie processing timezone of the given offset from the dataset effective nominal time
630     * @throws Exception if there was a problem formatting
631     */
632    public static String ph2_coord_offset(int n, String timeUnit) throws Exception {
633        if (isSyncDataSet()) { // For Sync Dataset
634            return coord_offset_sync(n, timeUnit);
635        }
636        else {
637            throw new UnsupportedOperationException("Asynchronous Dataset is not supported yet");
638        }
639    }
640
641    /**
642     * Determine how many hours is on the date of n-th dataset instance. <p> It depends on: <p> 1. Data set frequency
643     * <p> 2. Data set Time unit (day, month, minute) <p> 3. Data set Time zone/DST <p> 4. End Day/Month flag <p> 5.
644     * Data set initial instance <p> 6. Action Creation Time
645     *
646     * @param n instance count <p> domain: n is integer
647     * @return number of hours on that day <p> returns -1 means n-th instance is earlier than Initial-Instance of DS
648     * @throws Exception
649     */
650    public static int ph2_coord_hoursInDay(int n) throws Exception {
651        int datasetFrequency = (int) getDSFrequency();
652        // /Calendar nominalInstanceCal =
653        // getCurrentInstance(getActionCreationtime());
654        Calendar nominalInstanceCal = getEffectiveNominalTime();
655        if (nominalInstanceCal == null) {
656            return -1;
657        }
658        nominalInstanceCal.add(getDSTimeUnit().getCalendarUnit(), datasetFrequency * n);
659        /*
660         * if (nominalInstanceCal.getTime().compareTo(getInitialInstance()) < 0)
661         * { return -1; }
662         */
663        nominalInstanceCal.setTimeZone(getDatasetTZ());// Use Dataset TZ
664        // DateUtils.moveToEnd(nominalInstanceCal, getDSEndOfFlag());
665        return DateUtils.hoursInDay(nominalInstanceCal);
666    }
667
668    public static int ph3_coord_hoursInDay(int n) throws Exception {
669        return ph2_coord_hoursInDay(n);
670    }
671
672    /**
673     * Calculate number of days in one month for n-th dataset instance. <p> It depends on: <p> 1. Data set frequency .
674     * <p> 2. Data set Time unit (day, month, minute) <p> 3. Data set Time zone/DST <p> 4. End Day/Month flag <p> 5.
675     * Data set initial instance <p> 6. Action Creation Time
676     *
677     * @param n instance count. domain: n is integer
678     * @return number of days in that month <p> returns -1 means n-th instance is earlier than Initial-Instance of DS
679     * @throws Exception
680     */
681    public static int ph2_coord_daysInMonth(int n) throws Exception {
682        int datasetFrequency = (int) getDSFrequency();// in minutes
683        // Calendar nominalInstanceCal =
684        // getCurrentInstance(getActionCreationtime());
685        Calendar nominalInstanceCal = getEffectiveNominalTime();
686        if (nominalInstanceCal == null) {
687            return -1;
688        }
689        nominalInstanceCal.add(getDSTimeUnit().getCalendarUnit(), datasetFrequency * n);
690        /*
691         * if (nominalInstanceCal.getTime().compareTo(getInitialInstance()) < 0)
692         * { return -1; }
693         */
694        nominalInstanceCal.setTimeZone(getDatasetTZ());// Use Dataset TZ
695        // DateUtils.moveToEnd(nominalInstanceCal, getDSEndOfFlag());
696        return nominalInstanceCal.getActualMaximum(Calendar.DAY_OF_MONTH);
697    }
698
699    public static int ph3_coord_daysInMonth(int n) throws Exception {
700        return ph2_coord_daysInMonth(n);
701    }
702
703    /**
704     * Determine the date-time in Oozie processing timezone of n-th latest available dataset instance. <p> It depends
705     * on: <p> 1. Data set frequency <p> 2. Data set Time unit (day, month, minute) <p> 3. Data set Time zone/DST
706     * <p> 4. End Day/Month flag <p> 5. Data set initial instance <p> 6. Action Creation Time <p> 7. Existence of
707     * dataset's directory
708     *
709     * @param n :instance count <p> domain: n &lt;= 0, n is integer
710     * @return date-time in Oozie processing timezone of the n-th instance <p> returns 'null' means n-th instance is
711     * earlier than Initial-Instance of DS
712     * @throws Exception
713     */
714    public static String ph3_coord_latest(int n) throws Exception {
715        ParamChecker.checkLEZero(n, "latest:n");
716        if (isSyncDataSet()) {// For Sync Dataset
717            return coord_latest_sync(n);
718        }
719        else {
720            throw new UnsupportedOperationException("Asynchronous Dataset is not supported yet");
721        }
722    }
723
724    /**
725     * Determine the date-time in Oozie processing timezone of latest available dataset instances
726     * from start to end offsets from the nominal time. <p> It depends
727     * on: <p> 1. Data set frequency <p> 2. Data set Time unit (day, month, minute) <p> 3. Data set Time zone/DST
728     * <p> 4. End Day/Month flag <p> 5. Data set initial instance <p> 6. Action Creation Time <p> 7. Existence of
729     * dataset's directory
730     *
731     * @param start :start instance offset <p> domain: start &lt;= 0, start is integer
732     * @param end :end instance offset <p> domain: end &lt;= 0, end is integer
733     * @return date-time in Oozie processing timezone of the instances from start to end offsets
734     *        delimited by comma. <p> returns 'null' means start offset instance is
735     *        earlier than Initial-Instance of DS
736     * @throws Exception
737     */
738    public static String ph3_coord_latestRange(int start, int end) throws Exception {
739        ParamChecker.checkLEZero(start, "latest:n");
740        ParamChecker.checkLEZero(end, "latest:n");
741        if (isSyncDataSet()) {// For Sync Dataset
742            return coord_latestRange_sync(start, end);
743        }
744        else {
745            throw new UnsupportedOperationException("Asynchronous Dataset is not supported yet");
746        }
747    }
748
749    /**
750     * Configure an evaluator with data set and application specific information. <p> Helper method of associating
751     * dataset and application object
752     *
753     * @param evaluator : to set variables
754     * @param ds : Data Set object
755     * @param coordAction : Application instance
756     */
757    public static void configureEvaluator(ELEvaluator evaluator, SyncCoordDataset ds, SyncCoordAction coordAction) {
758        evaluator.setVariable(COORD_ACTION, coordAction);
759        evaluator.setVariable(DATASET, ds);
760    }
761
762    /**
763     * Helper method to wrap around with "${..}". <p>
764     *
765     *
766     * @param eval :EL evaluator
767     * @param expr : expression to evaluate
768     * @return Resolved expression or echo back the same expression
769     * @throws Exception
770     */
771    public static String evalAndWrap(ELEvaluator eval, String expr) throws Exception {
772        try {
773            eval.setVariable(".wrap", null);
774            String result = eval.evaluate(expr, String.class);
775            if (eval.getVariable(".wrap") != null) {
776                return "${" + result + "}";
777            }
778            else {
779                return result;
780            }
781        }
782        catch (Exception e) {
783            throw new ElException(ErrorCode.E1004, "Unable to evaluate :" + expr + ":\n", e);
784        }
785    }
786
787    // Set of echo functions
788
789    public static String ph1_coord_current_echo(String n) {
790        return echoUnResolved("current", n);
791    }
792
793    public static String ph1_coord_absolute_echo(String date) {
794        return echoUnResolved("absolute", date);
795    }
796
797    public static String ph1_coord_endOfMonths_echo(String date) {
798        return echoUnResolved("endOfMonths", date);
799    }
800
801    public static String ph1_coord_endOfWeeks_echo(String date) {
802        return echoUnResolved("endOfWeeks", date);
803    }
804
805    public static String ph1_coord_endOfDays_echo(String date) {
806        return echoUnResolved("endOfDays", date);
807    }
808
809    public static String ph1_coord_currentRange_echo(String start, String end) {
810        return echoUnResolved("currentRange", start + ", " + end);
811    }
812
813    public static String ph1_coord_offset_echo(String n, String timeUnit) {
814        return echoUnResolved("offset", n + " , " + timeUnit);
815    }
816
817    public static String ph2_coord_current_echo(String n) {
818        return echoUnResolved("current", n);
819    }
820
821    public static String ph2_coord_currentRange_echo(String start, String end) {
822        return echoUnResolved("currentRange", start + ", " + end);
823    }
824
825    public static String ph2_coord_offset_echo(String n, String timeUnit) {
826        return echoUnResolved("offset", n + " , " + timeUnit);
827    }
828
829    public static String ph2_coord_absolute_echo(String date) {
830        return echoUnResolved("absolute", date);
831    }
832
833    public static String ph2_coord_endOfMonths_echo(String date) {
834        return echoUnResolved("endOfMonths", date);
835    }
836
837    public static String ph2_coord_endOfWeeks_echo(String date) {
838        return echoUnResolved("endOfWeeks", date);
839    }
840
841    public static String ph2_coord_endOfDays_echo(String date) {
842        return echoUnResolved("endOfDays", date);
843    }
844
845    public static String ph2_coord_absolute_range(String startInstance, int end) throws Exception {
846        int[] instanceCount = new int[1];
847        Calendar startInstanceCal = DateUtils.getCalendar(startInstance);
848        Calendar currentInstance = getCurrentInstance(startInstanceCal.getTime(), instanceCount);
849        // getCurrentInstance() returns null, which means startInstance is less
850        // than initial instance
851        if (currentInstance == null) {
852            throw new CommandException(ErrorCode.E1010,
853                    "initial-instance should be equal or earlier than the start-instance. initial-instance is "
854                            + getInitialInstance() + " and start-instance is " + startInstance);
855        }
856        if (currentInstance.getTimeInMillis() != startInstanceCal.getTimeInMillis()) {
857            throw new CommandException(ErrorCode.E1010,
858                    "initial-instance is not in phase with start-instance. initial-instance is "
859                            + DateUtils.formatDateOozieTZ(getInitialInstanceCal()) + " and start-instance is "
860                            + DateUtils.formatDateOozieTZ(startInstanceCal));
861        }
862        int[] nominalCount = new int[1];
863        if (getCurrentInstance(getActionCreationtime(), nominalCount) == null) {
864            throw new CommandException(ErrorCode.E1010,
865                    "initial-instance should be equal or earlier than the nominal time. initial-instance is "
866                            + getInitialInstance() + " and nominal time is " + getActionCreationtime());
867        }
868        // getCurrentInstance return offset relative to initial instance.
869        // start instance offset - nominal offset = start offset relative to
870        // nominal time-stamp.
871        int start = instanceCount[0] - nominalCount[0];
872        if (start > end) {
873            throw new CommandException(ErrorCode.E1010,
874                    "start-instance should be equal or earlier than the end-instance. startInstance is "
875                            + startInstance + " which is equivalent to current (" + instanceCount[0]
876                            + ") but end is specified as current (" + end + ")");
877        }
878        return ph2_coord_currentRange(start, end);
879    }
880
881    public static String ph1_coord_dateOffset_echo(String n, String offset, String unit) {
882        return echoUnResolved("dateOffset", n + " , " + offset + " , " + unit);
883    }
884
885    public static String ph1_coord_dateTzOffset_echo(String n, String timezone) {
886        return echoUnResolved("dateTzOffset", n + " , " + timezone);
887    }
888
889    public static String ph1_coord_epochTime_echo(String dateTime, String millis) {
890        // Quote the dateTime value since it would contain a ':'.
891        return echoUnResolved("epochTime", "'"+dateTime+"'" + " , " + millis);
892    }
893
894    public static String ph1_coord_formatTime_echo(String dateTime, String format) {
895        // Quote the dateTime value since it would contain a ':'.
896        return echoUnResolved("formatTime", "'"+dateTime+"'" + " , " + format);
897    }
898
899    public static String ph1_coord_latest_echo(String n) {
900        return echoUnResolved("latest", n);
901    }
902
903    public static String ph2_coord_latest_echo(String n) {
904        return ph1_coord_latest_echo(n);
905    }
906
907    public static String ph1_coord_future_echo(String n, String instance) {
908        return echoUnResolved("future", n + ", " + instance + "");
909    }
910
911    public static String ph2_coord_future_echo(String n, String instance) {
912        return ph1_coord_future_echo(n, instance);
913    }
914
915    public static String ph1_coord_latestRange_echo(String start, String end) {
916        return echoUnResolved("latestRange", start + ", " + end);
917    }
918
919    public static String ph2_coord_latestRange_echo(String start, String end) {
920        return ph1_coord_latestRange_echo(start, end);
921    }
922
923    public static String ph1_coord_futureRange_echo(String start, String end, String instance) {
924        return echoUnResolved("futureRange", start + ", " + end + ", " + instance);
925    }
926
927    public static String ph2_coord_futureRange_echo(String start, String end, String instance) {
928        return ph1_coord_futureRange_echo(start, end, instance);
929    }
930
931    public static String ph1_coord_dataIn_echo(String n) {
932        ELEvaluator eval = ELEvaluator.getCurrent();
933        String val = (String) eval.getVariable("oozie.dataname." + n);
934        if ((val == null || val.equals("data-in") == false)) {
935            XLog.getLog(CoordELFunctions.class).error("data_in_name " + n + " is not valid");
936            throw new RuntimeException("data_in_name " + n + " is not valid");
937        }
938        return echoUnResolved("dataIn", "'" + n + "'");
939    }
940
941    public static String ph1_coord_dataOut_echo(String n) {
942        ELEvaluator eval = ELEvaluator.getCurrent();
943        String val = (String) eval.getVariable("oozie.dataname." + n);
944        if (val == null || val.equals("data-out") == false) {
945            XLog.getLog(CoordELFunctions.class).error("data_out_name " + n + " is not valid");
946            throw new RuntimeException("data_out_name " + n + " is not valid");
947        }
948        return echoUnResolved("dataOut", "'" + n + "'");
949    }
950
951    public static String ph1_coord_nominalTime_echo() {
952        return echoUnResolved("nominalTime", "");
953    }
954
955    public static String ph1_coord_nominalTime_echo_wrap() {
956        // return "${coord:nominalTime()}"; // no resolution
957        return echoUnResolved("nominalTime", "");
958    }
959
960    public static String ph1_coord_nominalTime_echo_fixed() {
961        return "2009-03-06T010:00"; // Dummy resolution
962    }
963
964    public static String ph1_coord_actualTime_echo_wrap() {
965        // return "${coord:actualTime()}"; // no resolution
966        return echoUnResolved("actualTime", "");
967    }
968
969    public static String ph1_coord_actionId_echo() {
970        return echoUnResolved("actionId", "");
971    }
972
973    public static String ph1_coord_name_echo() {
974        return echoUnResolved("name", "");
975    }
976
977    // The following echo functions are not used in any phases yet
978    // They are here for future purpose.
979    public static String coord_minutes_echo(String n) {
980        return echoUnResolved("minutes", n);
981    }
982
983    public static String coord_hours_echo(String n) {
984        return echoUnResolved("hours", n);
985    }
986
987    public static String coord_days_echo(String n) {
988        return echoUnResolved("days", n);
989    }
990
991    public static String coord_endOfDay_echo(String n) {
992        return echoUnResolved("endOfDay", n);
993    }
994
995    public static String coord_months_echo(String n) {
996        return echoUnResolved("months", n);
997    }
998
999    public static String coord_endOfMonth_echo(String n) {
1000        return echoUnResolved("endOfMonth", n);
1001    }
1002
1003    public static String coord_actualTime_echo() {
1004        return echoUnResolved("actualTime", "");
1005    }
1006
1007    // This echo function will always return "24" for validation only.
1008    // This evaluation ****should not**** replace the original XML
1009    // Create a temporary string and validate the function
1010    // This is **required** for evaluating an expression like
1011    // coord:HoursInDay(0) + 3
1012    // actual evaluation will happen in phase 2 or phase 3.
1013    public static String ph1_coord_hoursInDay_echo(String n) {
1014        return "24";
1015        // return echoUnResolved("hoursInDay", n);
1016    }
1017
1018    // This echo function will always return "30" for validation only.
1019    // This evaluation ****should not**** replace the original XML
1020    // Create a temporary string and validate the function
1021    // This is **required** for evaluating an expression like
1022    // coord:daysInMonth(0) + 3
1023    // actual evaluation will happen in phase 2 or phase 3.
1024    public static String ph1_coord_daysInMonth_echo(String n) {
1025        // return echoUnResolved("daysInMonth", n);
1026        return "30";
1027    }
1028
1029    // This echo function will always return "3" for validation only.
1030    // This evaluation ****should not**** replace the original XML
1031    // Create a temporary string and validate the function
1032    // This is **required** for evaluating an expression like coord:tzOffset + 2
1033    // actual evaluation will happen in phase 2 or phase 3.
1034    public static String ph1_coord_tzOffset_echo() {
1035        // return echoUnResolved("tzOffset", "");
1036        return "3";
1037    }
1038
1039    // Local methods
1040    /**
1041     * @param n
1042     * @return n-th instance Date-Time from current instance for data-set <p> return empty string ("") if the
1043     *         Action_Creation_time or the n-th instance <p> is earlier than the Initial_Instance of dataset.
1044     * @throws Exception
1045     */
1046    private static String coord_current_sync(int n) throws Exception {
1047        return coord_currentRange_sync(n, n);
1048    }
1049
1050    private static String coord_currentRange_sync(int start, int end) throws Exception {
1051        final XLog LOG = XLog.getLog(CoordELFunctions.class);
1052        int datasetFrequency = getDSFrequency();// in minutes
1053        TimeUnit dsTimeUnit = getDSTimeUnit();
1054        int[] instCount = new int[1];// used as pass by ref
1055        Calendar nominalInstanceCal = getCurrentInstance(getActionCreationtime(), instCount);
1056        if (nominalInstanceCal == null) {
1057            LOG.warn("If the initial instance of the dataset is later than the nominal time, an empty string is"
1058                    + " returned. This means that no data is available at the current-instance specified by the user"
1059                    + " and the user could try modifying his initial-instance to an earlier time.");
1060            return "";
1061        } else {
1062            Calendar initInstance = getInitialInstanceCal();
1063            // Add in the reverse order - newest instance first.
1064            nominalInstanceCal = (Calendar) initInstance.clone();
1065            nominalInstanceCal.add(dsTimeUnit.getCalendarUnit(), (instCount[0] + start) * datasetFrequency);
1066            List<String> instances = new ArrayList<String>();
1067            for (int i = start; i <= end; i++) {
1068                if (nominalInstanceCal.compareTo(initInstance) < 0) {
1069                    LOG.warn("If the initial instance of the dataset is later than the current-instance specified,"
1070                            + " such as coord:current({0}) in this case, an empty string is returned. This means that"
1071                            + " no data is available at the current-instance specified by the user and the user could"
1072                            + " try modifying his initial-instance to an earlier time.", start);
1073                }
1074                else {
1075                    instances.add(DateUtils.formatDateOozieTZ(nominalInstanceCal));
1076                }
1077                nominalInstanceCal.add(dsTimeUnit.getCalendarUnit(), datasetFrequency);
1078            }
1079            instances = Lists.reverse(instances);
1080            return StringUtils.join(instances, CoordELFunctions.INSTANCE_SEPARATOR);
1081        }
1082    }
1083
1084    /**
1085     *
1086     * @param n offset amount (integer)
1087     * @param timeUnit TimeUnit for offset n ("MINUTE", "HOUR", "DAY", "MONTH", "YEAR")
1088     * @return the offset time from the effective nominal time <p> return empty string ("") if the Action_Creation_time or the
1089     *         offset instance <p> is earlier than the Initial_Instance of dataset.
1090     * @throws Exception
1091     */
1092    private static String coord_offset_sync(int n, String timeUnit) throws Exception {
1093        Calendar rawCal = resolveOffsetRawTime(n, TimeUnit.valueOf(timeUnit), null);
1094        if (rawCal == null) {
1095            // warning already logged by resolveOffsetRawTime()
1096            return "";
1097        }
1098
1099        int freq = getDSFrequency();
1100        TimeUnit freqUnit = getDSTimeUnit();
1101        int freqCount = 0;
1102        // We're going to manually turn back/forward cal by decrements/increments of freq and then check that it gives the same
1103        // time as rawCal; this is to check that the offset time resolves to a frequency offset of the effective nominal time
1104        // In other words, that there exists an integer x, such that coord:offset(n, timeUnit) == coord:current(x) is true
1105        // If not, then we'll "rewind" rawCal to the latest instance earlier than rawCal and use that.
1106        Calendar cal = getInitialInstanceCal();
1107        if (rawCal.before(cal)) {
1108            while (cal.after(rawCal)) {
1109                cal.add(freqUnit.getCalendarUnit(), -freq);
1110                freqCount--;
1111            }
1112        }
1113        else if (rawCal.after(cal)) {
1114            while (cal.before(rawCal)) {
1115                cal.add(freqUnit.getCalendarUnit(), freq);
1116                freqCount++;
1117            }
1118        }
1119        if (cal.before(rawCal)) {
1120            rawCal = cal;
1121        }
1122        else if (cal.after(rawCal)) {
1123            cal.add(freqUnit.getCalendarUnit(), -freq);
1124            rawCal = cal;
1125            freqCount--;
1126        }
1127        String rawCalStr = DateUtils.formatDateOozieTZ(rawCal);
1128
1129        Calendar nominalInstanceCal = getInitialInstanceCal();
1130        nominalInstanceCal.add(freqUnit.getCalendarUnit(), freq * freqCount);
1131        if (nominalInstanceCal.getTime().compareTo(getInitialInstance()) < 0) {
1132            XLog.getLog(CoordELFunctions.class).warn("If the initial instance of the dataset is later than the offset instance"
1133                    + " specified, such as coord:offset({0}, {1}) in this case, an empty string is returned. This means that no"
1134                    + " data is available at the offset instance specified by the user and the user could try modifying his"
1135                    + " initial-instance to an earlier time.", n, timeUnit);
1136            return "";
1137        }
1138        String nominalCalStr = DateUtils.formatDateOozieTZ(nominalInstanceCal);
1139
1140        if (!rawCalStr.equals(nominalCalStr)) {
1141            throw new RuntimeException("Shouldn't happen");
1142        }
1143        return rawCalStr;
1144    }
1145
1146    /**
1147     * @param offset
1148     * @return n-th available latest instance Date-Time for SYNC data-set
1149     * @throws Exception
1150     */
1151    private static String coord_latest_sync(int offset) throws Exception {
1152        return coord_latestRange_sync(offset, offset);
1153    }
1154
1155    private static String coord_latestRange_sync(int startOffset, int endOffset) throws Exception {
1156        final XLog LOG = XLog.getLog(CoordELFunctions.class);
1157        final Thread currentThread = Thread.currentThread();
1158        ELEvaluator eval = ELEvaluator.getCurrent();
1159        String retVal = "";
1160        int datasetFrequency = (int) getDSFrequency();// in minutes
1161        TimeUnit dsTimeUnit = getDSTimeUnit();
1162        int[] instCount = new int[1];
1163        boolean useCurrentTime = Services.get().getConf().getBoolean(LATEST_EL_USE_CURRENT_TIME, false);
1164        Calendar nominalInstanceCal;
1165        if (useCurrentTime) {
1166            nominalInstanceCal = getCurrentInstance(new Date(), instCount);
1167        }
1168        else {
1169            nominalInstanceCal = getCurrentInstance(getActualTime(), instCount);
1170        }
1171        StringBuilder resolvedInstances = new StringBuilder();
1172        StringBuilder resolvedURIPaths = new StringBuilder();
1173        if (nominalInstanceCal != null) {
1174            Calendar initInstance = getInitialInstanceCal();
1175            SyncCoordDataset ds = (SyncCoordDataset) eval.getVariable(DATASET);
1176            if (ds == null) {
1177                throw new RuntimeException("Associated Dataset should be defined with key " + DATASET);
1178            }
1179            String uriTemplate = ds.getUriTemplate();
1180            Configuration conf = (Configuration) eval.getVariable(CONFIGURATION);
1181            if (conf == null) {
1182                throw new RuntimeException("Associated Configuration should be defined with key " + CONFIGURATION);
1183            }
1184            int available = 0;
1185            boolean resolved = false;
1186            String user = ParamChecker
1187                    .notEmpty((String) eval.getVariable(OozieClient.USER_NAME), OozieClient.USER_NAME);
1188            String doneFlag = ds.getDoneFlag();
1189            URIHandlerService uriService = Services.get().get(URIHandlerService.class);
1190            URIHandler uriHandler = null;
1191            Context uriContext = null;
1192            try {
1193                while (nominalInstanceCal.compareTo(initInstance) >= 0 && !currentThread.isInterrupted()) {
1194                    ELEvaluator uriEval = getUriEvaluator(nominalInstanceCal);
1195                    String uriPath = uriEval.evaluate(uriTemplate, String.class);
1196                    if (uriHandler == null) {
1197                        URI uri = new URI(uriPath);
1198                        uriHandler = uriService.getURIHandler(uri);
1199                        uriContext = uriHandler.getContext(uri, conf, user, true);
1200                    }
1201                    String uriWithDoneFlag = uriHandler.getURIWithDoneFlag(uriPath, doneFlag);
1202                    if (uriHandler.exists(new URI(uriWithDoneFlag), uriContext)) {
1203                        XLog.getLog(CoordELFunctions.class)
1204                        .debug("Found latest(" + available + "): " + uriWithDoneFlag);
1205                        if (available == startOffset) {
1206                            LOG.debug("Matched latest(" + available + "): " + uriWithDoneFlag);
1207                            resolved = true;
1208                            resolvedInstances.append(DateUtils.formatDateOozieTZ(nominalInstanceCal));
1209                            resolvedURIPaths.append(uriPath);
1210                            retVal = resolvedInstances.toString();
1211                            eval.setVariable(CoordELConstants.RESOLVED_PATH, resolvedURIPaths.toString());
1212
1213                            break;
1214                        }
1215                        else if (available <= endOffset) {
1216                            LOG.debug("Matched latest(" + available + "): " + uriWithDoneFlag);
1217                            resolvedInstances.append(DateUtils.formatDateOozieTZ(nominalInstanceCal)).append(
1218                                    INSTANCE_SEPARATOR);
1219                            resolvedURIPaths.append(uriPath).append(INSTANCE_SEPARATOR);
1220                        }
1221
1222                        available--;
1223                    }
1224                    // nominalInstanceCal.add(dsTimeUnit.getCalendarUnit(), -datasetFrequency);
1225                    nominalInstanceCal = (Calendar) initInstance.clone();
1226                    instCount[0]--;
1227                    nominalInstanceCal.add(dsTimeUnit.getCalendarUnit(), instCount[0] * datasetFrequency);
1228                    // DateUtils.moveToEnd(nominalInstanceCal, getDSEndOfFlag());
1229                }
1230                if (!StringUtils.isEmpty(resolvedURIPaths.toString()) && eval.getVariable(CoordELConstants.RESOLVED_PATH) == null) {
1231                    eval.setVariable(CoordELConstants.RESOLVED_PATH, resolvedURIPaths.toString());
1232                }
1233            }
1234            finally {
1235                if (uriContext != null) {
1236                    uriContext.destroy();
1237                }
1238            }
1239            if (!resolved) {
1240                // return unchanged latest function with variable 'is_resolved'
1241                // to 'false'
1242                eval.setVariable(CoordELConstants.IS_RESOLVED, Boolean.FALSE);
1243                if (startOffset == endOffset) {
1244                    retVal = "${coord:latest(" + startOffset + ")}";
1245                }
1246                else {
1247                    retVal = "${coord:latestRange(" + startOffset + "," + endOffset + ")}";
1248                }
1249            }
1250            else {
1251                eval.setVariable(CoordELConstants.IS_RESOLVED, Boolean.TRUE);
1252            }
1253        }
1254        else {// No feasible nominal time
1255            eval.setVariable(CoordELConstants.IS_RESOLVED, Boolean.FALSE);
1256        }
1257        return retVal;
1258    }
1259
1260    /**
1261     * @param tm
1262     * @return a new Evaluator to be used for URI-template evaluation
1263     */
1264    private static ELEvaluator getUriEvaluator(Calendar tm) {
1265        tm.setTimeZone(DateUtils.getOozieProcessingTimeZone());
1266        ELEvaluator retEval = new ELEvaluator();
1267        retEval.setVariable("YEAR", tm.get(Calendar.YEAR));
1268        retEval.setVariable("MONTH", (tm.get(Calendar.MONTH) + 1) < 10 ? "0" + (tm.get(Calendar.MONTH) + 1) : (tm
1269                .get(Calendar.MONTH) + 1));
1270        retEval.setVariable("DAY", tm.get(Calendar.DAY_OF_MONTH) < 10 ? "0" + tm.get(Calendar.DAY_OF_MONTH) : tm
1271                .get(Calendar.DAY_OF_MONTH));
1272        retEval.setVariable("HOUR", tm.get(Calendar.HOUR_OF_DAY) < 10 ? "0" + tm.get(Calendar.HOUR_OF_DAY) : tm
1273                .get(Calendar.HOUR_OF_DAY));
1274        retEval.setVariable("MINUTE", tm.get(Calendar.MINUTE) < 10 ? "0" + tm.get(Calendar.MINUTE) : tm
1275                .get(Calendar.MINUTE));
1276        return retEval;
1277    }
1278
1279    /**
1280     * @return whether a data set is SYNCH or ASYNC
1281     */
1282    private static boolean isSyncDataSet() {
1283        ELEvaluator eval = ELEvaluator.getCurrent();
1284        SyncCoordDataset ds = (SyncCoordDataset) eval.getVariable(DATASET);
1285        if (ds == null) {
1286            throw new RuntimeException("Associated Dataset should be defined with key " + DATASET);
1287        }
1288        return ds.getType().equalsIgnoreCase("SYNC");
1289    }
1290
1291    /**
1292     * Check whether a function should be resolved.
1293     *
1294     * @param functionName
1295     * @param n
1296     * @return null if the functionName needs to be resolved otherwise return the calling function unresolved.
1297     */
1298    private static String checkIfResolved(String functionName, String n) {
1299        ELEvaluator eval = ELEvaluator.getCurrent();
1300        String replace = (String) eval.getVariable("resolve_" + functionName);
1301        if (replace == null || (replace != null && replace.equalsIgnoreCase("false"))) { // Don't
1302            // resolve
1303            // return "${coord:" + functionName + "(" + n +")}"; //Unresolved
1304            eval.setVariable(".wrap", "true");
1305            return "coord:" + functionName + "(" + n + ")"; // Unresolved
1306        }
1307        return null; // Resolved it
1308    }
1309
1310    private static String echoUnResolved(String functionName, String n) {
1311        return echoUnResolvedPre(functionName, n, "coord:");
1312    }
1313
1314    private static String echoUnResolvedPre(String functionName, String n, String prefix) {
1315        ELEvaluator eval = ELEvaluator.getCurrent();
1316        eval.setVariable(".wrap", "true");
1317        return prefix + functionName + "(" + n + ")"; // Unresolved
1318    }
1319
1320    /**
1321     * @return the initial instance of a DataSet in DATE
1322     */
1323    private static Date getInitialInstance() {
1324        ELEvaluator eval = ELEvaluator.getCurrent();
1325        return getInitialInstance(eval);
1326    }
1327
1328    /**
1329     * @return the initial instance of a DataSet in DATE
1330     */
1331    private static Date getInitialInstance(ELEvaluator eval) {
1332        return getInitialInstanceCal(eval).getTime();
1333        // return ds.getInitInstance();
1334    }
1335
1336    /**
1337     * @return the initial instance of a DataSet in Calendar
1338     */
1339    private static Calendar getInitialInstanceCal() {
1340        ELEvaluator eval = ELEvaluator.getCurrent();
1341        return getInitialInstanceCal(eval);
1342    }
1343
1344    /**
1345     * @return the initial instance of a DataSet in Calendar
1346     */
1347    private static Calendar getInitialInstanceCal(ELEvaluator eval) {
1348        SyncCoordDataset ds = (SyncCoordDataset) eval.getVariable(DATASET);
1349        if (ds == null) {
1350            throw new RuntimeException("Associated Dataset should be defined with key " + DATASET);
1351        }
1352        Calendar effInitTS = new GregorianCalendar(ds.getTimeZone());
1353        effInitTS.setTime(ds.getInitInstance());
1354        // To adjust EOD/EOM
1355        DateUtils.moveToEnd(effInitTS, getDSEndOfFlag(eval));
1356        return effInitTS;
1357        // return ds.getInitInstance();
1358    }
1359
1360    /**
1361     * @return Nominal or action creation Time when all the dependencies of an application instance are met.
1362     */
1363    private static Date getActionCreationtime() {
1364        ELEvaluator eval = ELEvaluator.getCurrent();
1365        return getActionCreationtime(eval);
1366    }
1367
1368    /**
1369     * @return Nominal or action creation Time when all the dependencies of an application instance are met.
1370     */
1371    private static Date getActionCreationtime(ELEvaluator eval) {
1372        SyncCoordAction coordAction = (SyncCoordAction) eval.getVariable(COORD_ACTION);
1373        if (coordAction == null) {
1374            throw new RuntimeException("Associated Application instance should be defined with key " + COORD_ACTION);
1375        }
1376        return coordAction.getNominalTime();
1377    }
1378
1379    /**
1380     * @return Actual Time when all the dependencies of an application instance are met.
1381     */
1382    private static Date getActualTime() {
1383        ELEvaluator eval = ELEvaluator.getCurrent();
1384        SyncCoordAction coordAction = (SyncCoordAction) eval.getVariable(COORD_ACTION);
1385        if (coordAction == null) {
1386            throw new RuntimeException("Associated Application instance should be defined with key " + COORD_ACTION);
1387        }
1388        return coordAction.getActualTime();
1389    }
1390
1391    /**
1392     * @return TimeZone for the application or job.
1393     */
1394    private static TimeZone getJobTZ() {
1395        ELEvaluator eval = ELEvaluator.getCurrent();
1396        SyncCoordAction coordAction = (SyncCoordAction) eval.getVariable(COORD_ACTION);
1397        if (coordAction == null) {
1398            throw new RuntimeException("Associated Application instance should be defined with key " + COORD_ACTION);
1399        }
1400        return coordAction.getTimeZone();
1401    }
1402
1403    /**
1404     * Find the current instance based on effectiveTime (i.e Action_Creation_Time or Action_Start_Time)
1405     *
1406     * @return current instance i.e. current(0) returns null if effectiveTime is earlier than Initial Instance time of
1407     *         the dataset.
1408     */
1409    public static Calendar getCurrentInstance(Date effectiveTime, int instanceCount[]) {
1410        ELEvaluator eval = ELEvaluator.getCurrent();
1411        return getCurrentInstance(effectiveTime, instanceCount, eval);
1412    }
1413
1414    /**
1415     * Find the current instance based on effectiveTime (i.e Action_Creation_Time or Action_Start_Time)
1416     *
1417     * @return current instance i.e. current(0) returns null if effectiveTime is earlier than Initial Instance time of
1418     *         the dataset.
1419     */
1420    private static Calendar getCurrentInstance(Date effectiveTime, int instanceCount[], ELEvaluator eval) {
1421        Date datasetInitialInstance = getInitialInstance(eval);
1422        TimeUnit dsTimeUnit = getDSTimeUnit(eval);
1423        TimeZone dsTZ = getDatasetTZ(eval);
1424        int dsFreq = getDSFrequency(eval);
1425        // Convert Date to Calendar for corresponding TZ
1426        Calendar current = Calendar.getInstance(dsTZ);
1427        current.setTime(datasetInitialInstance);
1428
1429        Calendar calEffectiveTime = new GregorianCalendar(dsTZ);
1430        calEffectiveTime.setTime(effectiveTime);
1431        if (instanceCount == null) {    // caller doesn't care about this value
1432            instanceCount = new int[1];
1433        }
1434        instanceCount[0] = 0;
1435        if (current.compareTo(calEffectiveTime) > 0) {
1436            return null;
1437        }
1438
1439        switch(dsTimeUnit) {
1440            case MINUTE:
1441                instanceCount[0] = (int) ((effectiveTime.getTime() - datasetInitialInstance.getTime()) / MINUTE_MSEC);
1442                break;
1443            case HOUR:
1444                instanceCount[0] = (int) ((effectiveTime.getTime() - datasetInitialInstance.getTime()) / HOUR_MSEC);
1445                break;
1446            case DAY:
1447            case END_OF_DAY:
1448                instanceCount[0] = (int) ((effectiveTime.getTime() - datasetInitialInstance.getTime()) / DAY_MSEC);
1449                break;
1450            case WEEK:
1451            case END_OF_WEEK:
1452                instanceCount[0] = (int) ((effectiveTime.getTime() - datasetInitialInstance.getTime()) / WEEK_MSEC);
1453                break;
1454            case MONTH:
1455            case END_OF_MONTH:
1456                int diffYear = calEffectiveTime.get(Calendar.YEAR) - current.get(Calendar.YEAR);
1457                instanceCount[0] = diffYear * 12 + calEffectiveTime.get(Calendar.MONTH) - current.get(Calendar.MONTH);
1458                break;
1459            case YEAR:
1460                instanceCount[0] = calEffectiveTime.get(Calendar.YEAR) - current.get(Calendar.YEAR);
1461                break;
1462            default:
1463                throw new IllegalArgumentException("Unhandled dataset time unit " + dsTimeUnit);
1464        }
1465
1466        if (instanceCount[0] > 2) {
1467            instanceCount[0] = (instanceCount[0] / dsFreq);
1468            current.add(dsTimeUnit.getCalendarUnit(), instanceCount[0] * dsFreq);
1469        } else {
1470            instanceCount[0] = 0;
1471        }
1472        while (!current.getTime().after(effectiveTime)) {
1473            current.add(dsTimeUnit.getCalendarUnit(), dsFreq);
1474            instanceCount[0]++;
1475        }
1476        current.add(dsTimeUnit.getCalendarUnit(), -dsFreq);
1477        instanceCount[0]--;
1478        return current;
1479    }
1480
1481    /**
1482     * Find the current instance based on effectiveTime (i.e Action_Creation_Time or Action_Start_Time)
1483     *
1484     * @return current instance i.e. current(0) returns null if effectiveTime is earlier than Initial Instance time of
1485     *         the dataset.
1486     */
1487    private static Calendar getCurrentInstance_old(Date effectiveTime, int instanceCount[], ELEvaluator eval) {
1488        Date datasetInitialInstance = getInitialInstance(eval);
1489        TimeUnit dsTimeUnit = getDSTimeUnit(eval);
1490        TimeZone dsTZ = getDatasetTZ(eval);
1491        int dsFreq = getDSFrequency(eval);
1492        // Convert Date to Calendar for corresponding TZ
1493        Calendar current = Calendar.getInstance();
1494        current.setTime(datasetInitialInstance);
1495        current.setTimeZone(dsTZ);
1496
1497        Calendar calEffectiveTime = Calendar.getInstance();
1498        calEffectiveTime.setTime(effectiveTime);
1499        calEffectiveTime.setTimeZone(dsTZ);
1500        if (instanceCount == null) {    // caller doesn't care about this value
1501            instanceCount = new int[1];
1502        }
1503        instanceCount[0] = 0;
1504        if (current.compareTo(calEffectiveTime) > 0) {
1505            return null;
1506        }
1507        Calendar origCurrent = (Calendar) current.clone();
1508        while (current.compareTo(calEffectiveTime) <= 0) {
1509            current = (Calendar) origCurrent.clone();
1510            instanceCount[0]++;
1511            current.add(dsTimeUnit.getCalendarUnit(), instanceCount[0] * dsFreq);
1512        }
1513        instanceCount[0]--;
1514
1515        current = (Calendar) origCurrent.clone();
1516        current.add(dsTimeUnit.getCalendarUnit(), instanceCount[0] * dsFreq);
1517        return current;
1518    }
1519
1520    public static Calendar getEffectiveNominalTime() {
1521        Date datasetInitialInstance = getInitialInstance();
1522        TimeZone dsTZ = getDatasetTZ();
1523        // Convert Date to Calendar for corresponding TZ
1524        Calendar current = Calendar.getInstance();
1525        current.setTime(datasetInitialInstance);
1526        current.setTimeZone(dsTZ);
1527
1528        Calendar calEffectiveTime = Calendar.getInstance();
1529        calEffectiveTime.setTime(getActionCreationtime());
1530        calEffectiveTime.setTimeZone(dsTZ);
1531        if (current.compareTo(calEffectiveTime) > 0) {
1532            // Nominal Time < initial Instance
1533            // TODO: getClass() call doesn't work from static method.
1534            // XLog.getLog("CoordELFunction.class").warn("ACTION CREATED BEFORE INITIAL INSTACE "+
1535            // current.getTime());
1536            return null;
1537        }
1538        return calEffectiveTime;
1539    }
1540
1541    /**
1542     * @return dataset frequency in minutes
1543     */
1544    private static int getDSFrequency() {
1545        ELEvaluator eval = ELEvaluator.getCurrent();
1546        return getDSFrequency(eval);
1547    }
1548
1549    /**
1550     * @return dataset frequency in minutes
1551     */
1552    private static int getDSFrequency(ELEvaluator eval) {
1553        SyncCoordDataset ds = (SyncCoordDataset) eval.getVariable(DATASET);
1554        if (ds == null) {
1555            throw new RuntimeException("Associated Dataset should be defined with key " + DATASET);
1556        }
1557        return ds.getFrequency();
1558    }
1559
1560    /**
1561     * @return dataset TimeUnit
1562     */
1563    private static TimeUnit getDSTimeUnit() {
1564        ELEvaluator eval = ELEvaluator.getCurrent();
1565        return getDSTimeUnit(eval);
1566    }
1567
1568    /**
1569     * @return dataset TimeUnit
1570     */
1571    public static TimeUnit getDSTimeUnit(ELEvaluator eval) {
1572        SyncCoordDataset ds = (SyncCoordDataset) eval.getVariable(DATASET);
1573        if (ds == null) {
1574            throw new RuntimeException("Associated Dataset should be defined with key " + DATASET);
1575        }
1576        return ds.getTimeUnit();
1577    }
1578
1579    /**
1580     * @return dataset TimeZone
1581     */
1582    public static TimeZone getDatasetTZ() {
1583        ELEvaluator eval = ELEvaluator.getCurrent();
1584        return getDatasetTZ(eval);
1585    }
1586
1587    /**
1588     * @return dataset TimeZone
1589     */
1590    public static TimeZone getDatasetTZ(ELEvaluator eval) {
1591        SyncCoordDataset ds = (SyncCoordDataset) eval.getVariable(DATASET);
1592        if (ds == null) {
1593            throw new RuntimeException("Associated Dataset should be defined with key " + DATASET);
1594        }
1595        return ds.getTimeZone();
1596    }
1597
1598    /**
1599     * @return dataset TimeUnit
1600     */
1601    private static TimeUnit getDSEndOfFlag() {
1602        ELEvaluator eval = ELEvaluator.getCurrent();
1603        return getDSEndOfFlag(eval);
1604    }
1605
1606    /**
1607     * @return dataset TimeUnit
1608     */
1609    private static TimeUnit getDSEndOfFlag(ELEvaluator eval) {
1610        SyncCoordDataset ds = (SyncCoordDataset) eval.getVariable(DATASET);
1611        if (ds == null) {
1612            throw new RuntimeException("Associated Dataset should be defined with key " + DATASET);
1613        }
1614        return ds.getEndOfDuration();// == null ? "": ds.getEndOfDuration();
1615    }
1616
1617    /**
1618     * Return a job configuration property for the coordinator.
1619     *
1620     * @param property property name.
1621     * @return the value of the property, <code>null</code> if the property is undefined.
1622     */
1623    public static String coord_conf(String property) {
1624        ELEvaluator eval = ELEvaluator.getCurrent();
1625        return (String) eval.getVariable(property);
1626    }
1627
1628    /**
1629     * Return the user that submitted the coordinator job.
1630     *
1631     * @return the user that submitted the coordinator job.
1632     */
1633    public static String coord_user() {
1634        ELEvaluator eval = ELEvaluator.getCurrent();
1635        return (String) eval.getVariable(OozieClient.USER_NAME);
1636    }
1637
1638    /**
1639     * Takes two offset times and returns a list of multiples of the frequency offset from the effective nominal time that occur
1640     * between them.  The caller should make sure that startCal is earlier than endCal.
1641     * <p>
1642     * As a simple example, assume its the same day: startCal is 1:00, endCal is 2:00, frequency is 20min, and effective nominal
1643     * time is 1:20 -- then this method would return a list containing: -20, 0, 20, 40, 60
1644     *
1645     * @param startCal The earlier offset time
1646     * @param endCal The later offset time
1647     * @param eval The ELEvaluator to use; cannot be null
1648     * @return A list of multiple of the frequency offset from the effective nominal time that occur between the startCal and endCal
1649     */
1650    public static List<Integer> expandOffsetTimes(Calendar startCal, Calendar endCal, ELEvaluator eval) {
1651        List<Integer> expandedFreqs = new ArrayList<Integer>();
1652        // Use eval because the "current" eval isn't set
1653        int freq = getDSFrequency(eval);
1654        TimeUnit freqUnit = getDSTimeUnit(eval);
1655        Calendar cal = getCurrentInstance(getActionCreationtime(eval), null, eval);
1656        int totalFreq = 0;
1657        if (startCal.before(cal)) {
1658            while (cal.after(startCal)) {
1659                cal.add(freqUnit.getCalendarUnit(), -freq);
1660                totalFreq += -freq;
1661            }
1662            if (cal.before(startCal)) {
1663                cal.add(freqUnit.getCalendarUnit(), freq);
1664                totalFreq += freq;
1665            }
1666        }
1667        else if (startCal.after(cal)) {
1668            while (cal.before(startCal)) {
1669                cal.add(freqUnit.getCalendarUnit(), freq);
1670                totalFreq += freq;
1671            }
1672        }
1673        // At this point, cal is the smallest multiple of the dataset frequency that is >= to the startCal and offset from the
1674        // effective nominal time.  Now we can find all of the instances that occur between startCal and endCal, inclusive.
1675        while (cal.before(endCal) || cal.equals(endCal)) {
1676            expandedFreqs.add(totalFreq);
1677            cal.add(freqUnit.getCalendarUnit(), freq);
1678            totalFreq += freq;
1679        }
1680        return expandedFreqs;
1681    }
1682
1683    /**
1684     * Resolve the offset time from the effective nominal time
1685     *
1686     * @param n offset amount (integer)
1687     * @param timeUnit TimeUnit for offset n ("MINUTE", "HOUR", "DAY", "MONTH", "YEAR")
1688     * @param eval The ELEvaluator to use; or null to use the "current" eval
1689     * @return A Calendar of the offset time
1690     */
1691    public static Calendar resolveOffsetRawTime(int n, TimeUnit timeUnit, ELEvaluator eval) {
1692        // Use eval if given (for when the "current" eval isn't set)
1693        Calendar cal;
1694        if (eval == null) {
1695            cal = getCurrentInstance(getActionCreationtime(), null);
1696        }
1697        else {
1698            cal = getCurrentInstance(getActionCreationtime(eval), null, eval);
1699        }
1700        if (cal == null) {
1701            XLog.getLog(CoordELFunctions.class).warn("If the initial instance of the dataset is later than the nominal time, an"
1702                    + " empty string is returned. This means that no data is available at the offset instance specified by the user"
1703                    + " and the user could try modifying his or her initial-instance to an earlier time.");
1704            return null;
1705        }
1706        cal.add(timeUnit.getCalendarUnit(), n);
1707        return cal;
1708    }
1709}