001    /**
002     * Copyright (c) 2010 Yahoo! Inc. All rights reserved.
003     * Licensed under the Apache License, Version 2.0 (the "License");
004     * you may not use this file except in compliance with the License.
005     * You may obtain a copy of the License at
006     *
007     *   http://www.apache.org/licenses/LICENSE-2.0
008     *
009     *  Unless required by applicable law or agreed to in writing, software
010     *  distributed under the License is distributed on an "AS IS" BASIS,
011     *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
012     *  See the License for the specific language governing permissions and
013     *  limitations under the License. See accompanying LICENSE file.
014     */
015    package org.apache.oozie.client;
016    
017    import java.io.BufferedReader;
018    import java.io.IOException;
019    import java.io.InputStreamReader;
020    import java.io.OutputStream;
021    import java.io.Reader;
022    import java.net.HttpURLConnection;
023    import java.net.URL;
024    import java.net.URLEncoder;
025    import java.util.ArrayList;
026    import java.util.Collections;
027    import java.util.Enumeration;
028    import java.util.HashMap;
029    import java.util.Iterator;
030    import java.util.List;
031    import java.util.Map;
032    import java.util.Properties;
033    import java.util.concurrent.Callable;
034    
035    import javax.xml.parsers.DocumentBuilderFactory;
036    import javax.xml.transform.Transformer;
037    import javax.xml.transform.TransformerFactory;
038    import javax.xml.transform.dom.DOMSource;
039    import javax.xml.transform.stream.StreamResult;
040    
041    import org.apache.oozie.BuildInfo;
042    import org.apache.oozie.client.rest.JsonCoordinatorAction;
043    import org.apache.oozie.client.rest.JsonCoordinatorJob;
044    import org.apache.oozie.client.rest.JsonTags;
045    import org.apache.oozie.client.rest.JsonWorkflowAction;
046    import org.apache.oozie.client.rest.JsonWorkflowJob;
047    import org.apache.oozie.client.rest.RestConstants;
048    import org.json.simple.JSONArray;
049    import org.json.simple.JSONObject;
050    import org.json.simple.JSONValue;
051    import org.w3c.dom.Document;
052    import org.w3c.dom.Element;
053    
054    /**
055     * Client API to submit and manage Oozie workflow jobs against an Oozie intance.
056     * <p/>
057     * This class is thread safe.
058     * <p/>
059     * Syntax for filter for the {@link #getJobsInfo(String)} {@link #getJobsInfo(String, int, int)} methods:
060     * <code>[NAME=VALUE][;NAME=VALUE]*</code>.
061     * <p/>
062     * Valid filter names are:
063     * <p/>
064     * <ul/>
065     * <li>name: the workflow application name from the workflow definition.</li>
066     * <li>user: the user that submitted the job.</li>
067     * <li>group: the group for the job.</li>
068     * <li>status: the status of the job.</li>
069     * </ul>
070     * <p/>
071     * The query will do an AND among all the filter names. The query will do an OR among all the filter values for the same
072     * name. Multiple values must be specified as different name value pairs.
073     */
074    public class OozieClient {
075    
076        public static final long WS_PROTOCOL_VERSION_0 = 0;
077    
078        public static final long WS_PROTOCOL_VERSION = 1;
079    
080        public static final String USER_NAME = "user.name";
081    
082        public static final String GROUP_NAME = "group.name";
083    
084        public static final String APP_PATH = "oozie.wf.application.path";
085    
086        public static final String COORDINATOR_APP_PATH = "oozie.coord.application.path";
087    
088        public static final String EXTERNAL_ID = "oozie.wf.external.id";
089    
090        public static final String WORKFLOW_NOTIFICATION_URL = "oozie.wf.workflow.notification.url";
091    
092        public static final String ACTION_NOTIFICATION_URL = "oozie.wf.action.notification.url";
093    
094        public static final String COORD_ACTION_NOTIFICATION_URL = "oozie.coord.action.notification.url";
095    
096        public static final String RERUN_SKIP_NODES = "oozie.wf.rerun.skip.nodes";
097    
098        public static final String LOG_TOKEN = "oozie.wf.log.token";
099    
100        public static final String ACTION_MAX_RETRIES = "oozie.wf.action.max.retries";
101    
102        public static final String ACTION_RETRY_INTERVAL = "oozie.wf.action.retry.interval";
103    
104        public static final String FILTER_USER = "user";
105    
106        public static final String FILTER_GROUP = "group";
107    
108        public static final String FILTER_NAME = "name";
109    
110        public static final String FILTER_STATUS = "status";
111    
112        public static final String CHANGE_VALUE_ENDTIME = "endtime";
113    
114        public static final String CHANGE_VALUE_PAUSETIME = "pausetime";
115    
116        public static final String CHANGE_VALUE_CONCURRENCY = "concurrency";
117    
118        public static enum SYSTEM_MODE {
119            NORMAL, NOWEBSERVICE, SAFEMODE
120        };
121    
122        private String baseUrl;
123        private String protocolUrl;
124        private boolean validatedVersion = false;
125        private Map<String, String> headers = new HashMap<String, String>();
126    
127    
128    
129        protected OozieClient() {
130        }
131    
132        /**
133         * Create a Workflow client instance.
134         *
135         * @param oozieUrl URL of the Oozie instance it will interact with.
136         */
137        public OozieClient(String oozieUrl) {
138            this.baseUrl = notEmpty(oozieUrl, "oozieUrl");
139            if (!this.baseUrl.endsWith("/")) {
140                this.baseUrl += "/";
141            }
142        }
143    
144        /**
145         * Return the Oozie URL of the workflow client instance.
146         * <p/>
147         * This URL is the base URL fo the Oozie system, with not protocol versioning.
148         *
149         * @return the Oozie URL of the workflow client instance.
150         */
151        public String getOozieUrl() {
152            return baseUrl;
153        }
154    
155        /**
156         * Return the Oozie URL used by the client and server for WS communications.
157         * <p/>
158         * This URL is the original URL plus the versioning element path.
159         *
160         * @return the Oozie URL used by the client and server for communication.
161         * @throws OozieClientException thrown in the client and the server are not protocol compatible.
162         */
163        public String getProtocolUrl() throws OozieClientException {
164            validateWSVersion();
165            return protocolUrl;
166        }
167    
168        /**
169         * Validate that the Oozie client and server instances are protocol compatible.
170         *
171         * @throws OozieClientException thrown in the client and the server are not protocol compatible.
172         */
173        public synchronized void validateWSVersion() throws OozieClientException {
174            if (!validatedVersion) {
175                try {
176                    URL url = new URL(baseUrl + RestConstants.VERSIONS);
177                    HttpURLConnection conn = createConnection(url, "GET");
178                    if (conn.getResponseCode() == HttpURLConnection.HTTP_OK) {
179                        JSONArray array = (JSONArray) JSONValue.parse(new InputStreamReader(conn.getInputStream()));
180                        if (array == null) {
181                            throw new OozieClientException("HTTP error", "no response message");
182                        }
183                        if (!array.contains(WS_PROTOCOL_VERSION) && !array.contains(WS_PROTOCOL_VERSION_0)) {
184                            StringBuilder msg = new StringBuilder();
185                            msg.append("Supported version [").append(WS_PROTOCOL_VERSION).append(
186                            "] or less, Unsupported versions[");
187                            String separator = "";
188                            for (Object version : array) {
189                                msg.append(separator).append(version);
190                            }
191                            msg.append("]");
192                            throw new OozieClientException(OozieClientException.UNSUPPORTED_VERSION, msg.toString());
193                        }
194                        if (array.contains(WS_PROTOCOL_VERSION)) {
195                            protocolUrl = baseUrl + "v" + WS_PROTOCOL_VERSION + "/";
196                        }
197                        else {
198                            if (array.contains(WS_PROTOCOL_VERSION_0)) {
199                                protocolUrl = baseUrl + "v" + WS_PROTOCOL_VERSION_0 + "/";
200                            }
201                        }
202                    }
203                    else {
204                        handleError(conn);
205                    }
206                }
207                catch (IOException ex) {
208                    throw new OozieClientException(OozieClientException.IO_ERROR, ex);
209                }
210                validatedVersion = true;
211            }
212        }
213    
214        /**
215         * Create an empty configuration with just the {@link #USER_NAME} set to the JVM user name.
216         *
217         * @return an empty configuration.
218         */
219        public Properties createConfiguration() {
220            Properties conf = new Properties();
221            conf.setProperty(USER_NAME, System.getProperty("user.name"));
222            return conf;
223        }
224    
225        /**
226         * Set a HTTP header to be used in the WS requests by the workflow instance.
227         *
228         * @param name header name.
229         * @param value header value.
230         */
231        public void setHeader(String name, String value) {
232            headers.put(notEmpty(name, "name"), notNull(value, "value"));
233        }
234    
235        /**
236         * Get the value of a set HTTP header from the workflow instance.
237         *
238         * @param name header name.
239         * @return header value, <code>null</code> if not set.
240         */
241        public String getHeader(String name) {
242            return headers.get(notEmpty(name, "name"));
243        }
244    
245        /**
246         * Get the set HTTP header
247         *
248         * @return map of header key and value
249         */
250        public Map<String, String> getHeaders() {
251            return headers;
252        }
253    
254        /**
255         * Remove a HTTP header from the workflow client instance.
256         *
257         * @param name header name.
258         */
259        public void removeHeader(String name) {
260            headers.remove(notEmpty(name, "name"));
261        }
262    
263        /**
264         * Return an iterator with all the header names set in the workflow instance.
265         *
266         * @return header names.
267         */
268        public Iterator<String> getHeaderNames() {
269            return Collections.unmodifiableMap(headers).keySet().iterator();
270        }
271    
272        private URL createURL(String collection, String resource, Map<String, String> parameters) throws IOException,
273        OozieClientException {
274            validateWSVersion();
275            StringBuilder sb = new StringBuilder();
276            sb.append(protocolUrl).append(collection);
277            if (resource != null && resource.length() > 0) {
278                sb.append("/").append(resource);
279            }
280            if (parameters.size() > 0) {
281                String separator = "?";
282                for (Map.Entry<String, String> param : parameters.entrySet()) {
283                    if (param.getValue() != null) {
284                        sb.append(separator).append(URLEncoder.encode(param.getKey(), "UTF-8")).append("=").append(
285                                URLEncoder.encode(param.getValue(), "UTF-8"));
286                        separator = "&";
287                    }
288                }
289            }
290            return new URL(sb.toString());
291        }
292    
293        private boolean validateCommand(String url) {
294            {
295                if (protocolUrl.contains(baseUrl + "v0")) {
296                    if (url.contains("dryrun") || url.contains("jobtype=c") || url.contains("systemmode")) {
297                        return false;
298                    }
299                }
300            }
301            return true;
302        }
303    
304        /**
305         * Create http connection to oozie server.
306         *
307         * @param url
308         * @param method
309         * @return connection
310         * @throws IOException
311         * @throws OozieClientException
312         */
313        protected HttpURLConnection createConnection(URL url, String method) throws IOException, OozieClientException {
314            HttpURLConnection conn = (HttpURLConnection) url.openConnection();
315            conn.setRequestMethod(method);
316            if (method.equals("POST") || method.equals("PUT")) {
317                conn.setDoOutput(true);
318            }
319            for (Map.Entry<String, String> header : headers.entrySet()) {
320                conn.setRequestProperty(header.getKey(), header.getValue());
321            }
322            return conn;
323        }
324    
325        protected abstract class ClientCallable<T> implements Callable<T> {
326            private String method;
327            private String collection;
328            private String resource;
329            private Map<String, String> params;
330    
331            public ClientCallable(String method, String collection, String resource, Map<String, String> params) {
332                this.method = method;
333                this.collection = collection;
334                this.resource = resource;
335                this.params = params;
336            }
337    
338            public T call() throws OozieClientException {
339                try {
340                    URL url = createURL(collection, resource, params);
341                    if (validateCommand(url.toString())) {
342                        HttpURLConnection conn = createConnection(url, method);
343                        return call(conn);
344                    }
345                    else {
346                        System.out
347                        .println("Option not supported in target server. Supported only on Oozie-2.0 or greater. Use 'oozie help' for details");
348                        throw new OozieClientException(OozieClientException.UNSUPPORTED_VERSION, new Exception());
349                    }
350                }
351                catch (IOException ex) {
352                    throw new OozieClientException(OozieClientException.IO_ERROR, ex);
353                }
354    
355            }
356    
357            protected abstract T call(HttpURLConnection conn) throws IOException, OozieClientException;
358        }
359    
360        static void handleError(HttpURLConnection conn) throws IOException, OozieClientException {
361            int status = conn.getResponseCode();
362            String error = conn.getHeaderField(RestConstants.OOZIE_ERROR_CODE);
363            String message = conn.getHeaderField(RestConstants.OOZIE_ERROR_MESSAGE);
364    
365            if (error == null) {
366                error = "HTTP error code: " + status;
367            }
368    
369            if (message == null) {
370                message = conn.getResponseMessage();
371            }
372            throw new OozieClientException(error, message);
373        }
374    
375        static Map<String, String> prepareParams(String... params) {
376            Map<String, String> map = new HashMap<String, String>();
377            for (int i = 0; i < params.length; i = i + 2) {
378                map.put(params[i], params[i + 1]);
379            }
380            return map;
381        }
382    
383        public void writeToXml(Properties props, OutputStream out) throws IOException {
384            try {
385                Document doc = DocumentBuilderFactory.newInstance().newDocumentBuilder().newDocument();
386                Element conf = doc.createElement("configuration");
387                doc.appendChild(conf);
388                conf.appendChild(doc.createTextNode("\n"));
389                for (Enumeration e = props.keys(); e.hasMoreElements();) {
390                    String name = (String) e.nextElement();
391                    Object object = props.get(name);
392                    String value;
393                    if (object instanceof String) {
394                        value = (String) object;
395                    }
396                    else {
397                        continue;
398                    }
399                    Element propNode = doc.createElement("property");
400                    conf.appendChild(propNode);
401    
402                    Element nameNode = doc.createElement("name");
403                    nameNode.appendChild(doc.createTextNode(name.trim()));
404                    propNode.appendChild(nameNode);
405    
406                    Element valueNode = doc.createElement("value");
407                    valueNode.appendChild(doc.createTextNode(value.trim()));
408                    propNode.appendChild(valueNode);
409    
410                    conf.appendChild(doc.createTextNode("\n"));
411                }
412    
413                DOMSource source = new DOMSource(doc);
414                StreamResult result = new StreamResult(out);
415                TransformerFactory transFactory = TransformerFactory.newInstance();
416                Transformer transformer = transFactory.newTransformer();
417                transformer.transform(source, result);
418            }
419            catch (Exception e) {
420                throw new IOException(e);
421            }
422        }
423    
424        private class JobSubmit extends ClientCallable<String> {
425            private Properties conf;
426    
427            JobSubmit(Properties conf, boolean start) {
428                super("POST", RestConstants.JOBS, "", (start) ? prepareParams(RestConstants.ACTION_PARAM,
429                        RestConstants.JOB_ACTION_START) : prepareParams());
430                this.conf = notNull(conf, "conf");
431            }
432    
433            JobSubmit(String jobId, Properties conf) {
434                super("PUT", RestConstants.JOB, notEmpty(jobId, "jobId"), prepareParams(RestConstants.ACTION_PARAM,
435                        RestConstants.JOB_ACTION_RERUN));
436                this.conf = notNull(conf, "conf");
437            }
438    
439            public JobSubmit(Properties conf, String jobActionDryrun) {
440                super("POST", RestConstants.JOBS, "", prepareParams(RestConstants.ACTION_PARAM,
441                        RestConstants.JOB_ACTION_DRYRUN));
442                this.conf = notNull(conf, "conf");
443            }
444    
445            @Override
446            protected String call(HttpURLConnection conn) throws IOException, OozieClientException {
447                conn.setRequestProperty("content-type", RestConstants.XML_CONTENT_TYPE);
448                writeToXml(conf, conn.getOutputStream());
449                if (conn.getResponseCode() == HttpURLConnection.HTTP_CREATED) {
450                    JSONObject json = (JSONObject) JSONValue.parse(new InputStreamReader(conn.getInputStream()));
451                    return (String) json.get(JsonTags.JOB_ID);
452                }
453                if (conn.getResponseCode() != HttpURLConnection.HTTP_OK) {
454                    handleError(conn);
455                }
456                return null;
457            }
458        }
459    
460        /**
461         * Submit a workflow job.
462         *
463         * @param conf job configuration.
464         * @return the job Id.
465         * @throws OozieClientException thrown if the job could not be submitted.
466         */
467        public String submit(Properties conf) throws OozieClientException {
468            return (new JobSubmit(conf, false)).call();
469        }
470    
471        private class JobAction extends ClientCallable<Void> {
472    
473            JobAction(String jobId, String action) {
474                super("PUT", RestConstants.JOB, notEmpty(jobId, "jobId"), prepareParams(RestConstants.ACTION_PARAM, action));
475            }
476    
477            JobAction(String jobId, String action, String params) {
478                super("PUT", RestConstants.JOB, notEmpty(jobId, "jobId"), prepareParams(RestConstants.ACTION_PARAM, action,
479                        RestConstants.JOB_CHANGE_VALUE, params));
480            }
481    
482            @Override
483            protected Void call(HttpURLConnection conn) throws IOException, OozieClientException {
484                if (!(conn.getResponseCode() == HttpURLConnection.HTTP_OK)) {
485                    handleError(conn);
486                }
487                return null;
488            }
489        }
490    
491        /**
492         * dryrun for a given job
493         *
494         * @param conf Job configuration.
495         */
496        public String dryrun(Properties conf) throws OozieClientException {
497            return new JobSubmit(conf, RestConstants.JOB_ACTION_DRYRUN).call();
498        }
499    
500        /**
501         * Start a workflow job.
502         *
503         * @param jobId job Id.
504         * @throws OozieClientException thrown if the job could not be started.
505         */
506        public void start(String jobId) throws OozieClientException {
507            new JobAction(jobId, RestConstants.JOB_ACTION_START).call();
508        }
509    
510        /**
511         * Submit and start a workflow job.
512         *
513         * @param conf job configuration.
514         * @return the job Id.
515         * @throws OozieClientException thrown if the job could not be submitted.
516         */
517        public String run(Properties conf) throws OozieClientException {
518            return (new JobSubmit(conf, true)).call();
519        }
520    
521        /**
522         * Rerun a workflow job.
523         *
524         * @param jobId job Id to rerun.
525         * @param conf configuration information for the rerun.
526         * @throws OozieClientException thrown if the job could not be started.
527         */
528        public void reRun(String jobId, Properties conf) throws OozieClientException {
529            new JobSubmit(jobId, conf).call();
530        }
531    
532        /**
533         * Suspend a workflow job.
534         *
535         * @param jobId job Id.
536         * @throws OozieClientException thrown if the job could not be suspended.
537         */
538        public void suspend(String jobId) throws OozieClientException {
539            new JobAction(jobId, RestConstants.JOB_ACTION_SUSPEND).call();
540        }
541    
542        /**
543         * Resume a workflow job.
544         *
545         * @param jobId job Id.
546         * @throws OozieClientException thrown if the job could not be resume.
547         */
548        public void resume(String jobId) throws OozieClientException {
549            new JobAction(jobId, RestConstants.JOB_ACTION_RESUME).call();
550        }
551    
552        /**
553         * Kill a workflow job.
554         *
555         * @param jobId job Id.
556         * @throws OozieClientException thrown if the job could not be killed.
557         */
558        public void kill(String jobId) throws OozieClientException {
559            new JobAction(jobId, RestConstants.JOB_ACTION_KILL).call();
560        }
561    
562        /**
563         * Change a coordinator job.
564         *
565         * @param jobId job Id.
566         * @param changeValue change value.
567         * @throws OozieClientException thrown if the job could not be changed.
568         */
569        public void change(String jobId, String changeValue) throws OozieClientException {
570            new JobAction(jobId, RestConstants.JOB_ACTION_CHANGE, changeValue).call();
571        }
572    
573        private class JobInfo extends ClientCallable<WorkflowJob> {
574    
575            JobInfo(String jobId, int start, int len) {
576                super("GET", RestConstants.JOB, notEmpty(jobId, "jobId"), prepareParams(RestConstants.JOB_SHOW_PARAM,
577                        RestConstants.JOB_SHOW_INFO, RestConstants.OFFSET_PARAM, Integer.toString(start),
578                        RestConstants.LEN_PARAM, Integer.toString(len)));
579            }
580    
581            @Override
582            protected WorkflowJob call(HttpURLConnection conn) throws IOException, OozieClientException {
583                if ((conn.getResponseCode() == HttpURLConnection.HTTP_OK)) {
584                    Reader reader = new InputStreamReader(conn.getInputStream());
585                    JSONObject json = (JSONObject) JSONValue.parse(reader);
586                    return new JsonWorkflowJob(json);
587                }
588                else {
589                    handleError(conn);
590                }
591                return null;
592            }
593        }
594    
595        private class WorkflowActionInfo extends ClientCallable<WorkflowAction> {
596            WorkflowActionInfo(String actionId) {
597                super("GET", RestConstants.JOB, notEmpty(actionId, "id"), prepareParams(RestConstants.JOB_SHOW_PARAM,
598                        RestConstants.JOB_SHOW_INFO));
599            }
600    
601            @Override
602            protected WorkflowAction call(HttpURLConnection conn) throws IOException, OozieClientException {
603                if ((conn.getResponseCode() == HttpURLConnection.HTTP_OK)) {
604                    Reader reader = new InputStreamReader(conn.getInputStream());
605                    JSONObject json = (JSONObject) JSONValue.parse(reader);
606                    return new JsonWorkflowAction(json);
607                }
608                else {
609                    handleError(conn);
610                }
611                return null;
612            }
613        }
614    
615        /**
616         * Get the info of a workflow job.
617         *
618         * @param jobId job Id.
619         * @return the job info.
620         * @throws OozieClientException thrown if the job info could not be retrieved.
621         */
622        public WorkflowJob getJobInfo(String jobId) throws OozieClientException {
623            return getJobInfo(jobId, 0, 0);
624        }
625    
626        /**
627         * Get the info of a workflow job and subset actions.
628         *
629         * @param jobId job Id.
630         * @param start starting index in the list of actions belonging to the job
631         * @param len number of actions to be returned
632         * @return the job info.
633         * @throws OozieClientException thrown if the job info could not be retrieved.
634         */
635        public WorkflowJob getJobInfo(String jobId, int start, int len) throws OozieClientException {
636            return new JobInfo(jobId, start, len).call();
637        }
638    
639        /**
640         * Get the info of a workflow action.
641         *
642         * @param actionId Id.
643         * @return the workflow action info.
644         * @throws OozieClientException thrown if the job info could not be retrieved.
645         */
646        public WorkflowAction getWorkflowActionInfo(String actionId) throws OozieClientException {
647            return new WorkflowActionInfo(actionId).call();
648        }
649    
650        /**
651         * Get the log of a workflow job.
652         *
653         * @param jobId job Id.
654         * @return the job log.
655         * @throws OozieClientException thrown if the job info could not be retrieved.
656         */
657        public String getJobLog(String jobId) throws OozieClientException {
658            return new JobLog(jobId).call();
659        }
660    
661        private class JobLog extends JobMetadata {
662    
663            JobLog(String jobId) {
664                super(jobId, RestConstants.JOB_SHOW_LOG);
665            }
666        }
667    
668        /**
669         * Get the definition of a workflow job.
670         *
671         * @param jobId job Id.
672         * @return the job log.
673         * @throws OozieClientException thrown if the job info could not be retrieved.
674         */
675        public String getJobDefinition(String jobId) throws OozieClientException {
676            return new JobDefinition(jobId).call();
677        }
678    
679        private class JobDefinition extends JobMetadata {
680    
681            JobDefinition(String jobId) {
682                super(jobId, RestConstants.JOB_SHOW_DEFINITION);
683            }
684        }
685    
686        private class JobMetadata extends ClientCallable<String> {
687    
688            JobMetadata(String jobId, String metaType) {
689                super("GET", RestConstants.JOB, notEmpty(jobId, "jobId"), prepareParams(RestConstants.JOB_SHOW_PARAM,
690                        metaType));
691            }
692    
693            @Override
694            protected String call(HttpURLConnection conn) throws IOException, OozieClientException {
695                if ((conn.getResponseCode() == HttpURLConnection.HTTP_OK)) {
696    
697                    String output = getReaderAsString(new InputStreamReader(conn.getInputStream()), -1);
698                    return output;
699                }
700                else {
701                    handleError(conn);
702                }
703                return null;
704            }
705    
706            /**
707             * Return a reader as string.
708             * <p/>
709             *
710             * @param reader reader to read into a string.
711             * @param maxLen max content length allowed, if -1 there is no limit.
712             * @return the reader content.
713             * @throws IOException thrown if the resource could not be read.
714             */
715            private String getReaderAsString(Reader reader, int maxLen) throws IOException {
716                if (reader == null) {
717                    throw new IllegalArgumentException("reader cannot be null");
718                }
719    
720                StringBuffer sb = new StringBuffer();
721                char[] buffer = new char[2048];
722                int read;
723                int count = 0;
724                while ((read = reader.read(buffer)) > -1) {
725                    count += read;
726    
727                    // read up to maxLen chars;
728                    if ((maxLen > -1) && (count > maxLen)) {
729                        break;
730                    }
731                    sb.append(buffer, 0, read);
732                }
733                reader.close();
734                return sb.toString();
735            }
736        }
737    
738        private class CoordJobInfo extends ClientCallable<CoordinatorJob> {
739    
740            CoordJobInfo(String jobId, int start, int len) {
741                super("GET", RestConstants.JOB, notEmpty(jobId, "jobId"), prepareParams(RestConstants.JOB_SHOW_PARAM,
742                        RestConstants.JOB_SHOW_INFO, RestConstants.OFFSET_PARAM, Integer.toString(start),
743                        RestConstants.LEN_PARAM, Integer.toString(len)));
744            }
745    
746            @Override
747            protected CoordinatorJob call(HttpURLConnection conn) throws IOException, OozieClientException {
748                if ((conn.getResponseCode() == HttpURLConnection.HTTP_OK)) {
749                    Reader reader = new InputStreamReader(conn.getInputStream());
750                    JSONObject json = (JSONObject) JSONValue.parse(reader);
751                    return new JsonCoordinatorJob(json);
752                }
753                else {
754                    handleError(conn);
755                }
756                return null;
757            }
758        }
759    
760        private class CoordActionInfo extends ClientCallable<CoordinatorAction> {
761            CoordActionInfo(String actionId) {
762                super("GET", RestConstants.JOB, notEmpty(actionId, "id"), prepareParams(RestConstants.JOB_SHOW_PARAM,
763                        RestConstants.JOB_SHOW_INFO));
764            }
765    
766            @Override
767            protected CoordinatorAction call(HttpURLConnection conn) throws IOException, OozieClientException {
768                if ((conn.getResponseCode() == HttpURLConnection.HTTP_OK)) {
769                    Reader reader = new InputStreamReader(conn.getInputStream());
770                    JSONObject json = (JSONObject) JSONValue.parse(reader);
771                    return new JsonCoordinatorAction(json);
772                }
773                else {
774                    handleError(conn);
775                }
776                return null;
777            }
778        }
779    
780        /**
781         * Get the info of a coordinator job.
782         *
783         * @param jobId job Id.
784         * @return the job info.
785         * @throws OozieClientException thrown if the job info could not be retrieved.
786         */
787        public CoordinatorJob getCoordJobInfo(String jobId) throws OozieClientException {
788            return new CoordJobInfo(jobId, 0, 0).call();
789        }
790    
791        /**
792         * Get the info of a coordinator job and subset actions.
793         *
794         * @param jobId job Id.
795         * @param start starting index in the list of actions belonging to the job
796         * @param len number of actions to be returned
797         * @return the job info.
798         * @throws OozieClientException thrown if the job info could not be retrieved.
799         */
800        public CoordinatorJob getCoordJobInfo(String jobId, int start, int len) throws OozieClientException {
801            return new CoordJobInfo(jobId, start, len).call();
802        }
803    
804        /**
805         * Get the info of a coordinator action.
806         *
807         * @param actionId Id.
808         * @return the coordinator action info.
809         * @throws OozieClientException thrown if the job info could not be retrieved.
810         */
811        public CoordinatorAction getCoordActionInfo(String actionId) throws OozieClientException {
812            return new CoordActionInfo(actionId).call();
813        }
814    
815        private class JobsStatus extends ClientCallable<List<WorkflowJob>> {
816    
817            JobsStatus(String filter, int start, int len) {
818                super("GET", RestConstants.JOBS, "", prepareParams(RestConstants.JOBS_FILTER_PARAM, filter,
819                        RestConstants.JOBTYPE_PARAM, "wf", RestConstants.OFFSET_PARAM, Integer.toString(start),
820                        RestConstants.LEN_PARAM, Integer.toString(len)));
821            }
822    
823            @Override
824            @SuppressWarnings("unchecked")
825            protected List<WorkflowJob> call(HttpURLConnection conn) throws IOException, OozieClientException {
826                conn.setRequestProperty("content-type", RestConstants.XML_CONTENT_TYPE);
827                if ((conn.getResponseCode() == HttpURLConnection.HTTP_OK)) {
828                    Reader reader = new InputStreamReader(conn.getInputStream());
829                    JSONObject json = (JSONObject) JSONValue.parse(reader);
830                    JSONArray workflows = (JSONArray) json.get(JsonTags.WORKFLOWS_JOBS);
831                    if (workflows == null) {
832                        workflows = new JSONArray();
833                    }
834                    return JsonWorkflowJob.fromJSONArray(workflows);
835                }
836                else {
837                    handleError(conn);
838                }
839                return null;
840            }
841        }
842    
843        private class CoordJobsStatus extends ClientCallable<List<CoordinatorJob>> {
844    
845            CoordJobsStatus(String filter, int start, int len) {
846                super("GET", RestConstants.JOBS, "", prepareParams(RestConstants.JOBS_FILTER_PARAM, filter,
847                        RestConstants.JOBTYPE_PARAM, "coord", RestConstants.OFFSET_PARAM, Integer.toString(start),
848                        RestConstants.LEN_PARAM, Integer.toString(len)));
849            }
850    
851            @Override
852            @SuppressWarnings("unchecked")
853            protected List<CoordinatorJob> call(HttpURLConnection conn) throws IOException, OozieClientException {
854                conn.setRequestProperty("content-type", RestConstants.XML_CONTENT_TYPE);
855                if ((conn.getResponseCode() == HttpURLConnection.HTTP_OK)) {
856                    Reader reader = new InputStreamReader(conn.getInputStream());
857                    JSONObject json = (JSONObject) JSONValue.parse(reader);
858                    JSONArray jobs = (JSONArray) json.get(JsonTags.COORDINATOR_JOBS);
859                    if (jobs == null) {
860                        jobs = new JSONArray();
861                    }
862                    return JsonCoordinatorJob.fromJSONArray(jobs);
863                }
864                else {
865                    handleError(conn);
866                }
867                return null;
868            }
869        }
870    
871        private class CoordRerun extends ClientCallable<List<JsonCoordinatorAction>> {
872    
873            CoordRerun(String jobId, String rerunType, String scope, boolean refresh, boolean noCleanup) {
874                super("PUT", RestConstants.JOB, notEmpty(jobId, "jobId"), prepareParams(RestConstants.ACTION_PARAM,
875                        RestConstants.JOB_COORD_ACTION_RERUN, RestConstants.JOB_COORD_RERUN_TYPE_PARAM, rerunType,
876                        RestConstants.JOB_COORD_RERUN_SCOPE_PARAM, scope, RestConstants.JOB_COORD_RERUN_REFRESH_PARAM,
877                        Boolean.toString(refresh), RestConstants.JOB_COORD_RERUN_NOCLEANUP_PARAM, Boolean
878                        .toString(noCleanup)));
879            }
880    
881            @Override
882            protected List<JsonCoordinatorAction> call(HttpURLConnection conn) throws IOException, OozieClientException {
883                conn.setRequestProperty("content-type", RestConstants.XML_CONTENT_TYPE);
884                if ((conn.getResponseCode() == HttpURLConnection.HTTP_OK)) {
885                    Reader reader = new InputStreamReader(conn.getInputStream());
886                    JSONObject json = (JSONObject) JSONValue.parse(reader);
887                    JSONArray coordActions = (JSONArray) json.get(JsonTags.COORDINATOR_ACTIONS);
888                    return JsonCoordinatorAction.fromJSONArray(coordActions);
889                }
890                else {
891                    handleError(conn);
892                }
893                return null;
894            }
895        }
896    
897        /**
898         * Rerun coordinator actions.
899         *
900         * @param jobId coordinator jobId
901         * @param rerunType rerun type 'date' if -date is used, 'action-id' if -action is used
902         * @param scope rerun scope for date or actionIds
903         * @param refresh true if -refresh is given in command option
904         * @param noCleanup true if -nocleanup is given in command option
905         * @throws OozieClientException
906         */
907        public List<JsonCoordinatorAction> reRunCoord(String jobId, String rerunType, String scope, boolean refresh,
908                boolean noCleanup) throws OozieClientException {
909            return new CoordRerun(jobId, rerunType, scope, refresh, noCleanup).call();
910        }
911    
912        /**
913         * Return the info of the workflow jobs that match the filter.
914         *
915         * @param filter job filter. Refer to the {@link OozieClient} for the filter syntax.
916         * @param start jobs offset, base 1.
917         * @param len number of jobs to return.
918         * @return a list with the workflow jobs info, without node details.
919         * @throws OozieClientException thrown if the jobs info could not be retrieved.
920         */
921        public List<WorkflowJob> getJobsInfo(String filter, int start, int len) throws OozieClientException {
922            return new JobsStatus(filter, start, len).call();
923        }
924    
925        /**
926         * Return the info of the workflow jobs that match the filter.
927         * <p/>
928         * It returns the first 100 jobs that match the filter.
929         *
930         * @param filter job filter. Refer to the {@link OozieClient} for the filter syntax.
931         * @return a list with the workflow jobs info, without node details.
932         * @throws OozieClientException thrown if the jobs info could not be retrieved.
933         */
934        public List<WorkflowJob> getJobsInfo(String filter) throws OozieClientException {
935            return getJobsInfo(filter, 1, 50);
936        }
937    
938        /**
939         * Print sla info about coordinator and workflow jobs and actions.
940         *
941         * @param start starting offset
942         * @param len number of results
943         * @return
944         * @throws OozieClientException
945         */
946        public void getSlaInfo(int start, int len) throws OozieClientException {
947            new SlaInfo(start, len).call();
948        }
949    
950        private class SlaInfo extends ClientCallable<Void> {
951    
952            SlaInfo(int start, int len) {
953                super("GET", RestConstants.SLA, "", prepareParams(RestConstants.SLA_GT_SEQUENCE_ID,
954                        Integer.toString(start), RestConstants.MAX_EVENTS, Integer.toString(len)));
955            }
956    
957            @Override
958            @SuppressWarnings("unchecked")
959            protected Void call(HttpURLConnection conn) throws IOException, OozieClientException {
960                conn.setRequestProperty("content-type", RestConstants.XML_CONTENT_TYPE);
961                if ((conn.getResponseCode() == HttpURLConnection.HTTP_OK)) {
962                    BufferedReader br = new BufferedReader(new InputStreamReader(conn.getInputStream()));
963                    String line = null;
964                    while ((line = br.readLine()) != null) {
965                        System.out.println(line);
966                    }
967                }
968                else {
969                    handleError(conn);
970                }
971                return null;
972            }
973        }
974    
975        private class JobIdAction extends ClientCallable<String> {
976    
977            JobIdAction(String externalId) {
978                super("GET", RestConstants.JOBS, "", prepareParams(RestConstants.JOBTYPE_PARAM, "wf",
979                        RestConstants.JOBS_EXTERNAL_ID_PARAM, externalId));
980            }
981    
982            @Override
983            @SuppressWarnings("unchecked")
984            protected String call(HttpURLConnection conn) throws IOException, OozieClientException {
985                if ((conn.getResponseCode() == HttpURLConnection.HTTP_OK)) {
986                    Reader reader = new InputStreamReader(conn.getInputStream());
987                    JSONObject json = (JSONObject) JSONValue.parse(reader);
988                    return (String) json.get(JsonTags.JOB_ID);
989                }
990                else {
991                    handleError(conn);
992                }
993                return null;
994            }
995        }
996    
997        /**
998         * Return the workflow job Id for an external Id.
999         * <p/>
1000         * The external Id must have provided at job creation time.
1001         *
1002         * @param externalId external Id given at job creation time.
1003         * @return the workflow job Id for an external Id, <code>null</code> if none.
1004         * @throws OozieClientException thrown if the operation could not be done.
1005         */
1006        public String getJobId(String externalId) throws OozieClientException {
1007            return new JobIdAction(externalId).call();
1008        }
1009    
1010        private class SetSystemMode extends ClientCallable<Void> {
1011    
1012            public SetSystemMode(SYSTEM_MODE status) {
1013                super("PUT", RestConstants.ADMIN, RestConstants.ADMIN_STATUS_RESOURCE, prepareParams(
1014                        RestConstants.ADMIN_SYSTEM_MODE_PARAM, status + ""));
1015            }
1016    
1017            @Override
1018            public Void call(HttpURLConnection conn) throws IOException, OozieClientException {
1019                if (conn.getResponseCode() != HttpURLConnection.HTTP_OK) {
1020                    handleError(conn);
1021                }
1022                return null;
1023            }
1024        }
1025    
1026        /**
1027         * Enable or disable safe mode. Used by OozieCLI. In safe mode, Oozie would not accept any commands except status
1028         * command to change and view the safe mode status.
1029         *
1030         * @param status true to enable safe mode, false to disable safe mode.
1031         * @throws OozieClientException if it fails to set the safe mode status.
1032         */
1033        public void setSystemMode(SYSTEM_MODE status) throws OozieClientException {
1034            new SetSystemMode(status).call();
1035        }
1036    
1037        private class GetSystemMode extends ClientCallable<SYSTEM_MODE> {
1038    
1039            GetSystemMode() {
1040                super("GET", RestConstants.ADMIN, RestConstants.ADMIN_STATUS_RESOURCE, prepareParams());
1041            }
1042    
1043            @Override
1044            protected SYSTEM_MODE call(HttpURLConnection conn) throws IOException, OozieClientException {
1045                if ((conn.getResponseCode() == HttpURLConnection.HTTP_OK)) {
1046                    Reader reader = new InputStreamReader(conn.getInputStream());
1047                    JSONObject json = (JSONObject) JSONValue.parse(reader);
1048                    return SYSTEM_MODE.valueOf((String) json.get(JsonTags.OOZIE_SYSTEM_MODE));
1049                }
1050                else {
1051                    handleError(conn);
1052                }
1053                return SYSTEM_MODE.NORMAL;
1054            }
1055        }
1056    
1057        /**
1058         * Returns if Oozie is in safe mode or not.
1059         *
1060         * @return true if safe mode is ON<br>
1061         *         false if safe mode is OFF
1062         * @throws OozieClientException throw if it could not obtain the safe mode status.
1063         */
1064        /*
1065         * public boolean isInSafeMode() throws OozieClientException { return new GetSafeMode().call(); }
1066         */
1067        public SYSTEM_MODE getSystemMode() throws OozieClientException {
1068            return new GetSystemMode().call();
1069        }
1070    
1071        private class GetBuildVersion extends ClientCallable<String> {
1072    
1073            GetBuildVersion() {
1074                super("GET", RestConstants.ADMIN, RestConstants.ADMIN_BUILD_VERSION_RESOURCE, prepareParams());
1075            }
1076    
1077            @Override
1078            protected String call(HttpURLConnection conn) throws IOException, OozieClientException {
1079                if ((conn.getResponseCode() == HttpURLConnection.HTTP_OK)) {
1080                    Reader reader = new InputStreamReader(conn.getInputStream());
1081                    JSONObject json = (JSONObject) JSONValue.parse(reader);
1082                    return (String) json.get(JsonTags.BUILD_VERSION);
1083                }
1084                else {
1085                    handleError(conn);
1086                }
1087                return null;
1088            }
1089        }
1090    
1091        /**
1092         * Return the Oozie server build version.
1093         *
1094         * @return the Oozie server build version.
1095         * @throws OozieClientException throw if it the server build version could not be retrieved.
1096         */
1097        public String getServerBuildVersion() throws OozieClientException {
1098            return new GetBuildVersion().call();
1099        }
1100    
1101        /**
1102         * Return the Oozie client build version.
1103         *
1104         * @return the Oozie client build version.
1105         */
1106        public String getClientBuildVersion() {
1107            return BuildInfo.getBuildInfo().getProperty(BuildInfo.BUILD_VERSION);
1108        }
1109    
1110        /**
1111         * Return the info of the coordinator jobs that match the filter.
1112         *
1113         * @param filter job filter. Refer to the {@link OozieClient} for the filter syntax.
1114         * @param start jobs offset, base 1.
1115         * @param len number of jobs to return.
1116         * @return a list with the coordinator jobs info
1117         * @throws OozieClientException thrown if the jobs info could not be retrieved.
1118         */
1119        public List<CoordinatorJob> getCoordJobsInfo(String filter, int start, int len) throws OozieClientException {
1120            return new CoordJobsStatus(filter, start, len).call();
1121        }
1122    
1123        private class GetQueueDump extends ClientCallable<List<String>> {
1124            GetQueueDump() {
1125                super("GET", RestConstants.ADMIN, RestConstants.ADMIN_QUEUE_DUMP_RESOURCE, prepareParams());
1126            }
1127    
1128            @Override
1129            protected List<String> call(HttpURLConnection conn) throws IOException, OozieClientException {
1130                if ((conn.getResponseCode() == HttpURLConnection.HTTP_OK)) {
1131                    Reader reader = new InputStreamReader(conn.getInputStream());
1132                    JSONObject json = (JSONObject) JSONValue.parse(reader);
1133                    JSONArray array = (JSONArray) json.get(JsonTags.QUEUE_DUMP);
1134    
1135                    List<String> list = new ArrayList<String>();
1136                    for (Object o : array) {
1137                        JSONObject entry = (JSONObject) o;
1138                        if (entry.get(JsonTags.CALLABLE_DUMP) != null) {
1139                            String value = (String) entry.get(JsonTags.CALLABLE_DUMP);
1140                            list.add(value);
1141                        }
1142                    }
1143                    return list;
1144                }
1145                else {
1146                    handleError(conn);
1147                }
1148                return null;
1149            }
1150        }
1151    
1152        /**
1153         * Return the Oozie queue's commands' dump
1154         *
1155         * @return the list of strings of callable identification in queue
1156         * @throws OozieClientException throw if it the queue dump could not be retrieved.
1157         */
1158        public List<String> getQueueDump() throws OozieClientException {
1159            return new GetQueueDump().call();
1160        }
1161    
1162        /**
1163         * Check if the string is not null or not empty.
1164         *
1165         * @param str
1166         * @param name
1167         * @return string
1168         */
1169        public static String notEmpty(String str, String name) {
1170            if (str == null) {
1171                throw new IllegalArgumentException(name + " cannot be null");
1172            }
1173            if (str.length() == 0) {
1174                throw new IllegalArgumentException(name + " cannot be empty");
1175            }
1176            return str;
1177        }
1178    
1179        /**
1180         * Check if the object is not null.
1181         *
1182         * @param <T>
1183         * @param obj
1184         * @param name
1185         * @return string
1186         */
1187        public static <T> T notNull(T obj, String name) {
1188            if (obj == null) {
1189                throw new IllegalArgumentException(name + " cannot be null");
1190            }
1191            return obj;
1192        }
1193    
1194    }