Skip to content

Commit

Permalink
CAMEL-20602: Support user properties on Camel JBang bind command
Browse files Browse the repository at this point in the history
- Allow to specify properties on source, sink, step in a Pipe
- User properties can be set as additional command options targeting either the source or the sink or a specific step
  • Loading branch information
christophd committed Mar 22, 2024
1 parent 3cd788c commit 9f70818
Show file tree
Hide file tree
Showing 8 changed files with 863 additions and 78 deletions.
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 = { "-p", "--prop", "--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", "-o" },
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
@@ -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);
}
}
Expand Up @@ -12,5 +12,4 @@ spec:
{{ .Steps }}
sink:
uri: {{ .Sink }}
#properties:
#key: "value"
{{ .SinkProperties }}
Expand Up @@ -5,8 +5,7 @@ metadata:
spec:
source:
uri: {{ .Source }}
#properties:
#key: "value"
{{ .SourceProperties }}
{{ .Steps }}
sink:
ref:
Expand Down
Expand Up @@ -5,10 +5,8 @@ metadata:
spec:
source:
uri: {{ .Source }}
#properties:
#key: "value"
{{ .SourceProperties }}
{{ .Steps }}
sink:
uri: {{ .Sink }}
#properties:
#key: "value"
{{ .SinkProperties }}

0 comments on commit 9f70818

Please sign in to comment.