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.executor.jpa;
020
021import java.sql.Timestamp;
022import java.text.ParseException;
023import java.util.ArrayList;
024import java.util.Collections;
025import java.util.HashMap;
026import java.util.Iterator;
027import java.util.List;
028import java.util.Map;
029import java.util.Map.Entry;
030import java.util.regex.Matcher;
031import java.util.regex.Pattern;
032
033import javax.persistence.EntityManager;
034import javax.persistence.Query;
035
036import org.apache.oozie.BundleJobBean;
037import org.apache.oozie.client.BundleJob;
038import org.apache.oozie.client.CoordinatorJob;
039import org.apache.oozie.client.rest.BulkResponseImpl;
040import org.apache.oozie.BulkResponseInfo;
041import org.apache.oozie.CoordinatorActionBean;
042import org.apache.oozie.CoordinatorJobBean;
043import org.apache.oozie.ErrorCode;
044import org.apache.oozie.StringBlob;
045import org.apache.oozie.client.CoordinatorAction;
046import org.apache.oozie.service.Services;
047import org.apache.oozie.util.DateUtils;
048import org.apache.oozie.util.ParamChecker;
049
050/**
051 * The query executor class for bulk monitoring queries i.e. debugging bundle ->
052 * coord actions directly
053 */
054public class BulkJPAExecutor implements JPAExecutor<BulkResponseInfo> {
055    private Map<String, List<String>> bulkFilter;
056    // defaults
057    private int start = 1;
058    private int len = 50;
059    private enum PARAM_TYPE {
060        ID, NAME
061    }
062
063    public BulkJPAExecutor(Map<String, List<String>> bulkFilter, int start, int len) {
064        ParamChecker.notNull(bulkFilter, "bulkFilter");
065        this.bulkFilter = bulkFilter;
066        this.start = start;
067        this.len = len;
068    }
069
070    @Override
071    public String getName() {
072        return "BulkJPAExecutor";
073    }
074
075    @Override
076    public BulkResponseInfo execute(EntityManager em) throws JPAExecutorException {
077        List<BulkResponseImpl> responseList = new ArrayList<BulkResponseImpl>();
078        Map<String, Timestamp> actionTimes = new HashMap<String, Timestamp>();
079
080        try {
081            // Lightweight Query 1 on Bundle level to fetch the bundle job(s)
082            // corresponding to names or ids
083            List<BundleJobBean> bundleBeans = bundleQuery(em);
084
085            // Join query between coordinator job and coordinator action tables
086            // to get entries for specific bundleId only
087            String conditions = actionQuery(em, bundleBeans, actionTimes, responseList);
088
089            // Query to get the count of records
090            long total = countQuery(conditions, em, bundleBeans, actionTimes);
091
092            BulkResponseInfo bulk = new BulkResponseInfo(responseList, start, len, total);
093            return bulk;
094        }
095        catch (Exception e) {
096            throw new JPAExecutorException(ErrorCode.E0603, e.getMessage(), e);
097        }
098    }
099
100    /**
101     * build the bundle level query to get bundle beans for the specified ids or appnames
102     * @param em
103     * @return List BundleJobBeans
104     * @throws JPAExecutorException
105     */
106    @SuppressWarnings("unchecked")
107    private List<BundleJobBean> bundleQuery(EntityManager em) throws JPAExecutorException {
108        Query q = em.createNamedQuery("BULK_MONITOR_BUNDLE_QUERY");
109        StringBuilder bundleQuery = new StringBuilder(q.toString());
110
111        StringBuilder whereClause = null;
112        List<String> bundles = bulkFilter.get(BulkResponseImpl.BULK_FILTER_BUNDLE);
113        if (bundles != null) {
114            PARAM_TYPE type = getParamType(bundles.get(0), 'B');
115            if (type == PARAM_TYPE.NAME) {
116                whereClause = inClause(bundles, "appName", 'b');
117            }
118            else if (type == PARAM_TYPE.ID) {
119                whereClause = inClause(bundles, "id", 'b');
120            }
121
122            // Query: select <columns> from BundleJobBean b where b.id IN (...) _or_ b.appName IN (...)
123            bundleQuery.append(whereClause.replace(whereClause.indexOf("AND"), whereClause.indexOf("AND") + 3, "WHERE"));
124            List<Object[]> bundleObjs = (List<Object[]>) em.createQuery(bundleQuery.toString()).getResultList();
125            if (bundleObjs.isEmpty()) {
126                throw new JPAExecutorException(ErrorCode.E0603, "No entries found for given bundle(s)");
127            }
128
129            List<BundleJobBean> bundleBeans = new ArrayList<BundleJobBean>();
130            for (Object[] bundleElem : bundleObjs) {
131                bundleBeans.add(constructBundleBean(bundleElem));
132            }
133            return bundleBeans;
134        }
135        return null;
136    }
137
138    /**
139     * Validate and determine whether passed param is job-id or appname
140     * @param id
141     * @param job
142     * @return PARAM_TYPE
143     */
144    private PARAM_TYPE getParamType(String id, char job) {
145        Pattern p = Pattern.compile("\\d{7}-\\d{15}-" + Services.get().getSystemId() + "-" + job);
146        Matcher m = p.matcher(id);
147        if (m.matches()) {
148            return PARAM_TYPE.ID;
149        }
150        return PARAM_TYPE.NAME;
151    }
152
153    /**
154     * Compose the coord action level query comprising bundle id/appname filter and coord action
155     * status filter (if specified) and start-time or nominal-time filter (if specified)
156     * @param em
157     * @param bundles
158     * @param times
159     * @param responseList
160     * @return Query string
161     * @throws ParseException
162     */
163    @SuppressWarnings("unchecked")
164    private String actionQuery(EntityManager em, List<BundleJobBean> bundles,
165            Map<String, Timestamp> times, List<BulkResponseImpl> responseList) throws ParseException {
166        Query q = em.createNamedQuery("BULK_MONITOR_ACTIONS_QUERY");
167        StringBuilder getActions = new StringBuilder(q.toString());
168        int offset = getActions.indexOf("ORDER");
169        StringBuilder conditionClause = new StringBuilder();
170
171        List<String> coords = bulkFilter.get(BulkResponseImpl.BULK_FILTER_COORD);
172        // Query: Select <columns> from CoordinatorActionBean a, CoordinatorJobBean c WHERE a.jobId = c.id
173        // AND c.bundleId = :bundleId AND c.appName/id IN (...)
174        if (coords != null) {
175            PARAM_TYPE type = getParamType(coords.get(0), 'C');
176            if (type == PARAM_TYPE.NAME) {
177                conditionClause.append(inClause(bulkFilter.get(BulkResponseImpl.BULK_FILTER_COORD), "appName", 'c'));
178            }
179            else if (type == PARAM_TYPE.ID) {
180                conditionClause.append(inClause(bulkFilter.get(BulkResponseImpl.BULK_FILTER_COORD), "id", 'c'));
181            }
182        }
183        // Query: Select <columns> from CoordinatorActionBean a, CoordinatorJobBean c WHERE a.jobId = c.id
184        // AND c.bundleId = :bundleId AND c.appName/id IN (...) AND a.statusStr IN (...)
185        conditionClause.append(statusClause(bulkFilter.get(BulkResponseImpl.BULK_FILTER_STATUS)));
186        offset = getActions.indexOf("ORDER");
187        getActions.insert(offset - 1, conditionClause);
188
189        // Query: Select <columns> from CoordinatorActionBean a, CoordinatorJobBean c WHERE a.jobId = c.id
190        // AND c.bundleId = :bundleId AND c.appName/id IN (...) AND a.statusStr IN (...)
191        // AND a.createdTimestamp >= startCreated _or_ a.createdTimestamp <= endCreated
192        // AND a.nominalTimestamp >= startNominal _or_ a.nominalTimestamp <= endNominal
193        timesClause(getActions, offset, times);
194        q = em.createQuery(getActions.toString());
195        Iterator<Entry<String, Timestamp>> iter = times.entrySet().iterator();
196        while (iter.hasNext()) {
197            Entry<String, Timestamp> time = iter.next();
198            q.setParameter(time.getKey(), time.getValue());
199        }
200        // pagination
201        q.setFirstResult(start - 1);
202        q.setMaxResults(len);
203        // repeatedly execute above query for each bundle
204        for (BundleJobBean bundle : bundles) {
205            q.setParameter("bundleId", bundle.getId());
206            List<Object[]> response = q.getResultList();
207            for (Object[] r : response) {
208                BulkResponseImpl br = getResponseFromObject(bundle, r);
209                responseList.add(br);
210            }
211        }
212        return q.toString();
213    }
214
215    /**
216     * Get total number of records for use with offset and len in API
217     * @param clause
218     * @param em
219     * @param bundles
220     * @return total count of coord actions
221     */
222    private long countQuery(String clause, EntityManager em, List<BundleJobBean> bundles, Map<String, Timestamp> times) {
223        Query q = em.createNamedQuery("BULK_MONITOR_COUNT_QUERY");
224        StringBuilder getTotal = new StringBuilder(q.toString() + " ");
225        // Query: select COUNT(a) from CoordinatorActionBean a, CoordinatorJobBean c
226        // get entire WHERE clause from above i.e. actionQuery() for all conditions on coordinator job
227        // and action status and times
228        getTotal.append(clause.substring(clause.indexOf("WHERE"), clause.indexOf("ORDER")));
229        int offset = getTotal.indexOf("bundleId");
230        List<String> bundleIds = new ArrayList<String>();
231        for (BundleJobBean bundle : bundles) {
232            bundleIds.add(bundle.getId());
233        }
234        // Query: select COUNT(a) from CoordinatorActionBean a, CoordinatorJobBean c WHERE ...
235        // AND c.bundleId IN (... list of bundle ids) i.e. replace single :bundleId with list
236        getTotal = getTotal.replace(offset - 6, offset + 20, inClause(bundleIds, "bundleId", 'c').toString());
237        q = em.createQuery(getTotal.toString());
238        Iterator<Entry<String, Timestamp>> iter = times.entrySet().iterator();
239        while (iter.hasNext()) {
240            Entry<String, Timestamp> time = iter.next();
241            q.setParameter(time.getKey(), time.getValue());
242        }
243        long total = ((Long) q.getSingleResult()).longValue();
244        return total;
245    }
246
247    // Form the where clause to filter by coordinator appname/id
248    private StringBuilder inClause(List<String> values, String col, char type) {
249        StringBuilder sb = new StringBuilder();
250        boolean firstVal = true;
251        for (String name : nullToEmpty(values)) {
252            if (firstVal) {
253                sb.append(" AND " + type + "." + col + " IN (\'" + name + "\'");
254                firstVal = false;
255            }
256            else {
257                sb.append(",\'" + name + "\'");
258            }
259        }
260        if (!firstVal) {
261            sb.append(") ");
262        }
263        return sb;
264    }
265
266    // Form the where clause to filter by coord action status
267    private StringBuilder statusClause(List<String> statuses) {
268        StringBuilder sb = inClause(statuses, "statusStr", 'a');
269        if (sb.length() == 0) { // statuses was null. adding default
270            sb.append(" AND a.statusStr IN ('KILLED', 'FAILED') ");
271        }
272        return sb;
273    }
274
275    private void timesClause(StringBuilder sb, int offset, Map<String, Timestamp> eachTime) throws ParseException {
276        Timestamp ts = null;
277        List<String> times = bulkFilter.get(BulkResponseImpl.BULK_FILTER_START_CREATED_EPOCH);
278        if (times != null) {
279            ts = new Timestamp(DateUtils.parseDateUTC(times.get(0)).getTime());
280            sb.insert(offset - 1, " AND a.createdTimestamp >= :startCreated");
281            eachTime.put("startCreated", ts);
282        }
283        times = bulkFilter.get(BulkResponseImpl.BULK_FILTER_END_CREATED_EPOCH);
284        if (times != null) {
285            ts = new Timestamp(DateUtils.parseDateUTC(times.get(0)).getTime());
286            sb.insert(offset - 1, " AND a.createdTimestamp <= :endCreated");
287            eachTime.put("endCreated", ts);
288        }
289        times = bulkFilter.get(BulkResponseImpl.BULK_FILTER_START_NOMINAL_EPOCH);
290        if (times != null) {
291            ts = new Timestamp(DateUtils.parseDateUTC(times.get(0)).getTime());
292            sb.insert(offset - 1, " AND a.nominalTimestamp >= :startNominal");
293            eachTime.put("startNominal", ts);
294        }
295        times = bulkFilter.get(BulkResponseImpl.BULK_FILTER_END_NOMINAL_EPOCH);
296        if (times != null) {
297            ts = new Timestamp(DateUtils.parseDateUTC(times.get(0)).getTime());
298            sb.insert(offset - 1, " AND a.nominalTimestamp <= :endNominal");
299            eachTime.put("endNominal", ts);
300        }
301    }
302
303    private BulkResponseImpl getResponseFromObject(BundleJobBean bundleBean, Object arr[]) {
304        BulkResponseImpl bean = new BulkResponseImpl();
305        CoordinatorJobBean coordBean = new CoordinatorJobBean();
306        CoordinatorActionBean actionBean = new CoordinatorActionBean();
307        if (arr[0] != null) {
308            actionBean.setId((String) arr[0]);
309        }
310        if (arr[1] != null) {
311            actionBean.setActionNumber((Integer) arr[1]);
312        }
313        if (arr[2] != null) {
314            actionBean.setErrorCode((String) arr[2]);
315        }
316        if (arr[3] != null) {
317            actionBean.setErrorMessage((String) arr[3]);
318        }
319        if (arr[4] != null) {
320            actionBean.setExternalId((String) arr[4]);
321        }
322        if (arr[5] != null) {
323            actionBean.setExternalStatus((String) arr[5]);
324        }
325        if (arr[6] != null) {
326            actionBean.setStatus(CoordinatorAction.Status.valueOf((String) arr[6]));
327        }
328        if (arr[7] != null) {
329            actionBean.setCreatedTime(DateUtils.toDate((Timestamp) arr[7]));
330        }
331        if (arr[8] != null) {
332            actionBean.setNominalTime(DateUtils.toDate((Timestamp) arr[8]));
333        }
334        if (arr[9] != null) {
335            actionBean.setMissingDependenciesBlob((StringBlob) arr[9]);
336        }
337        if (arr[10] != null) {
338            coordBean.setId((String) arr[10]);
339            actionBean.setJobId((String) arr[10]);
340        }
341        if (arr[11] != null) {
342            coordBean.setAppName((String) arr[11]);
343        }
344        if (arr[12] != null) {
345            coordBean.setStatus(CoordinatorJob.Status.valueOf((String) arr[12]));
346        }
347        bean.setBundle(bundleBean);
348        bean.setCoordinator(coordBean);
349        bean.setAction(actionBean);
350        return bean;
351    }
352
353    private BundleJobBean constructBundleBean(Object[] barr) throws JPAExecutorException {
354        BundleJobBean bean = new BundleJobBean();
355        if (barr[0] != null) {
356            bean.setId((String) barr[0]);
357        }
358        else {
359            throw new JPAExecutorException(ErrorCode.E0603,
360                    "bundleId returned by query is null - cannot retrieve bulk results");
361        }
362        if (barr[1] != null) {
363            bean.setAppName((String) barr[1]);
364        }
365        if (barr[2] != null) {
366            bean.setStatus(BundleJob.Status.valueOf((String) barr[2]));
367        }
368        if (barr[3] != null) {
369            bean.setUser((String) barr[3]);
370        }
371        return bean;
372    }
373
374    // null safeguard
375    public static List<String> nullToEmpty(List<String> input) {
376        return input == null ? Collections.<String> emptyList() : input;
377    }
378
379}