Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

CAMEL-20602: Support user properties on Camel JBang bind command #13593

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -20,16 +20,22 @@
import java.io.InputStream;
import java.net.URL;
import java.nio.file.Path;
import java.util.Iterator;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.Stack;

import org.apache.camel.dsl.jbang.core.common.JSonHelper;
import org.apache.camel.dsl.jbang.core.common.YamlHelper;
import org.apache.camel.github.GitHubResourceResolver;
import org.apache.camel.impl.engine.DefaultResourceResolvers;
import org.apache.camel.spi.Resource;
import org.apache.camel.spi.ResourceResolver;
import org.apache.camel.util.FileUtil;
import org.apache.camel.util.IOHelper;
import org.apache.camel.util.StringHelper;
import org.apache.camel.util.URISupport;
import org.apache.camel.util.json.Jsoner;
import org.snakeyaml.engine.v2.api.LoadSettings;
import org.snakeyaml.engine.v2.api.YamlUnicodeReader;
import org.snakeyaml.engine.v2.composer.Composer;
Expand Down Expand Up @@ -64,21 +70,49 @@ public class Bind extends CamelCommand {
required = true)
String sink;

@CommandLine.Option(names = { "--property" },
description = "Adds a pipe property in the form of [source|sink|step-<n>].<key>=<value> where <n> is the step number starting from 1",
arity = "0")
String[] properties;

@CommandLine.Option(names = { "--output" },
defaultValue = "file",
description = "Output format generated by this command (supports: file, yaml or json).")
String output;

public Bind(CamelJBangMain main) {
super(main);
}

@Override
public Integer doCall() throws Exception {

// the pipe source and sink can either be a kamelet or an uri
String in = "kamelet";
String out = "kamelet";

String sourceEndpoint = source;
String sinkEndpoint = sink;
Map<String, Object> sourceUriProperties = new HashMap<>();
Map<String, Object> sinkUriProperties = new HashMap<>();
if (source.contains(":")) {
in = "uri";
if (source.contains("?")) {
sourceEndpoint = StringHelper.before(source, "?");
String query = StringHelper.after(source, "?");
if (query != null) {
sourceUriProperties = URISupport.parseQuery(query, true);
}
}
}
if (sink.contains(":")) {
out = "uri";
if (sink.contains("?")) {
sinkEndpoint = StringHelper.before(sink, "?");
String query = StringHelper.after(sink, "?");
if (query != null) {
sinkUriProperties = URISupport.parseQuery(query, true);
}
}
}

InputStream is = Bind.class.getClassLoader().getResourceAsStream("templates/pipe-" + in + "-" + out + ".yaml.tmpl");
Expand All @@ -88,21 +122,35 @@ public Integer doCall() throws Exception {
String stepsContext = "";
if (steps != null) {
StringBuilder sb = new StringBuilder("\n steps:\n");
for (String step : steps) {
for (int i = 0; i < steps.length; i++) {
String step = steps[i];
boolean uri = step.contains(":");
String text;
String stepType;
Map<String, Object> stepProperties = getProperties("step-%d".formatted(i + 1));
if (uri) {
is = Bind.class.getClassLoader().getResourceAsStream("templates/step-uri.yaml.tmpl");
text = IOHelper.loadText(is);
IOHelper.close(is);
text = text.replaceFirst("\\{\\{ \\.Name }}", step);
stepType = "uri";
if (step.contains("?")) {
String query = StringHelper.after(step, "?");
step = StringHelper.before(step, "?");
if (query != null) {
stepProperties.putAll(URISupport.parseQuery(query, true));
}
}
} else {
is = Bind.class.getClassLoader().getResourceAsStream("templates/step-kamelet.yaml.tmpl");
text = IOHelper.loadText(is);
IOHelper.close(is);
text = text.replaceFirst("\\{\\{ \\.Name }}", step);
String props = kameletProperties(step);
text = text.replaceFirst("\\{\\{ \\.StepProperties }}", props);
stepType = "kamelet";
stepProperties = kameletProperties(step, stepProperties);
}

is = Bind.class.getClassLoader().getResourceAsStream("templates/step-%s.yaml.tmpl".formatted(stepType));
text = IOHelper.loadText(is);
IOHelper.close(is);
text = text.replaceFirst("\\{\\{ \\.Name }}", step);

if (i == steps.length - 1) {
text = text.replaceFirst("\\{\\{ \\.StepProperties }}\n", asEndpointProperties(stepProperties));
} else {
text = text.replaceFirst("\\{\\{ \\.StepProperties }}", asEndpointProperties(stepProperties));
}
sb.append(text);
}
Expand All @@ -111,31 +159,117 @@ public Integer doCall() throws Exception {

String name = FileUtil.onlyName(file, false);
context = context.replaceFirst("\\{\\{ \\.Name }}", name);
context = context.replaceFirst("\\{\\{ \\.Source }}", source);
context = context.replaceFirst("\\{\\{ \\.Sink }}", sink);
context = context.replaceFirst("\\{\\{ \\.Source }}", sourceEndpoint);
context = context.replaceFirst("\\{\\{ \\.Sink }}", sinkEndpoint);
context = context.replaceFirst("\\{\\{ \\.Steps }}", stepsContext);

Map<String, Object> sourceProperties = getProperties("source");
if ("kamelet".equals(in)) {
String props = kameletProperties(source);
context = context.replaceFirst("\\{\\{ \\.SourceProperties }}", props);
sourceProperties = kameletProperties(sourceEndpoint, sourceProperties);
} else {
sourceProperties.putAll(sourceUriProperties);
}
context = context.replaceFirst("\\{\\{ \\.SourceProperties }}\n", asEndpointProperties(sourceProperties));

Map<String, Object> sinkProperties = getProperties("sink");
if ("kamelet".equals(out)) {
String props = kameletProperties(sink);
context = context.replaceFirst("\\{\\{ \\.SinkProperties }}", props);
sinkProperties = kameletProperties(sinkEndpoint, sinkProperties);
} else {
sinkProperties.putAll(sinkUriProperties);
}
context = context.replaceFirst("\\{\\{ \\.SinkProperties }}\n", asEndpointProperties(sinkProperties));

IOHelper.writeText(context, new FileOutputStream(file, false));
switch (output) {
case "file":
if (file.endsWith(".yaml")) {
IOHelper.writeText(context, new FileOutputStream(file, false));
} else if (file.endsWith(".json")) {
IOHelper.writeText(Jsoner.serialize(YamlHelper.yaml().loadAs(context, Map.class)),
new FileOutputStream(file, false));
} else {
IOHelper.writeText(context, new FileOutputStream(file + ".yaml", false));
}
break;
case "yaml":
printer().println(context);
break;
case "json":
printer().println(JSonHelper.prettyPrint(Jsoner.serialize(YamlHelper.yaml().loadAs(context, Map.class)), 2)
.replaceAll("\\\\/", "/"));
break;
default:
printer().printf("Unsupported output format '%s' (supported: file, yaml, json)%n", output);
return -1;
}
return 0;
}

protected String kameletProperties(String kamelet) throws Exception {
/**
* Creates YAML snippet representing the endpoint properties section.
*
* @param props the properties to set as endpoint properties.
* @return
*/
private String asEndpointProperties(Map<String, Object> props) {
StringBuilder sb = new StringBuilder();
if (props.isEmpty()) {
// create a dummy placeholder, so it is easier to add new properties manually
return sb.append("#properties:\n ").append("#key: \"value\"").toString();
}

sb.append("properties:\n");
for (Map.Entry<String, Object> propertyEntry : props.entrySet()) {
sb.append(" ").append(propertyEntry.getKey()).append(": ")
.append(propertyEntry.getValue()).append("\n");
}
return sb.toString().trim();
}

/**
* Extracts properties from given property arguments. Filter properties by given prefix. This way each component in
* pipe (source, sink, step[1-n]) can have its individual properties.
*
* @param keyPrefix
* @return
*/
private Map<String, Object> getProperties(String keyPrefix) {
Map<String, Object> props = new HashMap<>();
if (properties != null) {
for (String propertyExpression : properties) {
if (propertyExpression.startsWith(keyPrefix + ".")) {
String[] keyValue = propertyExpression.split("=", 2);
if (keyValue.length != 2) {
printer().printf(
"property '%s' does not follow format [source|sink|step-<n>].<key>=<value>%n",
propertyExpression);
continue;
}

props.put(keyValue[0].substring(keyPrefix.length() + 1), keyValue[1]);
}
}
}

return props;
}

/**
* Get required properties from Kamelet specification and add those to the given user properties if not already set.
* In case a required property is not present in the provided user properties the value is either set to the example
* coming from the Kamelet specification or to a placeholder value for users to fill in manually. Property values do
* already have quotes when the type is String.
*
* @param kamelet
* @return
* @throws Exception
*/
protected Map<String, Object> kameletProperties(String kamelet, Map<String, Object> userProperties) throws Exception {
Map<String, Object> endpointProperties = new HashMap<>();
InputStream is;
String loc;
Resource res;

// try local disk first before github
// try local disk first before GitHub
ResourceResolver resolver = new DefaultResourceResolvers.FileResolver();
try {
res = resolver.resolve("file:" + kamelet + ".kamelet.yaml");
Expand Down Expand Up @@ -167,26 +301,23 @@ protected String kameletProperties(String kamelet) throws Exception {
if (root != null) {
Set<String> required = asStringSet(nodeAt(root, "/spec/definition/required"));
if (required != null && !required.isEmpty()) {
sb.append("properties:\n");
Iterator<String> it = required.iterator();
while (it.hasNext()) {
String req = it.next();
String type = asText(nodeAt(root, "/spec/definition/properties/" + req + "/type"));
String example = asText(nodeAt(root, "/spec/definition/properties/" + req + "/example"));
sb.append(" ").append(req).append(": ");
if (example != null) {
if ("string".equals(type)) {
sb.append("\"");
}
sb.append(example);
if ("string".equals(type)) {
sb.append("\"");
for (String req : required) {
if (!userProperties.containsKey(req)) {
String type = asText(nodeAt(root, "/spec/definition/properties/" + req + "/type"));
String example = asText(nodeAt(root, "/spec/definition/properties/" + req + "/example"));
StringBuilder vb = new StringBuilder();
if (example != null) {
if ("string".equals(type)) {
vb.append("\"");
}
vb.append(example);
if ("string".equals(type)) {
vb.append("\"");
}
} else {
vb.append("\"value\"");
}
} else {
sb.append("\"value\"");
}
if (it.hasNext()) {
sb.append("\n");
endpointProperties.put(req, vb.toString());
}
}
}
Expand All @@ -199,12 +330,9 @@ protected String kameletProperties(String kamelet) throws Exception {
System.err.println("Kamelet not found on github: " + kamelet);
}

// create a dummy placeholder, so it is easier to add new properties manually
if (sb.isEmpty()) {
sb.append("#properties:\n #key: \"value\"");
}
endpointProperties.putAll(userProperties);

return sb.toString();
return endpointProperties;
}

static class FileConsumer extends ParameterConsumer<Bind> {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
/*
* 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.dsl.jbang.core.common;

import java.util.Collection;
import java.util.Map;

import org.yaml.snakeyaml.DumperOptions;
import org.yaml.snakeyaml.Yaml;
import org.yaml.snakeyaml.introspector.Property;
import org.yaml.snakeyaml.nodes.NodeTuple;
import org.yaml.snakeyaml.nodes.Tag;
import org.yaml.snakeyaml.representer.Representer;

public final class YamlHelper {

private YamlHelper() {
}

/**
* Creates new Yaml instance. The implementation provided by Snakeyaml is not thread-safe. It is better to create a
* fresh instance for every YAML stream.
*
* @return
*/
public static Yaml yaml() {
Representer representer = new Representer(new DumperOptions()) {
@Override
protected NodeTuple representJavaBeanProperty(
Object javaBean, Property property, Object propertyValue, Tag customTag) {
// if value of property is null, ignore it.
if (propertyValue == null || (propertyValue instanceof Collection && ((Collection<?>) propertyValue).isEmpty())
||
(propertyValue instanceof Map && ((Map<?, ?>) propertyValue).isEmpty())) {
return null;
} else {
return super.representJavaBeanProperty(javaBean, property, propertyValue, customTag);
}
}
};
representer.getPropertyUtils().setSkipMissingProperties(true);
return new Yaml(representer);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,5 +12,4 @@ spec:
{{ .Steps }}
sink:
uri: {{ .Sink }}
#properties:
#key: "value"
{{ .SinkProperties }}
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,7 @@ metadata:
spec:
source:
uri: {{ .Source }}
#properties:
#key: "value"
{{ .SourceProperties }}
{{ .Steps }}
sink:
ref:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,8 @@ metadata:
spec:
source:
uri: {{ .Source }}
#properties:
#key: "value"
{{ .SourceProperties }}
{{ .Steps }}
sink:
uri: {{ .Sink }}
#properties:
#key: "value"
{{ .SinkProperties }}