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;
020
021import java.io.IOException;
022import java.io.Writer;
023import java.sql.Timestamp;
024import java.text.ParseException;
025import java.util.ArrayList;
026import java.util.Collections;
027import java.util.Comparator;
028import java.util.Date;
029import java.util.HashMap;
030import java.util.HashSet;
031import java.util.Iterator;
032import java.util.LinkedHashSet;
033import java.util.List;
034import java.util.Map;
035import java.util.Set;
036import java.util.StringTokenizer;
037
038import org.apache.commons.lang.StringUtils;
039import org.apache.hadoop.conf.Configuration;
040import org.apache.oozie.client.CoordinatorAction;
041import org.apache.oozie.client.CoordinatorJob;
042import org.apache.oozie.client.OozieClient;
043import org.apache.oozie.client.WorkflowJob;
044import org.apache.oozie.client.rest.RestConstants;
045import org.apache.oozie.command.CommandException;
046import org.apache.oozie.command.OperationType;
047import org.apache.oozie.command.coord.BulkCoordXCommand;
048import org.apache.oozie.command.coord.CoordActionInfoXCommand;
049import org.apache.oozie.command.coord.CoordActionsIgnoreXCommand;
050import org.apache.oozie.command.coord.CoordActionsKillXCommand;
051import org.apache.oozie.command.coord.CoordChangeXCommand;
052import org.apache.oozie.command.coord.CoordJobXCommand;
053import org.apache.oozie.command.coord.CoordJobsXCommand;
054import org.apache.oozie.command.coord.CoordKillXCommand;
055import org.apache.oozie.command.coord.CoordRerunXCommand;
056import org.apache.oozie.command.coord.CoordResumeXCommand;
057import org.apache.oozie.command.coord.CoordSubmitXCommand;
058import org.apache.oozie.command.coord.CoordSuspendXCommand;
059import org.apache.oozie.command.coord.CoordUpdateXCommand;
060import org.apache.oozie.executor.jpa.CoordActionQueryExecutor;
061import org.apache.oozie.executor.jpa.CoordJobQueryExecutor;
062import org.apache.oozie.executor.jpa.JPAExecutorException;
063import org.apache.oozie.executor.jpa.WorkflowJobQueryExecutor;
064import org.apache.oozie.executor.jpa.WorkflowJobQueryExecutor.WorkflowJobQuery;
065import org.apache.oozie.service.DagXLogInfoService;
066import org.apache.oozie.service.Services;
067import org.apache.oozie.service.XLogStreamingService;
068import org.apache.oozie.util.CoordActionsInDateRange;
069import org.apache.oozie.util.DateUtils;
070import org.apache.oozie.util.Pair;
071import org.apache.oozie.util.ParamChecker;
072import org.apache.oozie.util.XLog;
073import org.apache.oozie.util.XLogFilter;
074import org.apache.oozie.util.XLogUserFilterParam;
075
076import com.google.common.annotations.VisibleForTesting;
077
078public class CoordinatorEngine extends BaseEngine {
079    private static final XLog LOG = XLog.getLog(CoordinatorEngine.class);
080    public final static String COORD_ACTIONS_LOG_MAX_COUNT = "oozie.coord.actions.log.max.count";
081    private final static int COORD_ACTIONS_LOG_MAX_COUNT_DEFAULT = 50;
082    private final int maxNumActionsForLog;
083
084    public enum FILTER_COMPARATORS {
085        //This ordering is important, dont change this
086        GREATER_EQUAL(">="), GREATER(">"), LESSTHAN_EQUAL("<="), LESSTHAN("<"), NOT_EQUALS("!="), EQUALS("=");
087
088        private final String sign;
089
090        FILTER_COMPARATORS(String sign) {
091            this.sign = sign;
092        }
093
094        public String getSign() {
095            return sign;
096        }
097    }
098
099    public static final String[] VALID_JOB_FILTERS = {OozieClient.FILTER_STATUS, OozieClient.FILTER_NOMINAL_TIME};
100
101    /**
102     * Create a system Coordinator engine, with no user and no group.
103     */
104    public CoordinatorEngine() {
105        maxNumActionsForLog = Services.get().getConf()
106                .getInt(COORD_ACTIONS_LOG_MAX_COUNT, COORD_ACTIONS_LOG_MAX_COUNT_DEFAULT);
107    }
108
109    /**
110     * Create a Coordinator engine to perform operations on behave of a user.
111     *
112     * @param user user name.
113     */
114    public CoordinatorEngine(String user) {
115        this();
116        this.user = ParamChecker.notEmpty(user, "user");
117    }
118
119    /*
120     * (non-Javadoc)
121     *
122     * @see org.apache.oozie.BaseEngine#getDefinition(java.lang.String)
123     */
124    @Override
125    public String getDefinition(String jobId) throws BaseEngineException {
126        CoordinatorJobBean job = getCoordJobWithNoActionInfo(jobId);
127        return job.getOrigJobXml();
128    }
129
130    /**
131     * @param jobId
132     * @return CoordinatorJobBean
133     * @throws BaseEngineException
134     */
135    private CoordinatorJobBean getCoordJobWithNoActionInfo(String jobId) throws BaseEngineException {
136        try {
137            return new CoordJobXCommand(jobId).call();
138        }
139        catch (CommandException ex) {
140            throw new BaseEngineException(ex);
141        }
142    }
143
144    /**
145     * @param actionId
146     * @return CoordinatorActionBean
147     * @throws BaseEngineException
148     */
149    public CoordinatorActionBean getCoordAction(String actionId) throws BaseEngineException {
150        try {
151            return new CoordActionInfoXCommand(actionId).call();
152        }
153        catch (CommandException ex) {
154            throw new BaseEngineException(ex);
155        }
156    }
157
158    /*
159     * (non-Javadoc)
160     *
161     * @see org.apache.oozie.BaseEngine#getCoordJob(java.lang.String)
162     */
163    @Override
164    public CoordinatorJobBean getCoordJob(String jobId) throws BaseEngineException {
165        try {
166            return new CoordJobXCommand(jobId).call();
167        }
168        catch (CommandException ex) {
169            throw new BaseEngineException(ex);
170        }
171    }
172
173    /*
174     * (non-Javadoc)
175     *
176     * @see org.apache.oozie.BaseEngine#getCoordJob(java.lang.String, java.lang.String, int, int)
177     */
178    @Override
179    public CoordinatorJobBean getCoordJob(String jobId, String filter, int offset, int length, boolean desc)
180            throws BaseEngineException {
181        Map<Pair<String, FILTER_COMPARATORS>, List<Object>> filterMap = parseJobFilter(filter);
182        try {
183            return new CoordJobXCommand(jobId, filterMap, offset, length, desc).call();
184        }
185        catch (CommandException ex) {
186            throw new BaseEngineException(ex);
187        }
188    }
189
190    /*
191     * (non-Javadoc)
192     *
193     * @see org.apache.oozie.BaseEngine#getJobIdForExternalId(java.lang.String)
194     */
195    @Override
196    public String getJobIdForExternalId(String externalId) throws CoordinatorEngineException {
197        return null;
198    }
199
200    /*
201     * (non-Javadoc)
202     *
203     * @see org.apache.oozie.BaseEngine#kill(java.lang.String)
204     */
205    @Override
206    public void kill(String jobId) throws CoordinatorEngineException {
207        try {
208            new CoordKillXCommand(jobId).call();
209            LOG.info("User " + user + " killed the Coordinator job " + jobId);
210        }
211        catch (CommandException e) {
212            throw new CoordinatorEngineException(e);
213        }
214    }
215
216    public CoordinatorActionInfo killActions(String jobId, String rangeType, String scope) throws CoordinatorEngineException {
217        try {
218            return new CoordActionsKillXCommand(jobId, rangeType, scope).call();
219        }
220        catch (CommandException e) {
221            throw new CoordinatorEngineException(e);
222        }
223    }
224
225    /* (non-Javadoc)
226     * @see org.apache.oozie.BaseEngine#change(java.lang.String, java.lang.String)
227     */
228    @Override
229    public void change(String jobId, String changeValue) throws CoordinatorEngineException {
230        try {
231            new CoordChangeXCommand(jobId, changeValue).call();
232            LOG.info("User " + user + " changed the Coordinator job [" + jobId + "] to " + changeValue);
233        }
234        catch (CommandException e) {
235            throw new CoordinatorEngineException(e);
236        }
237    }
238
239    public CoordinatorActionInfo ignore(String jobId, String type, String scope) throws CoordinatorEngineException {
240        try {
241            LOG.info("User " + user + " ignore a Coordinator Action (s) [" + scope + "] of the Coordinator Job ["
242                    + jobId + "]");
243            return new CoordActionsIgnoreXCommand(jobId, type, scope).call();
244        }
245        catch (CommandException e) {
246            throw new CoordinatorEngineException(e);
247        }
248    }
249
250    @Override
251    @Deprecated
252    public void reRun(String jobId, Configuration conf) throws BaseEngineException {
253        throw new BaseEngineException(new XException(ErrorCode.E0301, "invalid use of rerun"));
254    }
255
256    /**
257     * Rerun coordinator actions for given rerunType
258     *
259     * @param jobId
260     * @param rerunType
261     * @param scope
262     * @param refresh
263     * @param noCleanup
264     * @throws BaseEngineException
265     */
266    public CoordinatorActionInfo reRun(String jobId, String rerunType, String scope, boolean refresh, boolean noCleanup,
267                                       boolean failed)
268            throws BaseEngineException {
269        try {
270            return new CoordRerunXCommand(jobId, rerunType, scope, refresh,
271                    noCleanup, failed).call();
272        }
273        catch (CommandException ex) {
274            throw new BaseEngineException(ex);
275        }
276    }
277
278    /*
279     * (non-Javadoc)
280     *
281     * @see org.apache.oozie.BaseEngine#resume(java.lang.String)
282     */
283    @Override
284    public void resume(String jobId) throws CoordinatorEngineException {
285        try {
286            new CoordResumeXCommand(jobId).call();
287        }
288        catch (CommandException e) {
289            throw new CoordinatorEngineException(e);
290        }
291    }
292
293    @Override
294    @Deprecated
295    public void start(String jobId) throws BaseEngineException {
296        throw new BaseEngineException(new XException(ErrorCode.E0301, "invalid use of start"));
297    }
298
299    /*
300     * (non-Javadoc)
301     *
302     * @see org.apache.oozie.BaseEngine#streamLog(java.lang.String,
303     * java.io.Writer)
304     */
305    @Override
306    public void streamLog(String jobId, Writer writer, Map<String, String[]> params) throws IOException,
307            BaseEngineException {
308
309        try {
310            XLogFilter filter = new XLogFilter(new XLogUserFilterParam(params));
311            filter.setParameter(DagXLogInfoService.JOB, jobId);
312            Date lastTime = null;
313            CoordinatorJobBean job = getCoordJobWithNoActionInfo(jobId);
314            if (job.isTerminalStatus()) {
315                lastTime = job.getLastModifiedTime();
316            }
317            if (lastTime == null) {
318                lastTime = new Date();
319            }
320            Services.get().get(XLogStreamingService.class)
321                    .streamLog(filter, job.getCreatedTime(), lastTime, writer, params);
322        }
323        catch (Exception e) {
324            throw new IOException(e);
325        }
326    }
327
328    /**
329     * Add list of actions to the filter based on conditions
330     *
331     * @param jobId Job Id
332     * @param logRetrievalScope Value for the retrieval type
333     * @param logRetrievalType Based on which filter criteria the log is retrieved
334     * @param writer writer to stream the log to
335     * @param params additional parameters from the request
336     * @throws IOException
337     * @throws BaseEngineException
338     * @throws CommandException
339     */
340    public void streamLog(String jobId, String logRetrievalScope, String logRetrievalType, Writer writer,
341            Map<String, String[]> params) throws IOException, BaseEngineException, CommandException {
342
343        Date startTime = null;
344        Date endTime = null;
345        XLogFilter filter = new XLogFilter(new XLogUserFilterParam(params));
346
347        filter.setParameter(DagXLogInfoService.JOB, jobId);
348        if (logRetrievalScope != null && logRetrievalType != null) {
349            // if coordinator action logs are to be retrieved based on action id range
350            if (logRetrievalType.equals(RestConstants.JOB_LOG_ACTION)) {
351                // Use set implementation that maintains order or elements to achieve reproducibility:
352                Set<String> actionSet = new LinkedHashSet<String>();
353                String[] list = logRetrievalScope.split(",");
354                for (String s : list) {
355                    s = s.trim();
356                    if (s.contains("-")) {
357                        String[] range = s.split("-");
358                        if (range.length != 2) {
359                            throw new CommandException(ErrorCode.E0302, "format is wrong for action's range '" + s
360                                    + "'");
361                        }
362                        int start;
363                        int end;
364                        try {
365                            start = Integer.parseInt(range[0].trim());
366                        } catch (NumberFormatException ne) {
367                            throw new CommandException(ErrorCode.E0302, "could not parse " + range[0].trim() + "into an integer",
368                                    ne);
369                        }
370                        try {
371                            end = Integer.parseInt(range[1].trim());
372                        } catch (NumberFormatException ne) {
373                            throw new CommandException(ErrorCode.E0302, "could not parse " + range[1].trim() + "into an integer",
374                                    ne);
375                        }
376                        if (start > end) {
377                            throw new CommandException(ErrorCode.E0302, "format is wrong for action's range '" + s + "'");
378                        }
379                        for (int i = start; i <= end; i++) {
380                            actionSet.add(jobId + "@" + i);
381                        }
382                    }
383                    else {
384                        try {
385                            Integer.parseInt(s);
386                        }
387                        catch (NumberFormatException ne) {
388                            throw new CommandException(ErrorCode.E0302, "format is wrong for action id'" + s
389                                    + "'. Integer only.");
390                        }
391                        actionSet.add(jobId + "@" + s);
392                    }
393                }
394
395                if (actionSet.size() >= maxNumActionsForLog) {
396                    throw new CommandException(ErrorCode.E0302,
397                            "Retrieving log of too many coordinator actions. Max count is "
398                                    + maxNumActionsForLog + " actions");
399                }
400                Iterator<String> actionsIterator = actionSet.iterator();
401                StringBuilder orSeparatedActions = new StringBuilder("");
402                boolean orRequired = false;
403                while (actionsIterator.hasNext()) {
404                    if (orRequired) {
405                        orSeparatedActions.append("|");
406                    }
407                    orSeparatedActions.append(actionsIterator.next().toString());
408                    orRequired = true;
409                }
410                if (actionSet.size() > 1 && orRequired) {
411                    orSeparatedActions.insert(0, "(");
412                    orSeparatedActions.append(")");
413                }
414
415                filter.setParameter(DagXLogInfoService.ACTION, orSeparatedActions.toString());
416                if (actionSet != null && actionSet.size() == 1) {
417                    CoordinatorActionBean actionBean = getCoordAction(actionSet.iterator().next());
418                    startTime = actionBean.getCreatedTime();
419                    endTime = actionBean.getStatus().equals(CoordinatorAction.Status.RUNNING) ? new Date() : actionBean
420                            .getLastModifiedTime();
421                    filter.setActionList(true);
422                }
423                else if (actionSet != null && actionSet.size() > 0) {
424                    List<String> tempList = new ArrayList<String>(actionSet);
425                    Collections.sort(tempList, new Comparator<String>() {
426                        public int compare(String a, String b) {
427                            return Integer.valueOf(a.substring(a.lastIndexOf("@") + 1)).compareTo(
428                                    Integer.valueOf(b.substring(b.lastIndexOf("@") + 1)));
429                        }
430                    });
431                    startTime = getCoordAction(tempList.get(0)).getCreatedTime();
432                    endTime = CoordActionsInDateRange.getCoordActionsLastModifiedDate(jobId, tempList.get(0),
433                            tempList.get(tempList.size() - 1));
434                    filter.setActionList(true);
435                }
436            }
437            // if coordinator action logs are to be retrieved based on date range
438            // this block gets the corresponding list of coordinator actions to be used by the log filter
439            if (logRetrievalType.equalsIgnoreCase(RestConstants.JOB_LOG_DATE)) {
440                List<String> coordActionIdList = null;
441                try {
442                    coordActionIdList = CoordActionsInDateRange.getCoordActionIdsFromDates(jobId, logRetrievalScope);
443                }
444                catch (XException xe) {
445                    throw new CommandException(ErrorCode.E0302, "Error in date range for coordinator actions", xe);
446                }
447                if(coordActionIdList.size() >= maxNumActionsForLog) {
448                    throw new CommandException(ErrorCode.E0302,
449                            "Retrieving log of too many coordinator actions. Max count is "
450                                    + maxNumActionsForLog + " actions");
451                }
452                StringBuilder orSeparatedActions = new StringBuilder("");
453                boolean orRequired = false;
454                for (String coordActionId : coordActionIdList) {
455                    if (orRequired) {
456                        orSeparatedActions.append("|");
457                    }
458                    orSeparatedActions.append(coordActionId);
459                    orRequired = true;
460                }
461                if (coordActionIdList.size() > 1 && orRequired) {
462                    orSeparatedActions.insert(0, "(");
463                    orSeparatedActions.append(")");
464                }
465                filter.setParameter(DagXLogInfoService.ACTION, orSeparatedActions.toString());
466                if (coordActionIdList != null && coordActionIdList.size() == 1) {
467                    CoordinatorActionBean actionBean = getCoordAction(coordActionIdList.get(0));
468                    startTime = actionBean.getCreatedTime();
469                    endTime = actionBean.getStatus().equals(CoordinatorAction.Status.RUNNING) ? new Date() : actionBean
470                            .getLastModifiedTime();
471                    filter.setActionList(true);
472                }
473                else if (coordActionIdList != null && coordActionIdList.size() > 0) {
474                    Collections.sort(coordActionIdList, new Comparator<String>() {
475                        public int compare(String a, String b) {
476                            return Integer.valueOf(a.substring(a.lastIndexOf("@") + 1)).compareTo(
477                                    Integer.valueOf(b.substring(b.lastIndexOf("@") + 1)));
478                        }
479                    });
480                    startTime = getCoordAction(coordActionIdList.get(0)).getCreatedTime();
481                    endTime = CoordActionsInDateRange.getCoordActionsLastModifiedDate(jobId, coordActionIdList.get(0),
482                            coordActionIdList.get(coordActionIdList.size() - 1));
483                    filter.setActionList(true);
484                }
485            }
486        }
487        if (startTime == null || endTime == null) {
488            CoordinatorJobBean job = getCoordJobWithNoActionInfo(jobId);
489            if (startTime == null) {
490                startTime = job.getCreatedTime();
491            }
492            if (endTime == null) {
493                if (job.isTerminalStatus()) {
494                    endTime = job.getLastModifiedTime();
495                }
496                if (endTime == null) {
497                    endTime = new Date();
498                }
499            }
500        }
501        Services.get().get(XLogStreamingService.class).streamLog(filter, startTime, endTime, writer, params);
502    }
503
504    /*
505     * (non-Javadoc)
506     *
507     * @see
508     * org.apache.oozie.BaseEngine#submitJob(org.apache.hadoop.conf.Configuration
509     * , boolean)
510     */
511    @Override
512    public String submitJob(Configuration conf, boolean startJob) throws CoordinatorEngineException {
513        try {
514            CoordSubmitXCommand submit = new CoordSubmitXCommand(conf);
515            return submit.call();
516        }
517        catch (CommandException ex) {
518            throw new CoordinatorEngineException(ex);
519        }
520    }
521
522    /*
523     * (non-Javadoc)
524     *
525     * @see
526     * org.apache.oozie.BaseEngine#dryRunSubmit(org.apache.hadoop.conf.Configuration)
527     */
528    @Override
529    public String dryRunSubmit(Configuration conf) throws CoordinatorEngineException {
530        try {
531            CoordSubmitXCommand submit = new CoordSubmitXCommand(true, conf);
532            return submit.call();
533        }
534        catch (CommandException ex) {
535            throw new CoordinatorEngineException(ex);
536        }
537    }
538
539    /*
540     * (non-Javadoc)
541     *
542     * @see org.apache.oozie.BaseEngine#suspend(java.lang.String)
543     */
544    @Override
545    public void suspend(String jobId) throws CoordinatorEngineException {
546        try {
547            new CoordSuspendXCommand(jobId).call();
548        }
549        catch (CommandException e) {
550            throw new CoordinatorEngineException(e);
551        }
552
553    }
554
555    /*
556     * (non-Javadoc)
557     *
558     * @see org.apache.oozie.BaseEngine#getJob(java.lang.String)
559     */
560    @Override
561    public WorkflowJob getJob(String jobId) throws BaseEngineException {
562        throw new BaseEngineException(new XException(ErrorCode.E0301, "cannot get a workflow job from CoordinatorEngine"));
563    }
564
565    /*
566     * (non-Javadoc)
567     *
568     * @see org.apache.oozie.BaseEngine#getJob(java.lang.String, int, int)
569     */
570    @Override
571    public WorkflowJob getJob(String jobId, int start, int length) throws BaseEngineException {
572        throw new BaseEngineException(new XException(ErrorCode.E0301, "cannot get a workflow job from CoordinatorEngine"));
573    }
574
575    private static final Set<String> FILTER_NAMES = new HashSet<String>();
576
577    static {
578        FILTER_NAMES.add(OozieClient.FILTER_USER);
579        FILTER_NAMES.add(OozieClient.FILTER_NAME);
580        FILTER_NAMES.add(OozieClient.FILTER_GROUP);
581        FILTER_NAMES.add(OozieClient.FILTER_STATUS);
582        FILTER_NAMES.add(OozieClient.FILTER_ID);
583        FILTER_NAMES.add(OozieClient.FILTER_FREQUENCY);
584        FILTER_NAMES.add(OozieClient.FILTER_UNIT);
585    }
586
587    /**
588     * @param filter
589     * @param start
590     * @param len
591     * @return CoordinatorJobInfo
592     * @throws CoordinatorEngineException
593     */
594    public CoordinatorJobInfo getCoordJobs(String filter, int start, int len) throws CoordinatorEngineException {
595        Map<String, List<String>> filterList = parseJobsFilter(filter);
596
597        try {
598            return new CoordJobsXCommand(filterList, start, len).call();
599        }
600        catch (CommandException ex) {
601            throw new CoordinatorEngineException(ex);
602        }
603    }
604
605    // Parses the filter string (e.g status=RUNNING;status=WAITING) and returns a list of status values
606    public Map<Pair<String, FILTER_COMPARATORS>, List<Object>> parseJobFilter(String filter) throws
607        CoordinatorEngineException {
608        Map<Pair<String, FILTER_COMPARATORS>, List<Object>> filterMap = new HashMap<Pair<String,
609            FILTER_COMPARATORS>, List<Object>>();
610        if (filter != null) {
611            //split name value pairs
612            StringTokenizer st = new StringTokenizer(filter, ";");
613            while (st.hasMoreTokens()) {
614                String token = st.nextToken().trim();
615                Pair<String, FILTER_COMPARATORS> pair = null;
616                for (FILTER_COMPARATORS comp : FILTER_COMPARATORS.values()) {
617                    if (token.contains(comp.getSign())) {
618                        int index = token.indexOf(comp.getSign());
619                        String key = token.substring(0, index);
620                        String valueStr = token.substring(index + comp.getSign().length());
621                        Object value;
622
623                        if (key.equalsIgnoreCase(OozieClient.FILTER_STATUS)) {
624                            value = valueStr.toUpperCase();
625                            try {
626                                CoordinatorAction.Status.valueOf((String) value);
627                            } catch (IllegalArgumentException ex) {
628                                // Check for incorrect status value
629                                throw new CoordinatorEngineException(ErrorCode.E0421, filter,
630                                    XLog.format("invalid status value [{0}]." + " Valid status values are: [{1}]",
631                                        valueStr, StringUtils.join(CoordinatorAction.Status.values(), ", ")));
632                            }
633
634                            if (!(comp == FILTER_COMPARATORS.EQUALS || comp == FILTER_COMPARATORS.NOT_EQUALS)) {
635                                throw new CoordinatorEngineException(ErrorCode.E0421, filter,
636                                    XLog.format("invalid comparator [{0}] for status." + " Valid are = and !=",
637                                        comp.getSign()));
638                            }
639
640                            pair = Pair.of(OozieClient.FILTER_STATUS, comp);
641                        } else if (key.equalsIgnoreCase(OozieClient.FILTER_NOMINAL_TIME)) {
642                            try {
643                                value = new Timestamp(DateUtils.parseDateUTC(valueStr).getTime());
644                            } catch (ParseException e) {
645                                throw new CoordinatorEngineException(ErrorCode.E0421, filter,
646                                    XLog.format("invalid nominal time [{0}]." + " Valid format: " +
647                                            "[{1}]", valueStr, DateUtils.ISO8601_UTC_MASK));
648                            }
649                            pair = Pair.of(OozieClient.FILTER_NOMINAL_TIME, comp);
650                        } else {
651                            // Check for incorrect filter option
652                            throw new CoordinatorEngineException(ErrorCode.E0421, filter,
653                                XLog.format("invalid filter [{0}]." + " Valid filters [{1}]", key, StringUtils.join
654                                    (VALID_JOB_FILTERS, ", ")));
655                        }
656                        if (!filterMap.containsKey(pair)) {
657                            filterMap.put(pair, new ArrayList<Object>());
658                        }
659                        filterMap.get(pair).add(value);
660                        break;
661                    }
662                }
663
664                if (pair == null) {
665                    //token doesn't contain comparator
666                    throw new CoordinatorEngineException(ErrorCode.E0421, filter,
667                        "filter should be of format <key><comparator><value> pairs");
668                }
669            }
670        }
671        return filterMap;
672    }
673
674    /**
675     * @param filter
676     * @return Map<String, List<String>>
677     * @throws CoordinatorEngineException
678     */
679    @VisibleForTesting
680    Map<String, List<String>> parseJobsFilter(String filter) throws CoordinatorEngineException {
681        Map<String, List<String>> map = new HashMap<String, List<String>>();
682        boolean isTimeUnitSpecified = false;
683        String timeUnit = "MINUTE";
684        boolean isFrequencySpecified = false;
685        String frequency = "";
686        if (filter != null) {
687            StringTokenizer st = new StringTokenizer(filter, ";");
688            while (st.hasMoreTokens()) {
689                String token = st.nextToken();
690                if (token.contains("=")) {
691                    String[] pair = token.split("=");
692                    if (pair.length != 2) {
693                        throw new CoordinatorEngineException(ErrorCode.E0420, filter,
694                                "elements must be semicolon-separated name=value pairs");
695                    }
696                    if (!FILTER_NAMES.contains(pair[0].toLowerCase())) {
697                        throw new CoordinatorEngineException(ErrorCode.E0420, filter, XLog.format("invalid name [{0}]",
698                                pair[0]));
699                    }
700                    if (pair[0].equalsIgnoreCase("frequency")) {
701                        isFrequencySpecified = true;
702                        try {
703                            frequency = (int) Float.parseFloat(pair[1]) + "";
704                            continue;
705                        }
706                        catch (NumberFormatException NANException) {
707                            throw new CoordinatorEngineException(ErrorCode.E0420, filter, XLog.format(
708                                    "invalid value [{0}] for frequency. A numerical value is expected", pair[1]));
709                        }
710                    }
711                    if (pair[0].equalsIgnoreCase("unit")) {
712                        isTimeUnitSpecified = true;
713                        timeUnit = pair[1];
714                        if (!timeUnit.equalsIgnoreCase("months") && !timeUnit.equalsIgnoreCase("days")
715                                && !timeUnit.equalsIgnoreCase("hours") && !timeUnit.equalsIgnoreCase("minutes")) {
716                            throw new CoordinatorEngineException(ErrorCode.E0420, filter, XLog.format(
717                                    "invalid value [{0}] for time unit. "
718                                            + "Valid value is one of months, days, hours or minutes", pair[1]));
719                        }
720                        continue;
721                    }
722                    if (pair[0].equals("status")) {
723                        try {
724                            CoordinatorJob.Status.valueOf(pair[1]);
725                        }
726                        catch (IllegalArgumentException ex) {
727                            throw new CoordinatorEngineException(ErrorCode.E0420, filter, XLog.format(
728                                    "invalid status [{0}]", pair[1]));
729                        }
730                    }
731                    List<String> list = map.get(pair[0]);
732                    if (list == null) {
733                        list = new ArrayList<String>();
734                        map.put(pair[0], list);
735                    }
736                    list.add(pair[1]);
737                } else {
738                    throw new CoordinatorEngineException(ErrorCode.E0420, filter,
739                            "elements must be semicolon-separated name=value pairs");
740                }
741            }
742            // Unit is specified and frequency is not specified
743            if (!isFrequencySpecified && isTimeUnitSpecified) {
744                throw new CoordinatorEngineException(ErrorCode.E0420, filter, "time unit should be added only when "
745                        + "frequency is specified. Either specify frequency also or else remove the time unit");
746            } else if (isFrequencySpecified) {
747                // Frequency value is specified
748                if (isTimeUnitSpecified) {
749                    if (timeUnit.equalsIgnoreCase("months")) {
750                        timeUnit = "MONTH";
751                    } else if (timeUnit.equalsIgnoreCase("days")) {
752                        timeUnit = "DAY";
753                    } else if (timeUnit.equalsIgnoreCase("hours")) {
754                        // When job details are persisted to database, frequency in hours are converted to minutes.
755                        // This conversion is to conform with that.
756                        frequency = Integer.parseInt(frequency) * 60 + "";
757                        timeUnit = "MINUTE";
758                    } else if (timeUnit.equalsIgnoreCase("minutes")) {
759                        timeUnit = "MINUTE";
760                    }
761                }
762                // Adding the frequency and time unit filters to the filter map
763                List<String> list = new ArrayList<String>();
764                list.add(timeUnit);
765                map.put("unit", list);
766                list = new ArrayList<String>();
767                list.add(frequency);
768                map.put("frequency", list);
769            }
770        }
771        return map;
772    }
773
774    public List<WorkflowJobBean> getReruns(String coordActionId) throws CoordinatorEngineException {
775        List<WorkflowJobBean> wfBeans;
776        try {
777            wfBeans = WorkflowJobQueryExecutor.getInstance().getList(WorkflowJobQuery.GET_WORKFLOWS_PARENT_COORD_RERUN,
778                    coordActionId);
779        }
780        catch (JPAExecutorException e) {
781            throw new CoordinatorEngineException(e);
782        }
783        return wfBeans;
784    }
785
786    /**
787     * Update coord job definition.
788     *
789     * @param conf the conf
790     * @param jobId the job id
791     * @param dryrun the dryrun
792     * @param showDiff the show diff
793     * @return the string
794     * @throws CoordinatorEngineException the coordinator engine exception
795     */
796    public String updateJob(Configuration conf, String jobId, boolean dryrun, boolean showDiff)
797            throws CoordinatorEngineException {
798        try {
799            CoordUpdateXCommand update = new CoordUpdateXCommand(dryrun, conf, jobId, showDiff);
800            return update.call();
801        }
802        catch (CommandException ex) {
803            throw new CoordinatorEngineException(ex);
804        }
805    }
806
807    /**
808     * Return the status for a Job ID
809     *
810     * @param jobId job Id.
811     * @return the job's status
812     * @throws CoordinatorEngineException thrown if the job's status could not be obtained
813     */
814    @Override
815    public String getJobStatus(String jobId) throws CoordinatorEngineException {
816        try {
817            CoordinatorJobBean coordJob = CoordJobQueryExecutor.getInstance().get(
818                    CoordJobQueryExecutor.CoordJobQuery.GET_COORD_JOB_STATUS, jobId);
819            return coordJob.getStatusStr();
820        }
821        catch (JPAExecutorException e) {
822            throw new CoordinatorEngineException(e);
823        }
824    }
825
826    /**
827     * Return the status for an Action ID
828     *
829     * @param actionId action Id.
830     * @return the action's status
831     * @throws CoordinatorEngineException thrown if the action's status could not be obtained
832     */
833    public String getActionStatus(String actionId) throws CoordinatorEngineException {
834        try {
835            CoordinatorActionBean coordAction = CoordActionQueryExecutor.getInstance().get(
836                    CoordActionQueryExecutor.CoordActionQuery.GET_COORD_ACTION_STATUS, actionId);
837            return coordAction.getStatusStr();
838        }
839        catch (JPAExecutorException e) {
840            throw new CoordinatorEngineException(e);
841        }
842    }
843
844    /**
845     * return a list of killed Coordinator job
846     *
847     * @param filter, the filter string for which the coordinator jobs are killed
848     * @param start, the starting index for coordinator jobs
849     * @param length, maximum number of jobs to be killed
850     * @return the list of jobs being killed
851     * @throws CoordinatorEngineException thrown if one or more of the jobs cannot be killed
852     */
853    public CoordinatorJobInfo killJobs(String filter, int start, int length) throws CoordinatorEngineException {
854        try {
855            Map<String, List<String>> filterMap = parseJobsFilter(filter);
856            CoordinatorJobInfo coordinatorJobInfo =
857                    new BulkCoordXCommand(filterMap, start, length, OperationType.Kill).call();
858            if (coordinatorJobInfo == null) {
859                return new CoordinatorJobInfo(new ArrayList<CoordinatorJobBean>(), 0, 0, 0);
860            }
861            return coordinatorJobInfo;
862        }
863        catch (CommandException ex) {
864            throw new CoordinatorEngineException(ex);
865        }
866    }
867
868    /**
869     * return the jobs that've been suspended
870     * @param filter Filter for jobs that will be suspended, can be name, user, group, status, id or combination of any
871     * @param start Offset for the jobs that will be suspended
872     * @param length maximum number of jobs that will be suspended
873     * @return
874     * @throws CoordinatorEngineException
875     */
876    public CoordinatorJobInfo suspendJobs(String filter, int start, int length) throws CoordinatorEngineException {
877        try {
878            Map<String, List<String>> filterMap = parseJobsFilter(filter);
879            CoordinatorJobInfo coordinatorJobInfo =
880                    new BulkCoordXCommand(filterMap, start, length, OperationType.Suspend).call();
881            if (coordinatorJobInfo == null) {
882                return new CoordinatorJobInfo(new ArrayList<CoordinatorJobBean>(), 0, 0, 0);
883            }
884            return coordinatorJobInfo;
885        }
886        catch (CommandException ex) {
887            throw new CoordinatorEngineException(ex);
888        }
889    }
890
891    /**
892     * return the jobs that've been resumed
893     * @param filter Filter for jobs that will be resumed, can be name, user, group, status, id or combination of any
894     * @param start Offset for the jobs that will be resumed
895     * @param length maximum number of jobs that will be resumed
896     * @return
897     * @throws CoordinatorEngineException
898     */
899    public CoordinatorJobInfo resumeJobs(String filter, int start, int length) throws CoordinatorEngineException {
900        try {
901            Map<String, List<String>> filterMap = parseJobsFilter(filter);
902            CoordinatorJobInfo coordinatorJobInfo =
903                    new BulkCoordXCommand(filterMap, start, length, OperationType.Resume).call();
904            if (coordinatorJobInfo == null) {
905                return new CoordinatorJobInfo(new ArrayList<CoordinatorJobBean>(), 0, 0, 0);
906            }
907            return coordinatorJobInfo;
908        }
909        catch (CommandException ex) {
910            throw new CoordinatorEngineException(ex);
911        }
912    }
913}