org.jboss.hal.dmr.dispatch.Dispatcher Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of hal-dmr Show documentation
Show all versions of hal-dmr Show documentation
Contains classes to talk to the domain controller using the DMR protocol
The newest version!
/*
* Copyright 2022 Red Hat
*
* 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
*
* https://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.jboss.hal.dmr.dispatch;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.function.Consumer;
import java.util.function.Predicate;
import javax.inject.Inject;
import org.jboss.hal.config.AccessControlProvider;
import org.jboss.hal.config.Endpoints;
import org.jboss.hal.config.Environment;
import org.jboss.hal.config.Settings;
import org.jboss.hal.config.keycloak.Keycloak;
import org.jboss.hal.config.keycloak.KeycloakSingleton;
import org.jboss.hal.dmr.Composite;
import org.jboss.hal.dmr.CompositeResult;
import org.jboss.hal.dmr.ModelNode;
import org.jboss.hal.dmr.Operation;
import org.jboss.hal.dmr.Property;
import org.jboss.hal.dmr.ResourceAddress;
import org.jboss.hal.dmr.dispatch.ResponseHeadersProcessor.Header;
import org.jboss.hal.dmr.macro.Action;
import org.jboss.hal.dmr.macro.Macro;
import org.jboss.hal.dmr.macro.MacroFinishedEvent;
import org.jboss.hal.dmr.macro.MacroOperationEvent;
import org.jboss.hal.dmr.macro.MacroOptions;
import org.jboss.hal.dmr.macro.Macros;
import org.jboss.hal.dmr.macro.RecordingEvent;
import org.jboss.hal.dmr.macro.RecordingEvent.RecordingHandler;
import org.jboss.hal.resources.Resources;
import org.jboss.hal.spi.Message;
import org.jboss.hal.spi.MessageEvent;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.web.bindery.event.shared.EventBus;
import elemental2.dom.Blob;
import elemental2.dom.Blob.ConstructorBlobPartsArrayUnionType;
import elemental2.dom.BlobPropertyBag;
import elemental2.dom.File;
import elemental2.dom.FileList;
import elemental2.dom.FormData;
import elemental2.dom.FormData.AppendValueUnionType;
import elemental2.dom.Headers;
import elemental2.dom.Request;
import elemental2.dom.RequestInit;
import elemental2.dom.Response;
import elemental2.promise.IThenable.ThenOnFulfilledCallbackFn;
import elemental2.promise.Promise;
import elemental2.promise.Promise.CatchOnRejectedCallbackFn;
import static java.util.stream.Collectors.joining;
import static com.google.common.collect.Sets.difference;
import static elemental2.core.Global.encodeURIComponent;
import static elemental2.dom.DomGlobal.fetch;
import static elemental2.dom.DomGlobal.navigator;
import static org.jboss.hal.config.Settings.Key.RUN_AS;
import static org.jboss.hal.dmr.ModelDescriptionConstants.DESCRIPTION;
import static org.jboss.hal.dmr.ModelDescriptionConstants.FIND_NON_PROGRESSING_OPERATION;
import static org.jboss.hal.dmr.ModelDescriptionConstants.HOST;
import static org.jboss.hal.dmr.ModelDescriptionConstants.INSTALLED_DRIVER_LIST;
import static org.jboss.hal.dmr.ModelDescriptionConstants.OP;
import static org.jboss.hal.dmr.ModelDescriptionConstants.OPERATION;
import static org.jboss.hal.dmr.ModelDescriptionConstants.QUERY;
import static org.jboss.hal.dmr.ModelDescriptionConstants.RESPONSE;
import static org.jboss.hal.dmr.ModelDescriptionConstants.RESPONSE_HEADERS;
import static org.jboss.hal.dmr.ModelDescriptionConstants.RESULT;
import static org.jboss.hal.dmr.ModelDescriptionConstants.SERVER_GROUPS;
import static org.jboss.hal.dmr.dispatch.Dispatcher.HttpMethod.GET;
import static org.jboss.hal.dmr.dispatch.Dispatcher.HttpMethod.POST;
import static org.jboss.hal.dmr.dispatch.PayloadProcessor.PARSE_ERROR;
import static org.jboss.hal.dmr.dispatch.RequestHeader.ACCEPT;
import static org.jboss.hal.dmr.dispatch.RequestHeader.CONTENT_TYPE;
import static org.jboss.hal.dmr.dispatch.RequestHeader.X_MANAGEMENT_CLIENT_NAME;
/** Executes operations against the management endpoint. */
public class Dispatcher implements RecordingHandler {
static final String APPLICATION_DMR_ENCODED = "application/dmr-encoded";
static final String APPLICATION_JSON = "application/json";
private static final String HEADER_MANAGEMENT_CLIENT_VALUE = "HAL";
private static final Set READ_ONLY_OPERATIONS = new HashSet<>(Arrays.asList(QUERY, FIND_NON_PROGRESSING_OPERATION,
INSTALLED_DRIVER_LIST));
private static final Predicate READ_ONLY = operation -> operation.getName().startsWith("read")
|| READ_ONLY_OPERATIONS.contains(operation.getName());
private static final Logger logger = LoggerFactory.getLogger(Dispatcher.class);
private final Environment environment;
private final Endpoints endpoints;
private final Settings settings;
private final EventBus eventBus;
private final ResponseHeadersProcessors responseHeadersProcessors;
private final Macros macros;
private final ErrorCallback errorCallback;
@Inject
public Dispatcher(Environment environment, Endpoints endpoints, Settings settings,
EventBus eventBus, ResponseHeadersProcessors responseHeadersProcessors,
Macros macros, Resources resources) {
this.environment = environment;
this.endpoints = endpoints;
this.settings = settings;
this.eventBus = eventBus;
this.responseHeadersProcessors = responseHeadersProcessors;
this.macros = macros;
this.eventBus.addHandler(RecordingEvent.getType(), this);
this.errorCallback = (operation, error) -> {
logger.error("Dispatcher error: {}, operation {}", error, operation.asCli());
eventBus.fireEvent(new MessageEvent(Message.error(resources.messages().lastOperationException(), error)));
};
}
public ErrorCallback getDefaultErrorCallback() {
return this.errorCallback;
}
// ------------------------------------------------------ execute composite
public void execute(Composite operations, Consumer success) {
execute(operations, success, errorCallback);
}
public void execute(Composite operations, Consumer success, ErrorCallback errorCallback) {
dmr(operations)
.then(payload -> {
success.accept(compositeResult(payload));
return null;
})
.catch_(error -> {
errorCallback.onError(operations, String.valueOf(error));
return null;
});
}
public Promise execute(Composite operations) {
return dmr(operations).then(payload -> Promise.resolve(compositeResult(payload)));
}
private CompositeResult compositeResult(ModelNode payload) {
return new CompositeResult(payload.get(RESULT));
}
// ------------------------------------------------------ execute operation
public void execute(Operation operation, Consumer success) {
execute(operation, success, errorCallback);
}
public void execute(Operation operation, Consumer success, ErrorCallback errorCallback) {
dmr(operation)
.then(payload -> {
success.accept(operationResult(payload));
return null;
})
.catch_(error -> {
errorCallback.onError(operation, String.valueOf(error));
return null;
});
}
public Promise execute(Operation operation) {
return dmr(operation).then(payload -> Promise.resolve(operationResult(payload)));
}
private ModelNode operationResult(ModelNode payload) {
return payload.get(RESULT);
}
/**
* Executes the operation and upon successful result, calls the success function with the response results, but doesn't
* retrieve the "result" payload as the other execute methods does. You should use this method if the response node you want
* is not in the "result" attribute.
*/
public void dmr(Operation operation, Consumer success, ErrorCallback errorCallback) {
dmr(operation)
.then(payload -> {
success.accept(payload);
return null;
})
.catch_(error -> {
errorCallback.onError(operation, String.valueOf(error));
return null;
});
}
/**
* Executes the operation and upon successful result, returns the response results, but doesn't retrieve the "result"
* payload as the other execute methods does. You should use this method if the response node you want is not in the
* "result" attribute.
*/
public Promise dmr(Operation operation) {
RequestInit init = requestInit(POST, true);
init.setBody(runAs(operation).toBase64String());
Request request = new Request(endpoints.dmr(), init);
return fetch(request)
.then(processResponse())
.then(processText(operation, new DmrPayloadProcessor(), true))
.catch_(rejectWithError());
}
// ------------------------------------------------------ upload
public Promise upload(FileList files, Operation operation) {
Operation uploadOperation = runAs(operation);
ConstructorBlobPartsArrayUnionType blob = ConstructorBlobPartsArrayUnionType.of(
uploadOperation.toBase64String());
BlobPropertyBag options = BlobPropertyBag.create();
options.setType("application/dmr-encoded");
FormData formData = new FormData();
for (int i = 0; i < files.getLength(); i++) {
File file = files.item(i);
appendFile(formData, file);
}
formData.append(OPERATION, new Blob(new ConstructorBlobPartsArrayUnionType[] { blob }, options));
return fetch(uploadRequest(formData))
.then(processResponse())
.then(processText(operation, new UploadPayloadProcessor(), false))
.then(payload -> Promise.resolve(operationResult(payload)))
.catch_(rejectWithError());
}
public Promise upload(File file, Operation operation) {
Operation uploadOperation = runAs(operation);
ConstructorBlobPartsArrayUnionType blob = ConstructorBlobPartsArrayUnionType.of(
uploadOperation.toBase64String());
BlobPropertyBag options = BlobPropertyBag.create();
options.setType("application/dmr-encoded");
FormData formData = new FormData();
appendFile(formData, file);
formData.append(OPERATION, new Blob(new ConstructorBlobPartsArrayUnionType[] { blob }, options));
return fetch(uploadRequest(formData))
.then(processResponse())
.then(processText(operation, new UploadPayloadProcessor(), false))
.then(payload -> Promise.resolve(operationResult(payload)))
.catch_(rejectWithError());
}
private Request uploadRequest(FormData formData) {
RequestInit init = requestInit(POST, false);
init.setBody(formData);
return new Request(endpoints.upload(), init);
}
private void appendFile(FormData formData, File file) {
if (navigator.userAgent.contains("Safari") && !navigator.userAgent.contains("Chrome")) {
// Safari does not support sending new files
// https://bugs.webkit.org/show_bug.cgi?id=165081
ConstructorBlobPartsArrayUnionType fileAsBlob = ConstructorBlobPartsArrayUnionType.of(file);
formData.append(file.name, new Blob(new ConstructorBlobPartsArrayUnionType[] { fileAsBlob }));
} else {
formData.append(file.name, AppendValueUnionType.of(file));
}
}
// ------------------------------------------------------ download
public void download(Operation operation, Consumer success) {
Operation downloadOperation = runAs(operation);
String downloadUrl = downloadUrl(downloadOperation);
RequestInit init = requestInit(GET, true);
Request request = new Request(downloadUrl, init);
fetch(request)
.then(response -> {
if (response.status != 200) {
return Promise.reject(ResponseStatus.fromStatusCode(response.status).statusText());
} else {
return response.text();
}
})
.then(text -> {
success.accept(text);
return null;
})
.catch_(rejectWithError());
}
public String downloadUrl(Operation operation) {
return operationUrl(operation) + "&useStreamAsResponse"; // NON-NLS
}
// ------------------------------------------------------ run-as and urls
private Operation runAs(Operation operation) {
if (environment.getAccessControlProvider() == AccessControlProvider.RBAC) {
Set runAs = settings.get(RUN_AS).asSet();
if (!runAs.isEmpty() && !difference(runAs, operation.getRoles()).isEmpty()) {
return operation.runAs(runAs);
}
}
return operation;
}
private String operationUrl(Operation operation) {
StringBuilder builder = new StringBuilder();
builder.append(endpoints.dmr()).append("/");
// 1. address
ResourceAddress address = operation.getAddress();
if (!address.isEmpty()) {
String path = address.asPropertyList().stream()
.map(property -> encodeURIComponent(property.getName()) + "/" +
encodeURIComponent(property.getValue().asString()))
.collect(joining("/"));
builder.append(path);
}
// 2. operation
String name = operation.getName();
if (GetOperation.isSupported(name)) {
GetOperation getOperation = GetOperation.get(name);
name = getOperation.httpGetOperation();
}
builder.append("?").append(OP).append("=").append(name);
// 3. parameter
if (operation.hasParameter()) {
operation.getParameter().asPropertyList().forEach(property -> {
builder.append("&").append(encodeURIComponent(property.getName()));
if (property.getValue().isDefined()) {
builder.append("=").append(encodeURIComponent(property.getValue().asString()));
}
});
}
// 4. bearer token
String token = token();
if (token != null) {
builder.append("&access_token=").append(token);
}
// TODO operation headers
return builder.toString();
}
// ------------------------------------------------------ request && promise handlers
RequestInit requestInit(HttpMethod method, boolean dmr) {
Headers headers = new Headers();
if (dmr) {
headers.set(ACCEPT.header(), APPLICATION_DMR_ENCODED);
headers.set(CONTENT_TYPE.header(), APPLICATION_DMR_ENCODED);
}
headers.set(X_MANAGEMENT_CLIENT_NAME.header(), HEADER_MANAGEMENT_CLIENT_VALUE);
String bearerToken = token();
if (bearerToken != null) {
headers.set("Authorization", "Bearer " + bearerToken);
}
RequestInit init = RequestInit.create();
init.setMethod(method.name());
init.setHeaders(headers);
init.setMode("cors");
init.setCredentials("include");
return init;
}
// ------------------------------------------------------ promise handlers
ThenOnFulfilledCallbackFn processResponse() {
return response -> {
if (!response.ok && response.status != 500) {
return Promise.reject(ResponseStatus.fromStatusCode(response.status).statusText());
}
String contentType = response.headers.get(CONTENT_TYPE.header());
if (!contentType.startsWith(APPLICATION_DMR_ENCODED)) {
return Promise.reject(PARSE_ERROR + contentType);
}
return response.text();
};
}
ThenOnFulfilledCallbackFn processText(Operation operation, PayloadProcessor payloadProcessor,
boolean recordOperation) {
return text -> {
if (recordOperation) {
recordOperation(operation);
}
logger.trace("DMR operation: {}", operation);
ModelNode payload = payloadProcessor.processPayload(POST, APPLICATION_DMR_ENCODED, text);
if (!payload.isFailure()) {
if (environment.isStandalone()) {
if (payload.hasDefined(RESPONSE_HEADERS)) {
Header[] headers = new Header[] { new Header(payload.get(RESPONSE_HEADERS)) };
for (ResponseHeadersProcessor processor : responseHeadersProcessors.processors()) {
processor.process(headers);
}
}
} else {
if (payload.hasDefined(SERVER_GROUPS)) {
Header[] headers = collectHeaders(payload.get(SERVER_GROUPS));
if (headers.length != 0) {
for (ResponseHeadersProcessor processor : responseHeadersProcessors.processors()) {
processor.process(headers);
}
}
}
}
return Promise.resolve(payload);
} else {
return Promise.reject(payload.getFailureDescription());
}
};
}
private Header[] collectHeaders(ModelNode serverGroups) {
List headers = new ArrayList<>();
for (Property serverGroup : serverGroups.asPropertyList()) {
ModelNode serverGroupValue = serverGroup.getValue();
if (serverGroupValue.hasDefined(HOST)) {
List hosts = serverGroupValue.get(HOST).asPropertyList();
for (Property host : hosts) {
ModelNode hostValue = host.getValue();
List servers = hostValue.asPropertyList();
for (Property server : servers) {
ModelNode serverResponse = server.getValue().get(RESPONSE);
if (serverResponse.hasDefined(RESPONSE_HEADERS)) {
headers.add(new Header(serverGroup.getName(), host.getName(), server.getName(),
serverResponse.get(RESPONSE_HEADERS)));
}
}
}
}
}
return headers.toArray(new Header[0]);
}
// ------------------------------------------------------ error handling
CatchOnRejectedCallbackFn rejectWithError() {
return error -> {
logger.error("Dispatcher error: {}", error);
return Promise.reject(error);
};
}
// ------------------------------------------------------ macro recording
@Override
public void onRecording(RecordingEvent event) {
if (event.getAction() == Action.START && macros.current() == null) {
MacroOptions options = event.getOptions();
String description = options.hasDefined(DESCRIPTION) ? options.get(DESCRIPTION).asString() : null;
macros.startRecording(new Macro(options.getName(), description), options);
} else if (event.getAction() == Action.STOP && macros.current() != null) {
Macro finished = macros.current();
MacroOptions options = macros.currentOptions();
macros.stopRecording();
eventBus.fireEvent(new MacroFinishedEvent(finished, options));
}
}
private void recordOperation(Operation operation) {
if (macros.current() != null && !macros.current().isSealed()) {
if (macros.currentOptions().omitReadOperations() && readOnlyOperation(operation)) {
return;
}
if (operation instanceof Composite && ((Composite) operation).size() == 1) {
// TODO Is it ok to record a composite with one step as a single op?
macros.current().addOperation(((Composite) operation).iterator().next());
} else {
macros.current().addOperation(operation);
}
eventBus.fireEvent(new MacroOperationEvent(macros.current(), operation));
}
}
private boolean readOnlyOperation(Operation operation) {
if (operation instanceof Composite) {
Composite composite = (Composite) operation;
for (Operation op : composite) {
if (!READ_ONLY.test(op)) {
return false;
}
}
return true;
} else {
return READ_ONLY.test(operation);
}
}
// ------------------------------------------------------ Keycloak
private String token() {
Keycloak keycloak = KeycloakSingleton.instance();
if (keycloak != null) {
return keycloak.token;
}
return null;
}
// ------------------------------------------------------ inner classes
@FunctionalInterface
public interface ErrorCallback {
void onError(Operation operation, String error);
}
public enum HttpMethod {
GET, POST
}
public enum ResponseStatus {
_0(0, "The response for could not be processed."),
_401(401, "Unauthorized."),
_403(403, "Forbidden."),
_404(404, "Management interface not found."),
_500(500, "Internal Server Error."),
_503(503, "Service temporarily unavailable. Is the server still starting?"),
UNKNOWN(-1, "Unexpected status code.");
public static ResponseStatus fromStatusText(String statusText) {
for (ResponseStatus responseStatus : ResponseStatus.values()) {
if (responseStatus.statusText.equals(statusText)) {
return responseStatus;
}
}
return UNKNOWN;
}
public static ResponseStatus fromStatusCode(int statusCode) {
for (ResponseStatus responseStatus : ResponseStatus.values()) {
if (responseStatus.statusCode == statusCode) {
return responseStatus;
}
}
return UNKNOWN;
}
private final int statusCode;
private final String statusText;
ResponseStatus(final int statusCode, final String statusText) {
this.statusCode = statusCode;
this.statusText = statusText;
}
public boolean notAllowed() {
return statusCode == _401.statusCode || statusCode == _403.statusCode;
}
public int statusCode() {
return statusCode;
}
public String statusText() {
return statusText;
}
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy