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 java.text.ParseException;
022import java.util.ArrayList;
023import java.util.Arrays;
024import java.util.Date;
025import java.util.HashSet;
026import java.util.LinkedHashSet;
027import java.util.List;
028import java.util.Set;
029import java.util.Map;
030import java.util.HashMap;
031import java.util.concurrent.TimeUnit;
032
033import org.apache.commons.lang.StringUtils;
034import org.apache.hadoop.conf.Configuration;
035import org.apache.oozie.CoordinatorActionBean;
036import org.apache.oozie.CoordinatorEngine;
037import org.apache.oozie.ErrorCode;
038import org.apache.oozie.XException;
039import org.apache.oozie.client.OozieClient;
040import org.apache.oozie.client.rest.RestConstants;
041import org.apache.oozie.command.CommandException;
042import org.apache.oozie.coord.input.logic.CoordInputLogicEvaluator;
043import org.apache.oozie.coord.input.logic.InputLogicParser;
044import org.apache.oozie.executor.jpa.CoordActionGetJPAExecutor;
045import org.apache.oozie.executor.jpa.CoordJobGetActionForNominalTimeJPAExecutor;
046import org.apache.oozie.executor.jpa.JPAExecutorException;
047import org.apache.oozie.service.ConfigurationService;
048import org.apache.oozie.service.JPAService;
049import org.apache.oozie.service.Services;
050import org.apache.oozie.service.XLogService;
051import org.apache.oozie.sla.SLAOperations;
052import org.apache.oozie.util.CoordActionsInDateRange;
053import org.apache.oozie.util.DateUtils;
054import org.apache.oozie.util.Pair;
055import org.apache.oozie.util.ParamChecker;
056import org.apache.oozie.util.XLog;
057import org.apache.oozie.util.XmlUtils;
058import org.jdom.Element;
059import org.jdom.JDOMException;
060
061import com.google.common.annotations.VisibleForTesting;
062
063
064public class CoordUtils {
065    public static final String HADOOP_USER = "user.name";
066
067    public static String getDoneFlag(Element doneFlagElement) {
068        if (doneFlagElement != null) {
069            return doneFlagElement.getTextTrim();
070        }
071        else {
072            return CoordELConstants.DEFAULT_DONE_FLAG;
073        }
074    }
075
076    public static Configuration getHadoopConf(Configuration jobConf) {
077        Configuration conf = new Configuration();
078        ParamChecker.notNull(jobConf, "Configuration to be used for hadoop setup ");
079        String user = ParamChecker.notEmpty(jobConf.get(OozieClient.USER_NAME), OozieClient.USER_NAME);
080        conf.set(HADOOP_USER, user);
081        return conf;
082    }
083
084    /**
085     * Get the list of actions for a given coordinator job
086     * @param rangeType the rerun type (date, action)
087     * @param jobId the coordinator job id
088     * @param scope the date scope or action id scope
089     * @return the list of Coordinator actions
090     * @throws CommandException
091     */
092    public static List<CoordinatorActionBean> getCoordActions(String rangeType, String jobId, String scope,
093            boolean active) throws CommandException {
094        List<CoordinatorActionBean> coordActions = null;
095        if (rangeType.equals(RestConstants.JOB_COORD_SCOPE_DATE)) {
096            coordActions = CoordUtils.getCoordActionsFromDates(jobId, scope, active);
097        }
098        else if (rangeType.equals(RestConstants.JOB_COORD_SCOPE_ACTION)) {
099            coordActions = CoordUtils.getCoordActionsFromIds(jobId, scope);
100        }
101        return coordActions;
102    }
103
104    public static List<String> getActionListForScopeAndDate(String id, String scope, String dates) throws CommandException {
105        List<String> actionIds = new ArrayList<String>();
106
107        List<String> parsed = new ArrayList<String>();
108        if (scope == null && dates == null) {
109            parsed.add(id);
110            return parsed;
111        }
112
113        if (dates != null) {
114            List<CoordinatorActionBean> actionSet = CoordUtils.getCoordActionsFromDates(id, dates, true);
115            for (CoordinatorActionBean action : actionSet) {
116                actionIds.add(action.getId());
117            }
118            parsed.addAll(actionIds);
119        }
120        if (scope != null) {
121            parsed.addAll(CoordUtils.getActionsIds(id, scope));
122        }
123        return parsed;
124    }
125
126
127    /**
128     * Get the list of actions for given date ranges
129     *
130     * @param jobId coordinator job id
131     * @param scope a comma-separated list of date ranges. Each date range element is specified with two dates separated by '::'
132     * @return the list of Coordinator actions for the date range
133     * @throws CommandException thrown if failed to get coordinator actions by given date range
134     */
135    @VisibleForTesting
136    public static List<CoordinatorActionBean> getCoordActionsFromDates(String jobId, String scope, boolean active)
137            throws CommandException {
138        JPAService jpaService = Services.get().get(JPAService.class);
139        ParamChecker.notEmpty(jobId, "jobId");
140        ParamChecker.notEmpty(scope, "scope");
141
142        Set<CoordinatorActionBean> actionSet = new LinkedHashSet<CoordinatorActionBean>();
143        String[] list = scope.split(",");
144        for (String s : list) {
145            s = s.trim();
146            // A date range is specified with two dates separated by '::'
147            if (s.contains("::")) {
148            List<CoordinatorActionBean> listOfActions;
149            try {
150                // Get list of actions within the range of date
151                listOfActions = CoordActionsInDateRange.getCoordActionsFromDateRange(jobId, s, active);
152            }
153            catch (XException e) {
154                throw new CommandException(e);
155            }
156            actionSet.addAll(listOfActions);
157            }
158            else {
159                try {
160                    // Get action for the nominal time
161                    Date date = DateUtils.parseDateOozieTZ(s.trim());
162                    CoordinatorActionBean coordAction = jpaService
163                            .execute(new CoordJobGetActionForNominalTimeJPAExecutor(jobId, date));
164
165                    if (coordAction != null) {
166                        actionSet.add(coordAction);
167                    }
168                    else {
169                        throw new RuntimeException("This should never happen, Coordinator Action shouldn't be null");
170                    }
171                }
172                catch (ParseException e) {
173                    throw new CommandException(ErrorCode.E0302, s.trim(), e);
174                }
175                catch (JPAExecutorException e) {
176                    if (e.getErrorCode() == ErrorCode.E0605) {
177                        XLog.getLog(CoordUtils.class).info("No action for nominal time:" + s + ". Skipping over");
178                    }
179                    throw new CommandException(e);
180                }
181
182            }
183        }
184
185        List<CoordinatorActionBean> coordActions = new ArrayList<CoordinatorActionBean>();
186        for (CoordinatorActionBean coordAction : actionSet) {
187            coordActions.add(coordAction);
188        }
189        return coordActions;
190    }
191
192    public static Set<String> getActionsIds(String jobId, String scope) throws CommandException {
193        ParamChecker.notEmpty(jobId, "jobId");
194        ParamChecker.notEmpty(scope, "scope");
195
196        Set<String> actions = new LinkedHashSet<String>();
197        String[] list = scope.split(",");
198        for (String s : list) {
199            s = s.trim();
200            // An action range is specified with two actions separated by '-'
201            if (s.contains("-")) {
202                String[] range = s.split("-");
203                // Check the format for action's range
204                if (range.length != 2) {
205                    throw new CommandException(ErrorCode.E0302, "format is wrong for action's range '" + s + "', an example of"
206                            + " correct format is 1-5");
207                }
208                int start;
209                int end;
210                //Get the starting and ending action numbers
211                try {
212                    start = Integer.parseInt(range[0].trim());
213                } catch (NumberFormatException ne) {
214                    throw new CommandException(ErrorCode.E0302, "could not parse " + range[0].trim() + "into an integer", ne);
215                }
216                try {
217                    end = Integer.parseInt(range[1].trim());
218                } catch (NumberFormatException ne) {
219                    throw new CommandException(ErrorCode.E0302, "could not parse " + range[1].trim() + "into an integer", ne);
220                }
221                if (start > end) {
222                    throw new CommandException(ErrorCode.E0302, "format is wrong for action's range '" + s + "', starting action"
223                            + "number of the range should be less than ending action number, an example will be 1-4");
224                }
225                // Add the actionIds
226                for (int i = start; i <= end; i++) {
227                    actions.add(jobId + "@" + i);
228                }
229            }
230            else {
231                try {
232                    Integer.parseInt(s);
233                }
234                catch (NumberFormatException ne) {
235                    throw new CommandException(ErrorCode.E0302, "format is wrong for action id'" + s
236                            + "'. Integer only.");
237                }
238                actions.add(jobId + "@" + s);
239            }
240        }
241        return actions;
242    }
243
244    /**
245     * Get the list of actions for given id ranges
246     *
247     * @param jobId coordinator job id
248     * @param scope a comma-separated list of action ranges. The action range is specified with two action numbers separated by '-'
249     * @return the list of all Coordinator actions for action range
250     * @throws CommandException thrown if failed to get coordinator actions by given id range
251     */
252     @VisibleForTesting
253     public static List<CoordinatorActionBean> getCoordActionsFromIds(String jobId, String scope) throws CommandException {
254        JPAService jpaService = Services.get().get(JPAService.class);
255        Set<String> actions = getActionsIds(jobId, scope);
256        // Retrieve the actions using the corresponding actionIds
257        List<CoordinatorActionBean> coordActions = new ArrayList<CoordinatorActionBean>();
258        for (String id : actions) {
259            CoordinatorActionBean coordAction = null;
260            try {
261                coordAction = jpaService.execute(new CoordActionGetJPAExecutor(id));
262            }
263            catch (JPAExecutorException je) {
264                if (je.getErrorCode().equals(ErrorCode.E0605)) { //ignore retrieval of non-existent actions in range
265                    XLog.getLog(XLogService.class).warn(
266                            "Coord action ID num [{0}] not yet materialized. Hence skipping over it for Kill action",
267                            id.substring(id.indexOf("@") + 1));
268                    continue;
269                }
270                else {
271                    throw new CommandException(je);
272                }
273            }
274            coordActions.add(coordAction);
275        }
276        return coordActions;
277    }
278
279     /**
280      * Check if sla alert is disabled for action.
281      * @param actionBean
282      * @param coordName
283      * @param jobConf
284      * @return true if SLA alert is disabled for action
285      * @throws ParseException
286      */
287    public static boolean isSlaAlertDisabled(CoordinatorActionBean actionBean, String coordName, Configuration jobConf)
288            throws ParseException {
289
290        int disableSlaNotificationOlderThan = jobConf.getInt(OozieClient.SLA_DISABLE_ALERT_OLDER_THAN,
291                ConfigurationService.getInt(OozieClient.SLA_DISABLE_ALERT_OLDER_THAN));
292
293        if (disableSlaNotificationOlderThan > 0) {
294            // Disable alert for catchup jobs
295            long timeDiffinHrs = TimeUnit.MILLISECONDS.toHours(new Date().getTime()
296                    - actionBean.getNominalTime().getTime());
297            if (timeDiffinHrs > jobConf.getLong(OozieClient.SLA_DISABLE_ALERT_OLDER_THAN,
298                    ConfigurationService.getLong(OozieClient.SLA_DISABLE_ALERT_OLDER_THAN))) {
299                return true;
300            }
301        }
302
303        boolean disableAlert = false;
304        if (jobConf.get(OozieClient.SLA_DISABLE_ALERT_COORD) != null) {
305            String coords = jobConf.get(OozieClient.SLA_DISABLE_ALERT_COORD);
306            Set<String> coordsToDisableFor = new HashSet<String>(Arrays.asList(coords.split(",")));
307            if (coordsToDisableFor.contains(coordName)) {
308                return true;
309            }
310            if (coordsToDisableFor.contains(actionBean.getJobId())) {
311                return true;
312            }
313        }
314
315        // Check if sla alert is disabled for that action
316        if (!StringUtils.isEmpty(jobConf.get(OozieClient.SLA_DISABLE_ALERT))
317                && getCoordActionSLAAlertStatus(actionBean, coordName, jobConf, OozieClient.SLA_DISABLE_ALERT)) {
318            return true;
319        }
320
321        // Check if sla alert is enabled for that action
322        if (!StringUtils.isEmpty(jobConf.get(OozieClient.SLA_ENABLE_ALERT))
323                && getCoordActionSLAAlertStatus(actionBean, coordName, jobConf, OozieClient.SLA_ENABLE_ALERT)) {
324            return false;
325        }
326
327        return disableAlert;
328    }
329
330    /**
331     * Get coord action SLA alert status.
332     * @param actionBean
333     * @param coordName
334     * @param jobConf
335     * @param slaAlertType
336     * @return status of coord action SLA alert
337     * @throws ParseException
338     */
339    private static boolean getCoordActionSLAAlertStatus(CoordinatorActionBean actionBean, String coordName,
340            Configuration jobConf, String slaAlertType) throws ParseException {
341        String slaAlertList;
342
343       if (!StringUtils.isEmpty(jobConf.get(slaAlertType))) {
344            slaAlertList = jobConf.get(slaAlertType);
345            // check if ALL or date/action-num range
346            if (slaAlertList.equalsIgnoreCase(SLAOperations.ALL_VALUE)) {
347                return true;
348            }
349            String[] values = slaAlertList.split(",");
350            for (String value : values) {
351                value = value.trim();
352                if (value.contains("::")) {
353                    String[] datesInRange = value.split("::");
354                    Date start = DateUtils.parseDateOozieTZ(datesInRange[0].trim());
355                    Date end = DateUtils.parseDateOozieTZ(datesInRange[1].trim());
356                    // check if nominal time in this range
357                    if (actionBean.getNominalTime().compareTo(start) >= 0
358                            || actionBean.getNominalTime().compareTo(end) <= 0) {
359                        return true;
360                    }
361                }
362                else if (value.contains("-")) {
363                    String[] actionsInRange = value.split("-");
364                    int start = Integer.parseInt(actionsInRange[0].trim());
365                    int end = Integer.parseInt(actionsInRange[1].trim());
366                    // check if action number in this range
367                    if (actionBean.getActionNumber() >= start || actionBean.getActionNumber() <= end) {
368                        return true;
369                    }
370                }
371                else {
372                    int actionNumber = Integer.parseInt(value.trim());
373                    if (actionBean.getActionNumber() == actionNumber) {
374                        return true;
375                    }
376                }
377            }
378        }
379        return false;
380    }
381
382    // Form the where clause to filter by status values
383    public static Map<String, Object> getWhereClause(StringBuilder sb, Map<Pair<String, CoordinatorEngine.FILTER_COMPARATORS>,
384            List<Object>> filterMap) {
385        Map<String, Object> params = new HashMap<String, Object>();
386        int pcnt= 1;
387        for (Map.Entry<Pair<String, CoordinatorEngine.FILTER_COMPARATORS>, List<Object>> filter : filterMap.entrySet()) {
388            String field = filter.getKey().getFirst();
389            CoordinatorEngine.FILTER_COMPARATORS comp = filter.getKey().getSecond();
390            String sqlField;
391            if (field.equals(OozieClient.FILTER_STATUS)) {
392                sqlField = "a.statusStr";
393            } else if (field.equals(OozieClient.FILTER_NOMINAL_TIME)) {
394                sqlField = "a.nominalTimestamp";
395            } else {
396                throw new IllegalArgumentException("Invalid filter key " + field);
397            }
398
399            sb.append(" and ").append(sqlField).append(" ");
400            switch (comp) {
401                case EQUALS:
402                    sb.append("IN (");
403                    params.putAll(appendParams(sb, filter.getValue(), pcnt));
404                    sb.append(")");
405                    break;
406
407                case NOT_EQUALS:
408                    sb.append("NOT IN (");
409                    params.putAll(appendParams(sb, filter.getValue(), pcnt));
410                    sb.append(")");
411                    break;
412
413                case GREATER:
414                case GREATER_EQUAL:
415                case LESSTHAN:
416                case LESSTHAN_EQUAL:
417                    if (filter.getValue().size() != 1) {
418                        throw new IllegalArgumentException(field + comp.getSign() + " can't have more than 1 values");
419                    }
420
421                    sb.append(comp.getSign()).append(" ");
422                    params.putAll(appendParams(sb, filter.getValue(), pcnt));
423                    break;
424            }
425
426            pcnt += filter.getValue().size();
427        }
428        sb.append(" ");
429        return params;
430    }
431
432    private static Map<String, Object> appendParams(StringBuilder sb, List<Object> value, int sindex) {
433        Map<String, Object> params = new HashMap<String, Object>();
434        boolean first = true;
435        for (Object val : value) {
436            String pname = "p" + sindex++;
437            params.put(pname, val);
438            if (!first) {
439                sb.append(", ");
440            }
441            sb.append(':').append(pname);
442            first = false;
443        }
444        return params;
445    }
446
447    public static boolean isInputLogicSpecified(String actionXml) throws JDOMException {
448        return isInputLogicSpecified(XmlUtils.parseXml(actionXml));
449    }
450
451    public static boolean isInputLogicSpecified(Element eAction) throws JDOMException {
452        return eAction.getChild(CoordInputLogicEvaluator.INPUT_LOGIC, eAction.getNamespace()) != null;
453    }
454
455    public static String getInputLogic(String actionXml) throws JDOMException {
456        return getInputLogic(XmlUtils.parseXml(actionXml));
457    }
458
459    public static String getInputLogic(Element actionXml) throws JDOMException {
460        return new InputLogicParser().parse(actionXml.getChild(CoordInputLogicEvaluator.INPUT_LOGIC,
461                actionXml.getNamespace()));
462    }
463
464}