Java tutorial
/** * Copyright 2011-2016 Asakusa Framework Team. * * 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.asakusafw.yaess.jobqueue.client; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; import java.io.Reader; import java.lang.reflect.Field; import java.lang.reflect.Type; import java.net.Socket; import java.net.URI; import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; import java.security.GeneralSecurityException; import java.security.KeyManagementException; import java.security.NoSuchAlgorithmException; import java.security.cert.CertificateException; import java.security.cert.X509Certificate; import java.text.MessageFormat; import javax.net.ssl.SSLContext; import javax.net.ssl.TrustManager; import javax.net.ssl.X509TrustManager; import org.apache.http.HttpEntity; import org.apache.http.HttpResponse; import org.apache.http.HttpStatus; import org.apache.http.auth.AuthScope; import org.apache.http.auth.UsernamePasswordCredentials; import org.apache.http.client.HttpClient; import org.apache.http.client.methods.HttpGet; import org.apache.http.client.methods.HttpPost; import org.apache.http.client.methods.HttpPut; import org.apache.http.client.methods.HttpUriRequest; import org.apache.http.conn.scheme.Scheme; import org.apache.http.conn.ssl.SSLSocketFactory; import org.apache.http.entity.ContentType; import org.apache.http.entity.StringEntity; import org.apache.http.impl.client.DefaultHttpClient; import org.apache.http.impl.conn.PoolingClientConnectionManager; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.asakusafw.yaess.core.ExecutionPhase; import com.google.gson.FieldNamingStrategy; import com.google.gson.GsonBuilder; import com.google.gson.JsonDeserializationContext; import com.google.gson.JsonDeserializer; import com.google.gson.JsonElement; import com.google.gson.JsonObject; import com.google.gson.JsonParseException; import com.google.gson.JsonParser; import com.google.gson.JsonPrimitive; import com.google.gson.JsonSerializationContext; import com.google.gson.JsonSerializer; import com.google.gson.annotations.SerializedName; /** * An implementation of {@link JobClient} via HTTP(S) connections. * @since 0.2.6 */ public class HttpJobClient implements JobClient { static final Logger LOG = LoggerFactory.getLogger(HttpJobClient.class); private static final Charset ENCODING = StandardCharsets.UTF_8; static final ContentType CONTENT_TYPE = ContentType.create("text/json", ENCODING); private static final GsonBuilder GSON_BUILDER; static { GSON_BUILDER = new GsonBuilder(); GSON_BUILDER.registerTypeAdapter(JobStatus.Kind.class, new JobStatusKindAdapter()); GSON_BUILDER.registerTypeAdapter(ExecutionPhase.class, new ExecutionPhaseAdapter()); GSON_BUILDER.setFieldNamingStrategy(new FieldNamingStrategy() { @Override public String translateName(Field f) { SerializedName name = f.getAnnotation(SerializedName.class); if (name != null) { return name.value(); } return f.getName(); } }); } private final String baseUri; private final String user; private final HttpClient http; /** * Creates a new instance. * @param baseUri the target base URL * @throws IllegalArgumentException if some parameters were {@code null} */ public HttpJobClient(String baseUri) { if (baseUri == null) { throw new IllegalArgumentException("baseUri must not be null"); //$NON-NLS-1$ } this.baseUri = normalize(baseUri); this.user = null; this.http = createClient(); } /** * Creates a new instance. * @param baseUri the target base URL * @param user user name * @param password password * @throws IllegalArgumentException if some parameters were {@code null} */ public HttpJobClient(String baseUri, String user, String password) { if (baseUri == null) { throw new IllegalArgumentException("baseUri must not be null"); //$NON-NLS-1$ } if (user == null) { throw new IllegalArgumentException("user must not be null"); //$NON-NLS-1$ } if (password == null) { throw new IllegalArgumentException("password must not be null"); //$NON-NLS-1$ } this.baseUri = normalize(baseUri); this.user = user; DefaultHttpClient client = createClient(); client.getCredentialsProvider().setCredentials(AuthScope.ANY, new UsernamePasswordCredentials(user, password)); this.http = client; } private DefaultHttpClient createClient() { try { DefaultHttpClient client = new DefaultHttpClient(new PoolingClientConnectionManager()); SSLSocketFactory socketFactory = TrustedSSLSocketFactory.create(); Scheme sch = new Scheme("https", 443, socketFactory); client.getConnectionManager().getSchemeRegistry().register(sch); return client; } catch (GeneralSecurityException e) { throw new IllegalStateException( MessageFormat.format("Failed to initialize SSL socket factory: {0}", baseUri), e); } } private static String normalize(String url) { assert url != null; if (url.endsWith("/")) { return url; } return url + "/"; } /** * Returns the base URI. * This URI must end with a slash. * @return the base URI */ public String getBaseUri() { return baseUri; } /** * Returns the target user name for auth. * @return the user name for auth, or {@code null} if there are no credentials */ public String getUser() { return user; } @Override public JobId register(JobScript script) throws IOException, InterruptedException { if (script == null) { throw new IllegalArgumentException("script must not be null"); //$NON-NLS-1$ } HttpPost request = new HttpPost(); URI uri = createUri("jobs"); request.setURI(uri); request.setEntity(createEntity(script)); if (LOG.isDebugEnabled()) { LOG.debug("Registering a job: method=post, uri={}, script={}", uri, script); } HttpResponse response = http.execute(request); if (response.getStatusLine().getStatusCode() == HttpStatus.SC_OK) { JobStatus status = extractJobStatus(request, response); if (status.getKind() == JobStatus.Kind.ERROR) { throw toException(request, response, status, MessageFormat .format("Failed to register a job ({1}): {0}", script, status.getErrorMessage())); } return new JobId(status.getJobId()); } else { throw toException(request, response, MessageFormat.format("Failed to register a job: {0}", script)); } } @Override public JobStatus getStatus(JobId id) throws IOException, InterruptedException { if (id == null) { throw new IllegalArgumentException("id must not be null"); //$NON-NLS-1$ } HttpGet request = new HttpGet(); URI uri = createUri(String.format("jobs/%s", id.getToken())); request.setURI(uri); if (LOG.isDebugEnabled()) { LOG.debug("Obtaining information about job: method=get, uri={}", uri); } HttpResponse response = http.execute(request); if (response.getStatusLine().getStatusCode() == HttpStatus.SC_OK) { JobStatus status = extractJobStatus(request, response); return status; } else { throw toException(request, response, MessageFormat.format("Failed to obtain the job status: {0} ({1})", id.getToken(), request.getURI())); } } @Override public void submit(JobId id) throws IOException, InterruptedException { if (id == null) { throw new IllegalArgumentException("id must not be null"); //$NON-NLS-1$ } HttpPut request = new HttpPut(); URI uri = createUri(String.format("jobs/%s/execute", id.getToken())); request.setURI(uri); if (LOG.isDebugEnabled()) { LOG.debug("Submitting job: method=put, uri={}", uri); } HttpResponse response = http.execute(request); if (response.getStatusLine().getStatusCode() == HttpStatus.SC_OK) { JobStatus status = extractJobStatus(request, response); if (status.getKind() == JobStatus.Kind.ERROR) { throw toException(request, response, status, MessageFormat.format("Failed to submit job: {0} ({1})", id.getToken(), request.getURI())); } } else { throw toException(request, response, MessageFormat.format("Failed to submit job: {0} ({1})", id.getToken(), request.getURI())); } } private URI createUri(String path) { return URI.create(baseUri + path); } private JobStatus extractJobStatus(HttpUriRequest request, HttpResponse response) throws IOException { assert request != null; assert response != null; JobStatus status = extractContent(JobStatus.class, request, response); if (status.getKind() == null) { throw new IOException(MessageFormat.format("status was not specified: {0}", request.getURI())); } if (status.getKind() != JobStatus.Kind.ERROR && status.getJobId() == null) { throw new IOException(MessageFormat.format("job request ID was not specified: {0}", request.getURI())); } if (status.getKind() == JobStatus.Kind.COMPLETED && status.getExitCode() == null) { throw new IOException(MessageFormat.format("exit code was not specified: {0}", request.getURI())); } return status; } private <T> T extractContent(Class<T> type, HttpUriRequest request, HttpResponse response) throws IOException { assert request != null; assert response != null; HttpEntity entity = response.getEntity(); if (entity == null) { throw new IOException(MessageFormat.format("Response message was invalid (empty): {0} ({1})", request.getURI(), response.getStatusLine())); } try (Reader reader = new BufferedReader(new InputStreamReader(entity.getContent(), ENCODING));) { JsonParser parser = new JsonParser(); JsonElement element = parser.parse(reader); if ((element instanceof JsonObject) == false) { throw new IOException( MessageFormat.format("Response message was not a valid json object: {0} ({1})", request.getURI(), response.getStatusLine())); } if (LOG.isTraceEnabled()) { LOG.trace("response: {}", new Object[] { element }); } return GSON_BUILDER.create().fromJson(element, type); } catch (RuntimeException e) { throw new IOException(MessageFormat.format("Response message was invalid (not JSON): {0} ({1})", request.getURI(), response.getStatusLine()), e); } } private IOException toException(HttpUriRequest request, HttpResponse response, String message) { assert request != null; assert response != null; assert message != null; try { JobStatus status = extractJobStatus(request, response); return toException(request, response, status, message); } catch (IOException e) { if (LOG.isDebugEnabled()) { LOG.debug(MessageFormat.format("Failed to analyze an error response (uri={0}, status={1})", request.getURI(), response.getStatusLine()), e); } } return new IOException( MessageFormat.format("{0} (uri={1}, status={2})", message, request.getURI(), response)); } private IOException toException(HttpUriRequest request, HttpResponse response, JobStatus status, String message) { assert request != null; assert response != null; assert status != null; assert message != null; return new IOException(MessageFormat.format("{0} (uri={1}, status={2}, servercode={3}, servermessage={4})", message, request.getURI(), response.getStatusLine(), status.getErrorCode(), status.getErrorMessage())); } private HttpEntity createEntity(JobScript script) { assert script != null; String json = GSON_BUILDER.create().toJson(script); LOG.trace("request: {}", json); return new StringEntity(json, CONTENT_TYPE); } @Override public String toString() { return MessageFormat.format("HttpJobClient({0})", baseUri); } private static final class JobStatusKindAdapter implements JsonDeserializer<JobStatus.Kind> { public JobStatusKindAdapter() { return; } @Override public JobStatus.Kind deserialize(JsonElement json, Type type, JsonDeserializationContext context) throws JsonParseException { if (json.isJsonPrimitive()) { JsonPrimitive primitive = (JsonPrimitive) json; if (primitive.isString()) { JobStatus.Kind kind = JobStatus.Kind.findFromSymbol(primitive.getAsString()); if (kind != null) { return kind; } } } throw new JsonParseException(MessageFormat.format("Invalid JobStatus.Kind: {0}", json)); } } private static final class ExecutionPhaseAdapter implements JsonSerializer<ExecutionPhase> { public ExecutionPhaseAdapter() { return; } @Override public JsonElement serialize(ExecutionPhase src, Type type, JsonSerializationContext context) { return new JsonPrimitive(src.getSymbol()); } } private static final class TrustedSSLSocketFactory extends SSLSocketFactory { private static final String SSL_CONTEXT = "SSL"; private final SSLContext context; private TrustedSSLSocketFactory(SSLContext context) { super(context); this.context = context; } static TrustedSSLSocketFactory create() throws NoSuchAlgorithmException, KeyManagementException { SSLContext context = SSLContext.getInstance(SSL_CONTEXT); context.init(null, new TrustManager[] { new X509TrustManager() { @Override public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException { return; } @Override public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException { return; } @Override public X509Certificate[] getAcceptedIssuers() { return null; } } }, null); return new TrustedSSLSocketFactory(context); } @Override public Socket createSocket() throws IOException { return context.getSocketFactory().createSocket(); } } }