diff options
author | Sébastien Determe <sebastien.determe@intl.att.com> | 2020-03-09 09:22:24 +0000 |
---|---|---|
committer | Gerrit Code Review <gerrit@onap.org> | 2020-03-09 09:22:24 +0000 |
commit | 5c3a825851bb1a345620c5c4951726c5b94a0341 (patch) | |
tree | a9df35d4aa1de7b46ddd18d4faa75d7a9bfcd7b2 /src/main/java/org | |
parent | 5c6d23852ea949335eb7f85d787eb24ac6dbab2a (diff) | |
parent | 3121b5bee2e70b1f753f9988344d41f3132edf69 (diff) |
Merge "CLAMP-CDS integration to display all CDS actions for blueprint in CL"
Diffstat (limited to 'src/main/java/org')
6 files changed, 414 insertions, 9 deletions
diff --git a/src/main/java/org/onap/clamp/clds/client/CdsServices.java b/src/main/java/org/onap/clamp/clds/client/CdsServices.java new file mode 100644 index 000000000..fe1937ab1 --- /dev/null +++ b/src/main/java/org/onap/clamp/clds/client/CdsServices.java @@ -0,0 +1,171 @@ +/*-
+ * ============LICENSE_START=======================================================
+ * ONAP CLAMP
+ * ================================================================================
+ * Copyright (C) 2020 Huawei Technologies Co., Ltd.
+ * ================================================================================
+ * 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.
+ * ============LICENSE_END=========================================================
+ * ================================================================================
+ *
+ */
+
+package org.onap.clamp.clds.client;
+
+import com.att.eelf.configuration.EELFLogger;
+import com.att.eelf.configuration.EELFManager;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonObject;
+import com.google.gson.JsonParser;
+
+import java.util.Date;
+import java.util.Map;
+
+import org.apache.camel.CamelContext;
+import org.apache.camel.Exchange;
+import org.apache.camel.builder.ExchangeBuilder;
+import org.onap.clamp.clds.model.cds.CdsBpWorkFlowListResponse;
+import org.onap.clamp.clds.util.JsonUtils;
+import org.onap.clamp.clds.util.LoggingUtils;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Component;
+
+/**
+ * This class implements the communication with CDS for the service inventory.
+ */
+@Component
+public class CdsServices {
+
+ @Autowired
+ CamelContext camelContext;
+
+ protected static final EELFLogger logger = EELFManager.getInstance().getLogger(CdsServices.class);
+
+ /**
+ * Constructor.
+ */
+ @Autowired
+ public CdsServices() {
+ }
+
+
+ /**
+ * Query CDS to get blueprint's workflow list.
+ *
+ * @param blueprintName CDS blueprint name
+ * @param blueprintVersion CDS blueprint version
+ * @return CdsBpWorkFlowListResponse CDS blueprint's workflow list
+ */
+ public CdsBpWorkFlowListResponse getBlueprintWorkflowList(String blueprintName, String blueprintVersion) {
+ LoggingUtils.setTargetContext("CDS", "getBlueprintWorkflowList");
+
+ Exchange myCamelExchange = ExchangeBuilder.anExchange(camelContext)
+ .withProperty("blueprintName", blueprintName).withProperty("blueprintVersion", blueprintVersion)
+ .build();
+
+ Exchange exchangeResponse = camelContext.createProducerTemplate()
+ .send("direct:get-blueprint-workflow-list", myCamelExchange);
+
+ if (Integer.valueOf(200).equals(exchangeResponse.getIn().getHeader("CamelHttpResponseCode"))) {
+ String cdsResponse = (String) exchangeResponse.getIn().getBody();
+ logger.info("getBlueprintWorkflowList, response from CDS:" + cdsResponse);
+ LoggingUtils.setResponseContext("0", "Get Blueprint workflow list", this.getClass().getName());
+ Date startTime = new Date();
+ LoggingUtils.setTimeContext(startTime, new Date());
+ return JsonUtils.GSON_JPA_MODEL.fromJson(cdsResponse, CdsBpWorkFlowListResponse.class);
+ }
+ return null;
+ }
+
+ /**
+ * Query CDS to get input properties of workflow.
+ *
+ * @param blueprintName CDS blueprint name
+ * @param blueprintVersion CDS blueprint name
+ * @param workflow CDS blueprint's workflow
+ * @return input properties in json format
+ */
+ public JsonObject getWorkflowInputProperties(String blueprintName, String blueprintVersion,
+ String workflow) {
+ LoggingUtils.setTargetContext("CDS", "getWorkflowInputProperties");
+
+ Exchange myCamelExchange = ExchangeBuilder.anExchange(camelContext)
+ .withBody(getCdsPayloadForWorkFlow(blueprintName, blueprintVersion, workflow))
+ .build();
+
+ Exchange exchangeResponse = camelContext.createProducerTemplate()
+ .send("direct:get-blueprint-workflow-input-properties", myCamelExchange);
+
+ if (Integer.valueOf(200).equals(exchangeResponse.getIn().getHeader("CamelHttpResponseCode"))) {
+ String cdsResponse = (String) exchangeResponse.getIn().getBody();
+ logger.info("getWorkflowInputProperties, response from CDS:" + cdsResponse);
+ LoggingUtils.setResponseContext("0", "Get Blueprint workflow input properties", this.getClass().getName());
+ Date startTime = new Date();
+ LoggingUtils.setTimeContext(startTime, new Date());
+ return parseCdsResponse(cdsResponse);
+ }
+ return null;
+ }
+
+ private JsonObject parseCdsResponse(String response) {
+ JsonObject root = JsonParser.parseString(response).getAsJsonObject();
+ JsonObject inputs = root.getAsJsonObject("workFlowData").getAsJsonObject("inputs");
+ JsonObject dataTypes = root.getAsJsonObject("dataTypes");
+
+ JsonObject workFlowProperties = new JsonObject();
+ workFlowProperties.add("inputs", getInputProperties(inputs, dataTypes));
+ return workFlowProperties;
+ }
+
+ private JsonObject getInputProperties(JsonObject inputs, JsonObject dataTypes) {
+ JsonObject inputObject = new JsonObject();
+ for (Map.Entry<String, JsonElement> entry : inputs.entrySet()) {
+ String key = entry.getKey();
+ JsonObject inputProperty = inputs.getAsJsonObject(key);
+ String type = inputProperty.get("type").getAsString();
+ if (isComplexType(type, dataTypes)) {
+ inputObject.add(key, handleComplexType(type, dataTypes));
+ } else {
+ inputObject.addProperty(key, "");
+ }
+ }
+ return inputObject;
+ }
+
+ private JsonObject handleComplexType(String key, JsonObject dataTypes) {
+ JsonObject properties = dataTypes.get(key).getAsJsonObject().get("properties").getAsJsonObject();
+ return getInputProperties(properties, dataTypes);
+ }
+
+ private boolean isComplexType(String type, JsonObject dataTypes) {
+ return dataTypes.get(type) != null;
+ }
+
+ /**
+ * Creates payload to query CDS to get workflow input properties.
+ *
+ * @param blueprintName CDS blueprint name
+ * @param version CDS blueprint version
+ * @param workflow CDS blueprint workflow
+ * @return returns payload in json format
+ */
+ public String getCdsPayloadForWorkFlow(String blueprintName, String version, String workflow) {
+ JsonObject jsonObject = new JsonObject();
+ jsonObject.addProperty("blueprintName", blueprintName);
+ jsonObject.addProperty("version", version);
+ jsonObject.addProperty("returnContent", "json");
+ jsonObject.addProperty("workflowName", workflow);
+ jsonObject.addProperty("specType", "TOSCA");
+ return jsonObject.toString();
+ }
+}
diff --git a/src/main/java/org/onap/clamp/clds/model/cds/CdsBpWorkFlowListResponse.java b/src/main/java/org/onap/clamp/clds/model/cds/CdsBpWorkFlowListResponse.java new file mode 100644 index 000000000..66025c479 --- /dev/null +++ b/src/main/java/org/onap/clamp/clds/model/cds/CdsBpWorkFlowListResponse.java @@ -0,0 +1,67 @@ +/*- + * ============LICENSE_START======================================================= + * ONAP CLAMP + * ================================================================================ + * Copyright (C) 2020 Huawei Technologies Co., Ltd. + * ================================================================================ + * 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. + * ============LICENSE_END========================================================= + * ================================================================================ + * + */ + +package org.onap.clamp.clds.model.cds; + +import com.google.gson.annotations.Expose; + +import java.util.LinkedList; +import java.util.List; + +/** + * This class maps the CDS response to a pojo. + */ +public class CdsBpWorkFlowListResponse { + + @Expose + private String blueprintName; + + @Expose + private String version; + + @Expose + private List<String> workflows = new LinkedList<String>(); + + public String getBlueprintName() { + return blueprintName; + } + + public void setBlueprintName(String blueprintName) { + this.blueprintName = blueprintName; + } + + public String getVersion() { + return version; + } + + public void setVersion(String version) { + this.version = version; + } + + public List<String> getWorkflows() { + return workflows; + } + + public void setWorkflows(List<String> workflows) { + this.workflows = workflows; + } +} diff --git a/src/main/java/org/onap/clamp/loop/service/CsarServiceInstaller.java b/src/main/java/org/onap/clamp/loop/service/CsarServiceInstaller.java index 277fe004a..889125fee 100644 --- a/src/main/java/org/onap/clamp/loop/service/CsarServiceInstaller.java +++ b/src/main/java/org/onap/clamp/loop/service/CsarServiceInstaller.java @@ -4,6 +4,7 @@ * ================================================================================ * Copyright (C) 2020 AT&T Intellectual Property. All rights * reserved. + * Modifications Copyright (C) 2020 Huawei Technologies Co., Ltd. * ================================================================================ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -29,7 +30,9 @@ import com.google.gson.JsonObject; import java.util.Map.Entry; +import org.onap.clamp.clds.client.CdsServices; import org.onap.clamp.clds.exception.sdc.controller.SdcArtifactInstallerException; +import org.onap.clamp.clds.model.cds.CdsBpWorkFlowListResponse; import org.onap.clamp.clds.sdc.controller.installer.CsarHandler; import org.onap.clamp.clds.util.JsonUtils; import org.onap.sdc.tosca.parser.api.IEntityDetails; @@ -53,9 +56,12 @@ public class CsarServiceInstaller { @Autowired ServiceRepository serviceRepository; + @Autowired + CdsServices cdsServices; + /** * Install the Service from the csar. - * + * * @param csar The Csar Handler * @return The service object */ @@ -77,7 +83,7 @@ public class CsarServiceInstaller { return modelService; } - private static JsonObject createServicePropertiesByType(CsarHandler csar) { + private JsonObject createServicePropertiesByType(CsarHandler csar) { JsonObject resourcesProp = new JsonObject(); // Iterate on all types defined in the tosca lib for (SdcTypes type : SdcTypes.values()) { @@ -86,6 +92,13 @@ public class CsarServiceInstaller { for (NodeTemplate nodeTemplate : csar.getSdcCsarHelper().getServiceNodeTemplateBySdcType(type)) { resourcesPropByType.add(nodeTemplate.getName(), JsonUtils.GSON.toJsonTree(nodeTemplate.getMetaData().getAllProperties())); + // get cds artifact information and save in resources Prop + if (SdcTypes.PNF == type || SdcTypes.VF == type) { + JsonObject controllerProperties = createCdsArtifactProperties(nodeTemplate); + if (controllerProperties != null) { + resourcesPropByType.getAsJsonObject(nodeTemplate.getName()).add("controllerProperties", controllerProperties); + } + } } resourcesProp.add(type.getValue(), resourcesPropByType); } @@ -114,7 +127,7 @@ public class CsarServiceInstaller { /** * Verify whether Service in Csar is deployed. - * + * * @param csar The Csar Handler * @return The flag indicating whether Service is deployed * @throws SdcArtifactInstallerException The SdcArtifactInstallerException @@ -127,4 +140,43 @@ public class CsarServiceInstaller { return alreadyInstalled; } + + /** + * Retrive CDS artifacts information from node template and save in resource object. + * + * @param nodeTemplate node template + * @return Returns CDS artifacts information + */ + private JsonObject createCdsArtifactProperties(NodeTemplate nodeTemplate) { + Object artifactName = nodeTemplate.getPropertyValue("sdnc_model_name"); + Object artifactVersion = nodeTemplate.getPropertyValue("sdnc_model_version"); + if (artifactName != null && artifactVersion != null) { + CdsBpWorkFlowListResponse response = queryCdsToGetWorkFlowList(artifactName.toString(), artifactVersion.toString()); + if (response == null) { + return null; + } + + JsonObject workFlowProps = new JsonObject(); + for (String workFlow : response.getWorkflows()) { + JsonObject inputs = queryCdsToGetWorkFlowInputProperties(response.getBlueprintName(), + response.getVersion(), workFlow); + workFlowProps.add(workFlow, inputs); + } + + JsonObject controllerProperties = new JsonObject(); + controllerProperties.addProperty("sdnc_model_name", artifactName.toString()); + controllerProperties.addProperty("sdnc_model_version", artifactVersion.toString()); + controllerProperties.add("workflows", workFlowProps); + return controllerProperties; + } + return null; + } + + private CdsBpWorkFlowListResponse queryCdsToGetWorkFlowList(String artifactName, String artifactVersion) { + return cdsServices.getBlueprintWorkflowList(artifactName, artifactVersion); + } + + private JsonObject queryCdsToGetWorkFlowInputProperties(String artifactName, String artifactVersion, String workFlow) { + return cdsServices.getWorkflowInputProperties(artifactName, artifactVersion, workFlow); + } } diff --git a/src/main/java/org/onap/clamp/policy/operational/LegacyOperationalPolicy.java b/src/main/java/org/onap/clamp/policy/operational/LegacyOperationalPolicy.java index ff7777f7f..033f2cebb 100644 --- a/src/main/java/org/onap/clamp/policy/operational/LegacyOperationalPolicy.java +++ b/src/main/java/org/onap/clamp/policy/operational/LegacyOperationalPolicy.java @@ -4,6 +4,7 @@ * ================================================================================ * Copyright (C) 2019 AT&T Intellectual Property. All rights * reserved. + * Modifications Copyright (C) 2020 Huawei Technologies Co., Ltd. * ================================================================================ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -46,6 +47,11 @@ import org.yaml.snakeyaml.Yaml; */ public class LegacyOperationalPolicy { + private static final String ACTOR = "actor"; + private static final String RECIPE = "recipe"; + private static final String POLICIES = "policies"; + private static final String PAYLOAD = "payload"; + private LegacyOperationalPolicy() { } @@ -79,7 +85,7 @@ public class LegacyOperationalPolicy { /** * This method rework the payload attribute (yaml) that is normally wrapped in a * string when coming from the UI. - * + * * @param policyJson The operational policy json config * @return The same object reference but modified */ @@ -150,7 +156,7 @@ public class LegacyOperationalPolicy { /** * This method transforms the configuration json to a Yaml format. - * + * * @param operationalPolicyJsonElement The operational policy json config * @return The Yaml as string */ @@ -162,13 +168,13 @@ public class LegacyOperationalPolicy { // Policy can't support { } in the yaml options.setDefaultFlowStyle(DumperOptions.FlowStyle.BLOCK); return (new Yaml(options)).dump(createMap(fulfillPoliciesTreeField( - removeAllQuotes(reworkPayloadAttributes(operationalPolicyJsonElement.getAsJsonObject().deepCopy()))))); + removeAllQuotes(reworkActorAttributes(operationalPolicyJsonElement.getAsJsonObject().deepCopy()))))); } /** * This method load mandatory field in the operational policy configuration * JSON. - * + * * @param configurationsJson The operational policy JSON * @param loop The parent loop object */ @@ -182,4 +188,45 @@ public class LegacyOperationalPolicy { configurationsJson.add("operational_policy", controlLoop); } } + + /** + * This method rework on the actor/recipe and payload attribute. + * + * @param policyJson The operational policy json config + * @return The same object reference but modified + */ + public static JsonElement reworkActorAttributes(JsonElement policyJson) { + for (JsonElement policy : policyJson.getAsJsonObject().get(POLICIES).getAsJsonArray()) { + JsonObject actor = policy.getAsJsonObject().get(ACTOR).getAsJsonObject(); + policy.getAsJsonObject().remove(ACTOR); + String actorStr = actor.getAsJsonObject().get(ACTOR).getAsString(); + policy.getAsJsonObject().addProperty(ACTOR, actorStr); + policy.getAsJsonObject().addProperty(RECIPE, getRecipe(actor)); + + if ("CDS".equalsIgnoreCase(actorStr)) { + addPayloadAttributes(actor.getAsJsonObject(ACTOR).getAsJsonObject(RECIPE), policy); + } else { + addPayloadAttributes(actor, policy); + } + } + return policyJson; + } + + private static void addPayloadAttributes(JsonObject jsonObject, + JsonElement policy) { + JsonElement payloadElem = jsonObject.getAsJsonObject().get(PAYLOAD); + String payloadString = payloadElem != null ? payloadElem.getAsString() : ""; + if (!payloadString.isEmpty()) { + Map<String, String> testMap = new Yaml().load(payloadString); + String json = new GsonBuilder().create().toJson(testMap); + policy.getAsJsonObject().add(PAYLOAD, + new GsonBuilder().create().fromJson(json, JsonElement.class)); + } else { + policy.getAsJsonObject().addProperty(PAYLOAD, ""); + } + } + + private static String getRecipe(JsonObject actor) { + return actor.getAsJsonObject().get("type").getAsString(); + } } diff --git a/src/main/java/org/onap/clamp/policy/operational/OperationalPolicy.java b/src/main/java/org/onap/clamp/policy/operational/OperationalPolicy.java index 82cfcf4ef..b0e8b0c3d 100644 --- a/src/main/java/org/onap/clamp/policy/operational/OperationalPolicy.java +++ b/src/main/java/org/onap/clamp/policy/operational/OperationalPolicy.java @@ -4,6 +4,7 @@ * ================================================================================ * Copyright (C) 2019 AT&T Intellectual Property. All rights * reserved. + * Modifications Copyright (C) 2020 Huawei Technologies Co., Ltd. * ================================================================================ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -220,7 +221,7 @@ public class OperationalPolicy extends Policy implements Serializable { metadata.addProperty("policy-id", this.name); operationalPolicyDetails.add("properties", LegacyOperationalPolicy - .reworkPayloadAttributes(this.getConfigurationsJson().get("operational_policy").deepCopy())); + .reworkActorAttributes(this.getConfigurationsJson().get("operational_policy").deepCopy())); DumperOptions options = new DumperOptions(); options.setIndent(2); diff --git a/src/main/java/org/onap/clamp/policy/operational/OperationalPolicyRepresentationBuilder.java b/src/main/java/org/onap/clamp/policy/operational/OperationalPolicyRepresentationBuilder.java index 1d0d99080..244f4c27a 100644 --- a/src/main/java/org/onap/clamp/policy/operational/OperationalPolicyRepresentationBuilder.java +++ b/src/main/java/org/onap/clamp/policy/operational/OperationalPolicyRepresentationBuilder.java @@ -4,6 +4,7 @@ * ================================================================================ * Copyright (C) 2019 AT&T Intellectual Property. All rights * reserved. + * Modifications Copyright (C) 2020 Huawei Technologies Co., Ltd. * ================================================================================ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -42,7 +43,7 @@ public class OperationalPolicyRepresentationBuilder { * used by ui for rendering. It uses the model (VF and VFModule) defined in the * loop object to do so, so it's dynamic. It also uses the operational policy * schema template defined in the resource folder. - * + * * @param modelJson The loop model json * @return The json representation * @throws JsonSyntaxException If the schema template cannot be parsed @@ -58,6 +59,22 @@ public class OperationalPolicyRepresentationBuilder { .get("operational_policy").getAsJsonObject().get("properties").getAsJsonObject().get("policies") .getAsJsonObject().get("items").getAsJsonObject().get("properties").getAsJsonObject().get("target") .getAsJsonObject().get("anyOf").getAsJsonArray().addAll(createAnyOfArray(modelJson)); + + // update CDS recipe and payload information to schema + JsonArray actors = jsonSchema.get("schema").getAsJsonObject().get("items").getAsJsonObject().get("properties") + .getAsJsonObject().get("configurationsJson").getAsJsonObject().get("properties").getAsJsonObject() + .get("operational_policy").getAsJsonObject().get("properties").getAsJsonObject().get("policies") + .getAsJsonObject().get("items").getAsJsonObject().get("properties").getAsJsonObject().get("actor") + .getAsJsonObject().get("anyOf").getAsJsonArray(); + + for (JsonElement actor : actors) { + if ("CDS".equalsIgnoreCase(actor.getAsJsonObject().get("title").getAsString())) { + actor.getAsJsonObject().get("properties").getAsJsonObject().get("type").getAsJsonObject() + .get("anyOf").getAsJsonArray() + .addAll(createAnyOfArrayForCdsRecipe(modelJson.getResourceDetails())); + } + } + return jsonSchema; } @@ -143,4 +160,54 @@ public class OperationalPolicyRepresentationBuilder { targetOneOfStructure.addAll(createVfModuleSchema(modelJson)); return targetOneOfStructure; } + + private static JsonArray createAnyOfArrayForCdsRecipe(JsonObject resourceDetails) { + JsonArray anyOfStructure = new JsonArray(); + anyOfStructure.addAll(createAnyOfCdsRecipe(resourceDetails.getAsJsonObject("VF"))); + anyOfStructure.addAll(createAnyOfCdsRecipe(resourceDetails.getAsJsonObject("PNF"))); + return anyOfStructure; + } + + private static JsonArray createAnyOfCdsRecipe(JsonObject jsonObject) { + JsonArray schemaArray = new JsonArray(); + for (Entry<String, JsonElement> entry : jsonObject.entrySet()) { + JsonObject controllerProperties = entry.getValue().getAsJsonObject() + .getAsJsonObject("controllerProperties"); + + if (controllerProperties != null) { + JsonObject workflows = controllerProperties.getAsJsonObject("workflows"); + for (Entry<String, JsonElement> workflowsEntry : workflows.entrySet()) { + JsonObject obj = new JsonObject(); + obj.addProperty("title", workflowsEntry.getKey()); + obj.add("properties", createPayloadProperty(workflowsEntry.getValue().getAsJsonObject(), + controllerProperties)); + schemaArray.add(obj); + } + + } + } + return schemaArray; + } + + private static JsonObject createPayloadProperty(JsonObject workFlow, JsonObject controllerProperties) { + JsonObject type = new JsonObject(); + type.addProperty("title", "Payload (YAML)"); + type.addProperty("type", "string"); + type.addProperty("default", createDefaultStringForPayload(workFlow, controllerProperties)); + type.addProperty("format", "textarea"); + JsonObject properties = new JsonObject(); + properties.add("type", type); + return properties; + } + + private static String createDefaultStringForPayload(JsonObject workFlow, JsonObject controllerProperties) { + String artifactName = controllerProperties.get("sdnc_model_name").toString(); + String artifactVersion = controllerProperties.get("sdnc_model_version").toString(); + String data = workFlow.getAsJsonObject("inputs").toString(); + StringBuilder builder = new StringBuilder("'").append("artifact_name : ").append(artifactName).append("\n") + .append("artifact_version : ").append(artifactVersion).append("\n") + .append("mode : async").append("\n") + .append("data : ").append("'").append("\\").append("'").append(data).append("\\").append("'").append("'"); + return builder.toString(); + } } |