/**
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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 org.apache.camel.component.undertow;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import io.undertow.client.ClientCallback;
import io.undertow.client.ClientConnection;
import io.undertow.client.ClientExchange;
import io.undertow.client.ClientRequest;
import io.undertow.client.ClientResponse;
import io.undertow.client.UndertowClient;
import io.undertow.connector.ByteBufferPool;
import io.undertow.server.DefaultByteBufferPool;
import io.undertow.util.Headers;
import io.undertow.util.HttpString;
import io.undertow.util.Methods;
import org.apache.camel.impl.verifier.DefaultComponentVerifier;
import org.apache.camel.impl.verifier.ResultBuilder;
import org.apache.camel.impl.verifier.ResultErrorBuilder;
import org.apache.camel.util.FileUtil;
import org.apache.camel.util.ObjectHelper;
import org.apache.camel.util.URISupport;
import org.apache.camel.util.UnsafeUriCharactersEncoder;
import org.xnio.AbstractIoFuture;
import org.xnio.IoFuture;
import org.xnio.OptionMap;
import org.xnio.Xnio;
import org.xnio.XnioWorker;
public final class UndertowComponentVerifier extends DefaultComponentVerifier {
private final UndertowComponent component;
public UndertowComponentVerifier(UndertowComponent component) {
super("undertow", component.getCamelContext());
this.component = component;
}
// *********************************
// Parameters validation
// *********************************
@Override
protected Result verifyParameters(Map<String, Object> parameters) {
// Default is success
final ResultBuilder builder = ResultBuilder.withStatusAndScope(Result.Status.OK, Scope.PARAMETERS);
// Make a copy to avoid clashing with parent validation
final HashMap<String, Object> verifyParams = new HashMap<>(parameters);
// Check if validation is rest-related
final boolean isRest = verifyParams.entrySet().stream().anyMatch(e -> e.getKey().startsWith("rest."));
if (isRest) {
String httpUri = getOption(verifyParams, "rest.host", String.class).orElse(null);
String path = getOption(verifyParams, "rest.path", String.class).map(FileUtil::stripLeadingSeparator).orElse(null);
if (ObjectHelper.isNotEmpty(httpUri) && ObjectHelper.isNotEmpty(path)) {
httpUri = httpUri + "/" + path;
}
verifyParams.put("httpURI", httpUri);
// Cleanup parameters map from rest related stuffs
verifyParams.entrySet().removeIf(e -> e.getKey().startsWith("rest."));
}
// Validate using the catalog
super.verifyParametersAgainstCatalog(builder, verifyParams);
return builder.build();
}
// *********************************
// Connectivity validation
// *********************************
@Override
protected Result verifyConnectivity(Map<String, Object> parameters) {
// Default is success
final ResultBuilder builder = ResultBuilder.withStatusAndScope(Result.Status.OK, Scope.CONNECTIVITY);
// Make a copy to avoid clashing with parent validation
final HashMap<String, Object> verifyParams = new HashMap<>(parameters);
// Check if validation is rest-related
final boolean isRest = verifyParams.entrySet().stream().anyMatch(e -> e.getKey().startsWith("rest."));
String httpUri;
Optional<String> httpMethod;
if (isRest) {
// We are doing rest endpoint validation but as today the endpoint
// can't do any param substitution so the validation is performed
// against the http uri
httpUri = getOption(verifyParams, "rest.host", String.class).orElse(null);
httpMethod = getOption(verifyParams, "rest.method", String.class);
String path = getOption(verifyParams, "rest.path", String.class).map(FileUtil::stripLeadingSeparator).orElse(null);
if (ObjectHelper.isNotEmpty(httpUri) && ObjectHelper.isNotEmpty(path)) {
httpUri = httpUri + "/" + path;
}
verifyParams.put("httpURI", httpUri);
// Cleanup parameters from rest related stuffs
verifyParams.entrySet().removeIf(e -> e.getKey().startsWith("rest."));
}
httpUri = getOption(verifyParams, "httpURI", String.class).orElse(null);
httpMethod = Optional.empty();
// Check whether the http uri is null or empty
if (ObjectHelper.isEmpty(httpUri)) {
builder.error(
ResultErrorBuilder.withMissingOption("httpURI")
.detail("rest", isRest)
.build()
);
// lack of httpURI is a blocking issue so no need to go further
// with the validation
return builder.build();
}
try {
final UndertowClientWrapper wrapper = new UndertowClientWrapper();
final ClientResponse response = wrapper.send(httpUri, httpMethod);
if (response != null) {
int code = response.getResponseCode();
if (code == 401) {
// Unauthorized, add authUsername and authPassword to the list
// of parameters in error
builder.error(
ResultErrorBuilder.withHttpCode(code)
.description(response.getStatus())
.build()
);
} else if (code >= 300 && code < 400) {
// redirect
builder.error(
ResultErrorBuilder.withHttpCode(code)
.description(response.getStatus())
.parameterKey("httpURI")
.detail(
VerificationError.HttpAttribute.HTTP_REDIRECT,
() -> Optional.ofNullable(response.getResponseHeaders().get(Headers.LOCATION).getFirst()))
.build()
);
} else if (code >= 400) {
// generic http error
builder.error(
ResultErrorBuilder.withHttpCode(code)
.description(response.getStatus())
.build()
);
}
}
} catch (Exception e) {
builder.error(
ResultErrorBuilder.withException(e).build()
);
}
return builder.build();
}
// *********************************
// Helpers
// *********************************
private final class UndertowClientWrapper {
private final XnioWorker worker;
private final ByteBufferPool pool;
private UndertowClient client;
private UndertowClientWrapper() throws IOException, URISyntaxException {
this.worker = Xnio.getInstance().createWorker(OptionMap.EMPTY);
this.pool = new DefaultByteBufferPool(true, 17 * 1024);
this.client = UndertowClient.getInstance(getCamelContext().getApplicationContextClassLoader());
}
public ClientResponse send(String httpUri, Optional<String> httpMethod) throws Exception {
URI uri = new URI(UnsafeUriCharactersEncoder.encodeHttpURI(httpUri));
HttpString method = httpMethod.map(Methods::fromString).orElse(Methods.GET);
ClientRequest request = new ClientRequest();
request.setMethod(method);
request.setPath(URISupport.pathAndQueryOf(uri));
IoFuture<ClientConnection> connectFuture = client.connect(uri, worker, pool, OptionMap.EMPTY);
UndertowClientResponseFuture responseFuture = new UndertowClientResponseFuture();
connectFuture.get().sendRequest(request, responseFuture);
// We should set a timeout
return responseFuture.get().getResponse();
}
}
private static final class UndertowClientResponseFuture extends AbstractIoFuture<ClientExchange> implements ClientCallback<ClientExchange> {
@Override
public void completed(ClientExchange result) {
result.setResponseListener(new ClientCallback<ClientExchange>() {
@Override
public void completed(ClientExchange result) {
setResult(result);
}
@Override
public void failed(IOException e) {
setException(e);
}
});
}
@Override
public void failed(IOException e) {
setException(e);
}
}
}