/** * Copyright © 2013 enioka. All rights reserved * Authors: Marc-Antoine GOUILLART (marc-antoine.gouillart@enioka.com) * Pierre COPPEE (pierre.coppee@enioka.com) * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.enioka.jqm.api; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; import java.security.KeyStore; import java.security.KeyStoreException; import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.Properties; import javax.ws.rs.BadRequestException; import javax.ws.rs.client.Client; import javax.ws.rs.client.ClientBuilder; import javax.ws.rs.client.Entity; import javax.ws.rs.client.WebTarget; import javax.ws.rs.core.GenericType; import javax.ws.rs.core.MediaType; import org.glassfish.jersey.client.authentication.HttpAuthenticationFeature; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * Main JQM client API entry point. */ final class JerseyClient implements JqmClient { private static Logger jqmlogger = LoggerFactory.getLogger(JerseyClient.class); private Properties p; private Client client; private WebTarget target; // ///////////////////////////////////////////////////////////////////// // Construction/Connection // ///////////////////////////////////////////////////////////////////// // No public constructor. MUST use factory. JerseyClient(Properties p) { jqmlogger.trace("creating a new WS client"); // ///////////////////////////////////// // Property loading // Properties priority is: Given explicitly > Given as sys param > Given inside conf file. // They are therefore loaded in that reverse order. this.p = new Properties(); // Configuration file InputStream fis = null; try { fis = this.getClass().getClassLoader().getResourceAsStream("jqm.properties"); if (fis != null) { jqmlogger.info("Loading jqm properties file"); this.p.load(fis); } else { jqmlogger.info("No jqm properties file for WS client found in class path"); } } catch (Exception e) { throw new JqmClientException("An error occurred during jqm.properties file search or load", e); } finally { try { fis.close(); } catch (Exception e) { // Ignore. } } // System parameter (only for URL, not documented nor supported by the way) if (System.getProperty("com.enioka.jqm.ws.url") != null) { this.p.setProperty("com.enioka.jqm.ws.url", System.getProperty("com.enioka.jqm.ws.url")); } // Given explicitly this.p.putAll(p); // ///////////////////////////////////// // SSL certificates if (this.p.containsKey("com.enioka.jqm.ws.truststoreFile")) { jqmlogger.info("A trustore was specified and will be loaded"); ClientBuilder bld = ClientBuilder.newBuilder(); KeyStore trust = null; InputStream trustIs = null; try { trust = KeyStore.getInstance(this.p.getProperty("com.enioka.jqm.ws.truststoreType", "JKS")); } catch (KeyStoreException e) { throw new JqmInvalidRequestException("Specified trust store type [" + this.p.getProperty("com.enioka.jqm.ws.truststoreType", "JKS") + "] is invalid", e); } try { trustIs = new FileInputStream(this.p.getProperty("com.enioka.jqm.ws.truststoreFile")); } catch (FileNotFoundException e) { throw new JqmInvalidRequestException("Trust store file [" + this.p.getProperty("com.enioka.jqm.ws.truststoreFile") + "] cannot be found", e); } String trustp = this.p.getProperty("com.enioka.jqm.ws.truststorePass", null); try { trust.load(trustIs, (trustp == null ? null : trustp.toCharArray())); } catch (Exception e) { throw new JqmInvalidRequestException("Could not load the trust store file", e); } finally { try { trustIs.close(); } catch (IOException e) { // Nothing to do. } } bld.trustStore(trust); // Client certificate if (this.p.containsKey("com.enioka.jqm.ws.keystoreFile")) { jqmlogger.info("A keystore was specified and will be loaded"); KeyStore keyStore = null; InputStream keyIs = null; try { keyStore = KeyStore.getInstance(this.p.getProperty("com.enioka.jqm.ws.keystoreType", "JKS")); } catch (KeyStoreException e) { throw new JqmInvalidRequestException("Specified key store type [" + this.p.getProperty("com.enioka.jqm.ws.keystoreType", "JKS") + "] is invalid", e); } try { keyIs = new FileInputStream(this.p.getProperty("com.enioka.jqm.ws.keystoreFile")); } catch (FileNotFoundException e) { throw new JqmInvalidRequestException("Key store file [" + this.p.getProperty("com.enioka.jqm.ws.keystoreFile") + "] cannot be found", e); } String keyp = this.p.getProperty("com.enioka.jqm.ws.keystorePass", null); try { keyStore.load(keyIs, (keyp == null ? null : keyp.toCharArray())); } catch (Exception e) { throw new JqmInvalidRequestException("Could not load the key store file", e); } finally { try { keyIs.close(); } catch (IOException e) { // Nothing to do. } } bld.keyStore(keyStore, keyp); } client = bld.build(); } else { // No trust store given = no SSL allowed => default client client = ClientBuilder.newClient(); } // Basic Authentication (only allowed when no client certificate is given) if (this.p.containsKey("com.enioka.jqm.ws.login") && this.p.containsKey("com.enioka.jqm.ws.password") && !this.p.containsKey("com.enioka.jqm.ws.keystoreFile")) { jqmlogger.info("A login/password pair was specified and will be used"); HttpAuthenticationFeature auth = HttpAuthenticationFeature.basic(this.p.getProperty("com.enioka.jqm.ws.login"), this.p.getProperty("com.enioka.jqm.ws.password")); client.register(auth); } // ///////////////////////////////////// // Create client String url = this.p.getProperty("com.enioka.jqm.ws.url", "http://localhost:1789/ws/client"); jqmlogger.debug("The following root service URL will be used: " + url); this.target = client.target(url); } @Override public void dispose() { p = null; this.client.close(); } // ///////////////////////////////////////////////////////////////////// // Enqueue functions // ///////////////////////////////////////////////////////////////////// @Override public int enqueue(JobRequest jd) { try { return target.path("ji").request().post(Entity.entity(jd, MediaType.APPLICATION_XML), JobInstance.class).getId(); } catch (BadRequestException e) { throw new JqmInvalidRequestException(e.getResponse().readEntity(String.class), e); } catch (Exception e) { throw new JqmClientException(e); } } @Override public int enqueue(String applicationName, String userName) { return enqueue(new JobRequest(applicationName, userName)); } @Override public int enqueueFromHistory(int jobIdToCopy) { JobInstance h = getJob(jobIdToCopy); JobRequest jd = new JobRequest(); jd.setApplication(h.getApplication()); jd.setApplicationName(h.getApplicationName()); jd.setEmail(h.getEmail()); jd.setKeyword1(h.getKeyword1()); jd.setKeyword2(h.getKeyword2()); jd.setKeyword3(h.getKeyword3()); jd.setModule(h.getModule()); jd.setParentID(h.getParent()); jd.setSessionID(h.getSessionID()); jd.setUser(h.getUser()); for (Map.Entry<String, String> p : h.getParameters().entrySet()) { jd.addParameter(p.getKey(), p.getValue()); } return enqueue(jd); } // ///////////////////////////////////////////////////////////////////// // Job destruction // ///////////////////////////////////////////////////////////////////// @Override public void cancelJob(int idJob) { try { target.path("ji/cancelled/" + idJob).request().post(null); } catch (BadRequestException e) { throw new JqmInvalidRequestException(e.getResponse().readEntity(String.class), e); } catch (Exception e) { throw new JqmClientException(e); } } @Override public void deleteJob(int idJob) { try { target.path("ji/waiting/" + idJob).request().delete(); } catch (BadRequestException e) { throw new JqmInvalidRequestException(e.getResponse().readEntity(String.class), e); } catch (Exception e) { throw new JqmClientException(e); } } @Override public void killJob(int idJob) { try { target.path("ji/killed/" + idJob).request().post(null); } catch (BadRequestException e) { throw new JqmInvalidRequestException(e.getResponse().readEntity(String.class), e); } catch (Exception e) { throw new JqmClientException(e); } } // ///////////////////////////////////////////////////////////////////// // Job Pause/restart // ///////////////////////////////////////////////////////////////////// @Override public void pauseQueuedJob(int idJob) { try { target.path("ji/paused/" + idJob).request().post(null); } catch (BadRequestException e) { throw new JqmInvalidRequestException(e.getResponse().readEntity(String.class), e); } catch (Exception e) { throw new JqmClientException(e); } } @Override public void resumeJob(int idJob) { try { target.path("ji/paused/" + idJob).request().delete(); } catch (BadRequestException e) { throw new JqmInvalidRequestException(e.getResponse().readEntity(String.class), e); } catch (Exception e) { throw new JqmClientException(e); } } public int restartCrashedJob(int idJob) { try { return target.path("ji/crashed/" + idJob).request().delete(JobInstance.class).getId(); } catch (BadRequestException e) { throw new JqmInvalidRequestException(e.getResponse().readEntity(String.class), e); } catch (Exception e) { throw new JqmClientException(e); } } // ///////////////////////////////////////////////////////////////////// // Misc. // ///////////////////////////////////////////////////////////////////// @Override public void setJobQueue(int idJob, int idQueue) { try { target.path("q/" + idQueue + "/" + idJob).request().post(null); } catch (BadRequestException e) { throw new JqmInvalidRequestException(e.getResponse().readEntity(String.class), e); } catch (Exception e) { throw new JqmClientException(e); } } @Override public void setJobQueue(int idJob, com.enioka.jqm.api.Queue queue) { setJobQueue(idJob, queue.getId()); } @Override public void setJobQueuePosition(int idJob, int position) { try { target.path("ji/" + idJob + "/position/" + position).request().post(null); } catch (BadRequestException e) { throw new JqmInvalidRequestException(e.getResponse().readEntity(String.class), e); } catch (Exception e) { throw new JqmClientException(e); } } // ///////////////////////////////////////////////////////////////////// // Job queries // ///////////////////////////////////////////////////////////////////// @Override public com.enioka.jqm.api.JobInstance getJob(int idJob) { try { return target.path("ji/" + idJob).request().get(JobInstance.class); } catch (BadRequestException e) { throw new JqmInvalidRequestException(e.getResponse().readEntity(String.class), e); } catch (Exception e) { throw new JqmClientException("An internal JQM error occured", e); } } @Override public List<com.enioka.jqm.api.JobInstance> getJobs() { try { return target.path("ji").request().get(new GenericType<List<JobInstance>>() { }); } catch (Exception e) { throw new JqmClientException(e); } } @Override public List<com.enioka.jqm.api.JobInstance> getActiveJobs() { try { return target.path("ji/active").request().get(new GenericType<List<JobInstance>>() { }); } catch (Exception e) { throw new JqmClientException(e); } } @Override public List<com.enioka.jqm.api.JobInstance> getUserActiveJobs(String user) { try { return target.path("user/" + user + "/ji").request().get(new GenericType<List<JobInstance>>() { }); } catch (BadRequestException e) { throw new JqmInvalidRequestException(e.getResponse().readEntity(String.class), e); } catch (Exception e) { throw new JqmClientException(e); } } @Override public List<JobInstance> getJobs(Query query) { try { Query res = target.path("ji/query").request().post(Entity.entity(query, MediaType.APPLICATION_XML), Query.class); query.setResultSize(res.getResultSize()); query.setResults(res.getResults()); return query.getResults(); } catch (BadRequestException e) { throw new JqmInvalidRequestException(e.getResponse().readEntity(String.class), e); } catch (Exception e) { throw new JqmClientException(e); } } // ///////////////////////////////////////////////////////////////////// // Helpers to quickly access some job instance properties // ///////////////////////////////////////////////////////////////////// @Override public List<String> getJobMessages(int idJob) { try { return getJob(idJob).getMessages(); } catch (BadRequestException e) { throw new JqmInvalidRequestException(e.getResponse().readEntity(String.class), e); } catch (Exception e) { throw new JqmClientException(e); } } @Override public int getJobProgress(int idJob) { try { return getJob(idJob).getProgress(); } catch (BadRequestException e) { throw new JqmInvalidRequestException(e.getResponse().readEntity(String.class), e); } catch (Exception e) { throw new JqmClientException(e); } } // ///////////////////////////////////////////////////////////////////// // Deliverables retrieval // ///////////////////////////////////////////////////////////////////// @Override public List<com.enioka.jqm.api.Deliverable> getJobDeliverables(int idJob) { try { return target.path("ji/" + idJob + "/files").request().get(new GenericType<List<Deliverable>>() { }); } catch (BadRequestException e) { throw new JqmInvalidRequestException(e.getResponse().readEntity(String.class), e); } catch (Exception e) { throw new JqmClientException(e); } } @Override public List<InputStream> getJobDeliverablesContent(int idJob) { List<InputStream> res = new ArrayList<InputStream>(); for (Deliverable d : getJobDeliverables(idJob)) { res.add(getDeliverableContent(d)); } return res; } @Override public InputStream getDeliverableContent(com.enioka.jqm.api.Deliverable d) { try { return target.path("ji/files").request().post(Entity.entity(d, MediaType.APPLICATION_XML), InputStream.class); } catch (BadRequestException e) { throw new JqmInvalidRequestException(e.getResponse().readEntity(String.class), e); } catch (Exception e) { throw new JqmClientException(e); } } @Override public InputStream getDeliverableContent(int d) { try { return target.path("ji/files/" + d).request().get(InputStream.class); } catch (BadRequestException e) { throw new JqmInvalidRequestException(e.getResponse().readEntity(String.class), e); } catch (Exception e) { throw new JqmClientException(e); } } @Override public InputStream getJobLogStdErr(int jobId) { try { return target.path("ji/" + jobId + "/stderr").request().get(InputStream.class); } catch (BadRequestException e) { throw new JqmInvalidRequestException(e.getResponse().readEntity(String.class), e); } catch (Exception e) { throw new JqmClientException(e); } } @Override public InputStream getJobLogStdOut(int jobId) { try { return target.path("ji/" + jobId + "/stdout").request().get(InputStream.class); } catch (BadRequestException e) { throw new JqmInvalidRequestException(e.getResponse().readEntity(String.class), e); } catch (Exception e) { throw new JqmClientException(e); } } // ///////////////////////////////////////////////////////////////////// // Parameters retrieval // ///////////////////////////////////////////////////////////////////// @Override public List<com.enioka.jqm.api.Queue> getQueues() { try { return target.path("q").request().get(new GenericType<List<Queue>>() { }); } catch (Exception e) { throw new JqmClientException(e); } } @Override public List<JobDef> getJobDefinitions() { return getJobDefinitions(null); } @Override public List<JobDef> getJobDefinitions(String application) { try { return target.path("jd").request().get(new GenericType<List<JobDef>>() { }); } catch (Exception e) { throw new JqmClientException(e); } } }