/* * Copyright 2016 Red Hat, Inc. and/or its affiliates * and other contributors as indicated by the @author tags. * * 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 org.keycloak.client.admin.cli.commands; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.ObjectNode; import org.jboss.aesh.console.command.CommandException; import org.jboss.aesh.console.command.CommandResult; import org.jboss.aesh.console.command.invocation.CommandInvocation; import org.keycloak.client.admin.cli.common.AttributeOperation; import org.keycloak.client.admin.cli.common.CmdStdinContext; import org.keycloak.client.admin.cli.config.ConfigData; import org.keycloak.client.admin.cli.util.AccessibleBufferOutputStream; import org.keycloak.client.admin.cli.util.Header; import org.keycloak.client.admin.cli.util.Headers; import org.keycloak.client.admin.cli.util.HeadersBody; import org.keycloak.client.admin.cli.util.HeadersBodyStatus; import org.keycloak.client.admin.cli.util.HttpUtil; import org.keycloak.client.admin.cli.util.OutputFormat; import org.keycloak.client.admin.cli.util.ReflectionUtil; import org.keycloak.client.admin.cli.util.ReturnFields; import java.io.BufferedInputStream; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; import java.nio.charset.Charset; import java.util.HashMap; import java.util.Iterator; import java.util.LinkedHashMap; import java.util.LinkedList; import java.util.List; import java.util.Map; import static org.keycloak.client.admin.cli.common.AttributeOperation.Type.DELETE; import static org.keycloak.client.admin.cli.common.AttributeOperation.Type.SET; import static org.keycloak.client.admin.cli.util.AuthUtil.ensureToken; import static org.keycloak.client.admin.cli.util.ConfigUtil.credentialsAvailable; import static org.keycloak.client.admin.cli.util.ConfigUtil.loadConfig; import static org.keycloak.client.admin.cli.util.HttpUtil.checkSuccess; import static org.keycloak.client.admin.cli.util.HttpUtil.composeResourceUrl; import static org.keycloak.client.admin.cli.util.HttpUtil.doGet; import static org.keycloak.client.admin.cli.util.IoUtil.copyStream; import static org.keycloak.client.admin.cli.util.IoUtil.printErr; import static org.keycloak.client.admin.cli.util.IoUtil.printOut; import static org.keycloak.client.admin.cli.util.OutputUtil.MAPPER; import static org.keycloak.client.admin.cli.util.OutputUtil.printAsCsv; import static org.keycloak.client.admin.cli.util.ParseUtil.mergeAttributes; import static org.keycloak.client.admin.cli.util.ParseUtil.parseFileOrStdin; import static org.keycloak.client.admin.cli.util.ParseUtil.parseKeyVal; /** * @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a> */ public abstract class AbstractRequestCmd extends AbstractAuthOptionsCmd { String file; String body; String fields; boolean printHeaders; boolean returnId; boolean outputResult; boolean compressed; boolean unquoted; boolean mergeMode; boolean noMerge; Integer offset; Integer limit; String format = "json"; OutputFormat outputFormat; String httpVerb; Headers headers = new Headers(); List<AttributeOperation> attrs = new LinkedList<>(); Map<String, String> filter = new HashMap<>(); String url = null; @Override public CommandResult execute(CommandInvocation commandInvocation) throws CommandException, InterruptedException { try { initOptions(); if (printHelp()) { return help ? CommandResult.SUCCESS : CommandResult.FAILURE; } processGlobalOptions(); processOptions(commandInvocation); return process(commandInvocation); } catch (IllegalArgumentException e) { throw new IllegalArgumentException(e.getMessage() + suggestHelp(), e); } finally { commandInvocation.stop(); } } abstract void initOptions(); abstract String suggestHelp(); void processOptions(CommandInvocation commandInvocation) { if (args == null || args.isEmpty()) { throw new IllegalArgumentException("URI not specified"); } Iterator<String> it = args.iterator(); while (it.hasNext()) { String option = it.next(); switch (option) { case "-s": case "--set": { if (!it.hasNext()) { throw new IllegalArgumentException("Option " + option + " requires a value"); } String[] keyVal = parseKeyVal(it.next()); attrs.add(new AttributeOperation(SET, keyVal[0], keyVal[1])); break; } case "-d": case "--delete": { attrs.add(new AttributeOperation(DELETE, it.next())); break; } case "-h": case "--header": { requireValue(it, option); String[] keyVal = parseKeyVal(it.next()); headers.add(keyVal[0], keyVal[1]); break; } case "-q": case "--query": { if (!it.hasNext()) { throw new IllegalArgumentException("Option " + option + " requires a value"); } String arg = it.next(); String[] keyVal; if (arg.indexOf("=") == -1) { keyVal = new String[] {"", arg}; } else { keyVal = parseKeyVal(arg); } filter.put(keyVal[0], keyVal[1]); break; } default: { if (url == null) { url = option; } else { throw new IllegalArgumentException("Invalid option: " + option); } } } } if (url == null) { throw new IllegalArgumentException("Resource URI not specified"); } if (outputResult && returnId) { throw new IllegalArgumentException("Options -o and -i are mutually exclusive"); } try { outputFormat = OutputFormat.valueOf(format.toUpperCase()); } catch (Exception e) { throw new RuntimeException("Unsupported output format: " + format); } if (mergeMode && noMerge) { throw new IllegalArgumentException("Options --merge and --no-merge are mutually exclusive"); } if (body != null && file != null) { throw new IllegalArgumentException("Options --body and --file are mutually exclusive"); } if (file == null && attrs.size() > 0 && !noMerge) { mergeMode = true; } } public CommandResult process(CommandInvocation commandInvocation) throws CommandException, InterruptedException { // see if Content-Type header is explicitly set to non-json value Header ctype = headers.get("content-type"); InputStream content = null; CmdStdinContext<JsonNode> ctx = new CmdStdinContext<>(); if (file != null) { if (ctype != null && !"application/json".equals(ctype.getValue())) { if ("-".equals(file)) { content = System.in; } else { try { content = new BufferedInputStream(new FileInputStream(file)); } catch (FileNotFoundException e) { throw new RuntimeException("File not found: " + file); } } } else { ctx = parseFileOrStdin(file); } } else if (body != null) { content = new ByteArrayInputStream(body.getBytes(Charset.forName("utf-8"))); } ConfigData config = loadConfig(); config = copyWithServerInfo(config); setupTruststore(config, commandInvocation); String auth = null; config = ensureAuthInfo(config, commandInvocation); config = copyWithServerInfo(config); if (credentialsAvailable(config)) { auth = ensureToken(config); } auth = auth != null ? "Bearer " + auth : null; if (auth != null) { headers.addIfMissing("Authorization", auth); } final String server = config.getServerUrl(); final String realm = getTargetRealm(config); final String adminRoot = adminRestRoot != null ? adminRestRoot : composeAdminRoot(server); String resourceUrl = composeResourceUrl(adminRoot, realm, url); String typeName = extractTypeNameFromUri(resourceUrl); if (filter.size() > 0) { resourceUrl = HttpUtil.addQueryParamsToUri(resourceUrl, filter); } headers.addIfMissing("Accept", "application/json"); if (isUpdate() && mergeMode) { ObjectNode result; HeadersBodyStatus response; try { response = HttpUtil.doGet(resourceUrl, new HeadersBody(headers)); checkSuccess(resourceUrl, response); ByteArrayOutputStream buffer = new ByteArrayOutputStream(); copyStream(response.getBody(), buffer); result = MAPPER.readValue(buffer.toByteArray(), ObjectNode.class); } catch (IOException e) { throw new RuntimeException("HTTP request error: " + e.getMessage(), e); } CmdStdinContext<JsonNode> ctxremote = new CmdStdinContext<>(); ctxremote.setResult(result); // merge local representation over remote one if (ctx.getResult() != null) { ReflectionUtil.merge(ctx.getResult(), (ObjectNode) ctxremote.getResult()); } ctx = ctxremote; } if (attrs.size() > 0) { if (content != null) { throw new RuntimeException("Can't set attributes on content of type other than application/json"); } ctx = mergeAttributes(ctx, MAPPER.createObjectNode(), attrs); } if (content == null && ctx.getContent() != null) { content = new ByteArrayInputStream(ctx.getContent().getBytes(Charset.forName("utf-8"))); } ReturnFields returnFields = null; if (fields != null) { returnFields = new ReturnFields(fields); } // make sure content type is set if (content != null) { headers.addIfMissing("Content-Type", "application/json"); } LinkedHashMap<String, String> queryParams = new LinkedHashMap<>(); if (offset != null) { queryParams.put("first", String.valueOf(offset)); } if (limit != null) { queryParams.put("max", String.valueOf(limit)); } if (queryParams.size() > 0) { resourceUrl = HttpUtil.addQueryParamsToUri(resourceUrl, queryParams); } HeadersBodyStatus response; try { response = HttpUtil.doRequest(httpVerb, resourceUrl, new HeadersBody(headers, content)); } catch (IOException e) { throw new RuntimeException("HTTP request error: " + e.getMessage(), e); } // output response if (printHeaders) { printOut(response.getStatus()); for (Header header : response.getHeaders()) { printOut(header.getName() + ": " + header.getValue()); } } checkSuccess(resourceUrl, response); AccessibleBufferOutputStream abos = new AccessibleBufferOutputStream(System.out); if (response.getBody() == null) { throw new RuntimeException("Internal error - response body should never be null"); } if (printHeaders) { printOut(""); } Header location = response.getHeaders().get("Location"); String id = location != null ? extractLastComponentOfUri(location.getValue()) : null; if (id != null) { if (returnId) { printOut(id); } else if (!outputResult) { printErr("Created new " + typeName + " with id '" + id + "'"); } } if (outputResult) { if (isCreateOrUpdate() && (response.getStatusCode() == 204 || id != null)) { // get object for id headers = new Headers(); if (auth != null) { headers.add("Authorization", auth); } try { String fetchUrl = id != null ? (resourceUrl + "/" + id) : resourceUrl; response = doGet(fetchUrl, new HeadersBody(headers)); } catch (IOException e) { throw new RuntimeException("HTTP request error: " + e.getMessage(), e); } } Header contentType = response.getHeaders().get("content-type"); boolean canPrettyPrint = contentType != null && contentType.getValue().equals("application/json"); boolean pretty = !compressed; if (canPrettyPrint && (pretty || returnFields != null)) { ByteArrayOutputStream buffer = new ByteArrayOutputStream(); copyStream(response.getBody(), buffer); try { JsonNode rootNode = MAPPER.readValue(buffer.toByteArray(), JsonNode.class); if (returnFields != null) { rootNode = applyFieldFilter(MAPPER, rootNode, returnFields); } if (outputFormat == OutputFormat.JSON) { // now pretty print it to output MAPPER.writeValue(abos, rootNode); } else { printAsCsv(rootNode, returnFields, unquoted); } } catch (Exception ignored) { copyStream(new ByteArrayInputStream(buffer.toByteArray()), abos); } } else { copyStream(response.getBody(), abos); } } int lastByte = abos.getLastByte(); if (lastByte != -1 && lastByte != 13 && lastByte != 10) { printErr(""); } return CommandResult.SUCCESS; } private boolean isUpdate() { return "put".equals(httpVerb); } private boolean isCreateOrUpdate() { return "post".equals(httpVerb) || "put".equals(httpVerb); } }