Commit fb1357cb authored by Romain Bioteau's avatar Romain Bioteau Committed by benjaminParisel

feat(formGenerator) improve formOutput expression (#2706)

* when generating a javascript expression in `edit` mode, the contract
and the data are mapped for each expected input instead of just passing
the data content to the root input.
This way only the expected content is send on submit avoiding
performance and security issues
* add comment in generated script
* make the `formOutput` script more concise (no more `output` variable)

Closes BST-239
parent e3389813
......@@ -22,6 +22,7 @@ import java.io.File;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;
import org.bonitasoft.web.designer.experimental.mapping.data.FormInputData;
import org.bonitasoft.web.designer.experimental.parametrizedWidget.ParametrizedWidgetFactory;
......@@ -35,6 +36,7 @@ import org.bonitasoft.web.designer.model.contract.NodeContractInput;
public class ContractInputDataHandler {
public static final String PERSISTENCEID_INPUT_NAME = "persistenceId_string";
public static final String ITERATOR_NAME = "it";
private ContractInput input;
public ContractInputDataHandler(ContractInput input) {
......@@ -160,21 +162,25 @@ public class ContractInputDataHandler {
? ((AbstractContractInput) input).getDataReference().getType()
: null;
}
public String getPath() {
public String getDataPath() {
List<String> pathNames = newArrayList();
pathNames.add(input.getName());
ContractInput pInput = input.getParent();
while (pInput != null) {
if (pInput.isMultiple()) {
pathNames.add(getRefName() == null ? getInputName() : getRefName() );
ContractInputDataHandler parent = getParent();
while (parent != null) {
if(parent.isMultiple()) {
pathNames.add(ITERATOR_NAME);
break;
}
pathNames.add(pInput.getName());
pInput = pInput.getParent();
pathNames.add(parent.getRefName() == null ? parent.getInputName() : parent.getRefName());
parent = parent.getParent();
}
if (pathNames.isEmpty()) {
return null;
}
if(parent == null) {
pathNames.add("$data");
}
return on(".").join(reverse(pathNames));
}
......@@ -193,4 +199,11 @@ public class ContractInputDataHandler {
public boolean isDocument() {
return Objects.equals(File.class.getName(), input.getType());
}
public List<ContractInputDataHandler> getNonReadOnlyChildren() {
return input.getInput().stream()
.filter(input -> !input.isReadOnly())
.map(ContractInputDataHandler::new)
.collect(Collectors.toList());
}
}
......@@ -14,6 +14,8 @@
*/
package org.bonitasoft.web.designer.experimental.mapping.data;
import static org.bonitasoft.web.designer.experimental.mapping.data.StringUtil.indent;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
......@@ -69,39 +71,36 @@ public class FormOutputVisitor implements ContractInputVisitor {
.distinct()
.collect(Collectors.joining(" && "));
boolean addCheckDependencies = !expressionDependencies.isEmpty();
if(addCheckDependencies) {
if (addCheckDependencies) {
outputExpressionBuffer.append("if( ");
outputExpressionBuffer.append(expressionDependencies);
outputExpressionBuffer.append(" ){\n");
}
outputExpressionBuffer.append(indent("var output = {\n",addCheckDependencies ? 1 : 0));
outputExpressionBuffer.append(properties.stream()
.filter(OutputProperty::isRootProperty)
.map(Object::toString)
.map(line -> indent(line, addCheckDependencies ? 1 : 0))
.collect(Collectors.joining(",\n")));
outputExpressionBuffer.append("\n");
outputExpressionBuffer.append(indent("};\n",addCheckDependencies ? 1 : 0));
if (properties.stream().anyMatch(OutputProperty::isReference)) {
outputExpressionBuffer.append(
indent("//attach lazy references variables to parent variables\n", addCheckDependencies ? 1 : 0));
}
properties.stream()
.filter(OutputProperty::isReference)
.map(Object::toString)
.map(line -> indent(line, addCheckDependencies ? 1 : 0))
.forEach(referencedProperty -> outputExpressionBuffer.append(referencedProperty + "\n"));
outputExpressionBuffer.append(indent("return output;",addCheckDependencies ? 1 : 0));
if(addCheckDependencies) {
outputExpressionBuffer.append("\n}");
}
return outputExpressionBuffer.toString();
}
outputExpressionBuffer.append(indent("return {\n", addCheckDependencies ? 1 : 0));
outputExpressionBuffer.append(properties.stream()
.filter(OutputProperty::isRootProperty)
.map(Object::toString)
.map(expression -> indent(expression, addCheckDependencies ? 1 : 0))
.collect(Collectors.joining(",\n")));
outputExpressionBuffer.append("\n");
outputExpressionBuffer.append(indent("}", addCheckDependencies ? 1 : 0));
private String indent(String value,int size) {
for(int i = 0 ; i < size ; i++) {
value = "\t" + value;
if (addCheckDependencies) {
outputExpressionBuffer.append("\n");
outputExpressionBuffer.append("}");
}
return value;
return outputExpressionBuffer.toString();
}
}
......@@ -15,32 +15,33 @@
package org.bonitasoft.web.designer.experimental.mapping.data;
import static java.lang.String.format;
import static org.bonitasoft.web.designer.experimental.mapping.data.StringUtil.indent;
import org.bonitasoft.web.designer.experimental.mapping.ContractInputDataHandler;
public class OutputProperty {
private ContractInputDataHandler dataHandler;
public OutputProperty(ContractInputDataHandler dataHandler) {
this.dataHandler = dataHandler;
}
@Override
public String toString() {
if(dataHandler.hasLazyDataRef()) {
return format("output.%s = $data.%s;", dataHandler.getPath(), dataHandler.inputValue());
}else if (dataHandler.hasDataReference() && !dataHandler.isDocumentEdition()){
return format("\t'%s': $data.%s", dataHandler.getInputName(), dataHandler.getRefName());
}else if(dataHandler.isDocumentEdition() ) {
if(dataHandler.isMultiple()) {
return mapToFileInputValues(dataHandler);
}else {
return mapToFileInputValue(dataHandler);
}
}else {
return format("\t'%s': $data.formInput.%s", dataHandler.getInputName(), dataHandler.getInputName());
}
if (dataHandler.hasLazyDataRef()) {
return format("%s = $data.%s;", dataHandler.getDataPath(), dataHandler.inputValue());
} else if (dataHandler.hasDataReference() && !dataHandler.isDocumentEdition()) {
return mapDataToContractExpression(dataHandler);
} else if (dataHandler.isDocumentEdition()) {
if (dataHandler.isMultiple()) {
return mapToFileInputValues(dataHandler);
} else {
return mapToFileInputValue(dataHandler);
}
} else {
return format("\t%s: $data.formInput.%s", dataHandler.getInputName(), dataHandler.getInputName());
}
}
public boolean isRootProperty() {
......@@ -48,41 +49,101 @@ public class OutputProperty {
}
public boolean isReference() {
return dataHandler.hasLazyDataRef() || dataHandler.isDocumentEdition();
return dataHandler.hasLazyDataRef();
}
public String getDependency() {
if(dataHandler.isDocumentEdition()) {
if (dataHandler.isDocumentEdition()) {
return "$data.context";
}
if(dataHandler.hasLazyDataRef()) {
if (dataHandler.hasLazyDataRef()) {
return format("$data.%s", dataHandler.inputValue());
}
return dataHandler.hasDataReference() ? format("$data.%s", dataHandler.getRefName()) : null;
}
private String mapDataToContractExpression(ContractInputDataHandler handler) {
StringBuffer sb = new StringBuffer();
sb.append(String.format("\t//map %s variable to expected task contract input\n", handler.getRefName()));
sb.append(String.format("\t%s: ", handler.getInputName()));
mapDataToContract(handler, sb, 2);
return sb.toString();
}
private void mapDataToContract(ContractInputDataHandler handler, StringBuffer sb, int indentSize) {
if (handler.isMultiple()) {
sb.append(String.format("%s.map( %s => ({\n", handler.getDataPath(),ContractInputDataHandler.ITERATOR_NAME));
for (int i = 0; i < handler.getNonReadOnlyChildren().size(); i++) {
sb.append(indent(dataToContractInput(handler.getNonReadOnlyChildren().get(i)), indentSize));
if (i != handler.getNonReadOnlyChildren().size() - 1) {
sb.append(",");
}
sb.append("\n");
}
sb.append(indent("}))",indentSize - 1));
} else {
toSimpleObjectMapping(handler, sb, indentSize);
}
}
private void toSimpleObjectMapping(ContractInputDataHandler handler, StringBuffer sb, int indentSize) {
sb.append("{\n");
for (int i = 0; i < handler.getNonReadOnlyChildren().size(); i++) {
sb.append(indent(dataToContractInput(handler.getNonReadOnlyChildren().get(i)), indentSize));
if (i != handler.getNonReadOnlyChildren().size() - 1) {
sb.append(",");
}
sb.append("\n");
}
sb.append(indent("}", indentSize - 1));
}
private String dataToContractInput(ContractInputDataHandler dataHandler) {
if (!dataHandler.hasDataReference()) {
return String.format("%s: %s", dataHandler.getInputName(), dataHandler.getDataPath());
}
StringBuffer sb = new StringBuffer();
if (dataHandler.isMultiple()) {
sb.append(String.format("%s: %s.map( %s => (", dataHandler.getInputName(), dataHandler.getDataPath(), ContractInputDataHandler.ITERATOR_NAME));
toSimpleObjectMapping(dataHandler, sb, 1);
sb.append("))");
} else {
sb.append(String.format("%s: %s ? ", dataHandler.getInputName(), dataHandler.getDataPath()));
mapDataToContract(dataHandler, sb, 1);
sb.append(" : null");
}
return sb.toString();
}
private String mapToFileInputValues(ContractInputDataHandler handler) {
StringBuffer sb = new StringBuffer();
sb.append(String.format("output.%s = $data.context.%s_ref.map( doc => {\n",handler.getInputName(),handler.getRefName()));
sb.append("\treturn {\n");
sb.append("\t\t'id' : doc.id ? doc.id.toString() : null,\n");
sb.append("\t\t'filename' : doc.newValue && doc.newValue.filename ? doc.newValue.filename : null,\n");
sb.append("\t\t'tempPath' : doc.newValue && doc.newValue.tempPath ? doc.newValue.tempPath : null,\n");
sb.append("\t\t'contentType' : doc.newValue && doc.newValue.contentType ? doc.newValue.contentType : null\n");
sb.append("\t};\n");
sb.append("});");
sb.append(String.format("\t%s: $data.context.%s_ref.map( doc => ({\n", handler.getInputName(),
handler.getRefName()));
sb.append("\t\tid : doc.id ? doc.id.toString() : null,\n");
sb.append("\t\tfilename : doc.newValue && doc.newValue.filename ? doc.newValue.filename : null,\n");
sb.append("\t\ttempPath : doc.newValue && doc.newValue.tempPath ? doc.newValue.tempPath : null,\n");
sb.append("\t\tcontentType : doc.newValue && doc.newValue.contentType ? doc.newValue.contentType : null\n");
sb.append("\t}))");
return sb.toString();
}
private String mapToFileInputValue(ContractInputDataHandler handler) {
StringBuffer sb = new StringBuffer();
sb.append(String.format("output.%s = {\n",handler.getInputName()));
sb.append("\t'id' : $data.context.%s_ref && $data.context.%s_ref.id ? $data.context.%s_ref.id.toString() : null,\n".replaceAll("%s", handler.getRefName()));
sb.append("\t'filename' : $data.context.%s_ref.newValue && $data.context.%s_ref.newValue.filename ? $data.context.%s_ref.newValue.filename : null,\n".replaceAll("%s", handler.getRefName()));
sb.append("\t'tempPath' : $data.context.%s_ref.newValue && $data.context.%s_ref.newValue.tempPath ? $data.context.%s_ref.newValue.tempPath : null,\n".replaceAll("%s", handler.getRefName()));
sb.append("\t'contentType' : $data.context.%s_ref.newValue && $data.context.%s_ref.newValue.contentType ? $data.context.%s_ref.newValue.contentType : null\n".replaceAll("%s", handler.getRefName()));
sb.append("};");
sb.append(String.format("\t%s: {\n", handler.getInputName()));
sb.append(
"\t\\tid: $data.context.%s_ref && $data.context.%s_ref.id ? $data.context.%s_ref.id.toString() : null,\n"
.replaceAll("%s", handler.getRefName()));
sb.append(
"\t\tfilename: $data.context.%s_ref.newValue && $data.context.%s_ref.newValue.filename ? $data.context.%s_ref.newValue.filename : null,\n"
.replaceAll("%s", handler.getRefName()));
sb.append(
"\t\ttempPath: $data.context.%s_ref.newValue && $data.context.%s_ref.newValue.tempPath ? $data.context.%s_ref.newValue.tempPath : null,\n"
.replaceAll("%s", handler.getRefName()));
sb.append(
"\t\tcontentType: $data.context.%s_ref.newValue && $data.context.%s_ref.newValue.contentType ? $data.context.%s_ref.newValue.contentType : null\n"
.replaceAll("%s", handler.getRefName()));
sb.append("\t}");
return sb.toString();
}
}
/**
* Copyright (C) 2015 Bonitasoft S.A.
* Bonitasoft, 32 rue Gustave Eiffel - 38000 Grenoble
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2.0 of the License, or
* (at your option) any later version.
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.bonitasoft.web.designer.experimental.mapping.data;
import java.util.stream.Stream;
public class StringUtil {
public static String indent(String value, int size) {
StringBuffer sb = new StringBuffer();
boolean appendNewLine = value.endsWith("\n");
String[] lines = value.split("\n");
Stream.of(lines).forEach(line -> {
for (int i = 0; i < size; i++) {
line = "\t" + line;
}
sb.append(line);
sb.append("\n");
});
String indentedValue = sb.toString();
return !appendNewLine ? indentedValue.substring(0,indentedValue.length()-1) : indentedValue;
}
}
......@@ -97,7 +97,7 @@ public class ContractToPageMapperTest {
Page page = contractToPageMapper.createFormPage("myPage", aContractWithMultipleInput(), FormScope.TASK);
assertThat(page.getData()).contains(entry("formInput", aJSONData().value(objectMapper.prettyPrint("{\"names\":[]}")).build()));
assertThat(page.getData()).contains(entry("formOutput", anExpressionData().value("var output = {\n\t'names': $data.formInput.names\n};\nreturn output;").build()));
assertThat(page.getData()).contains(entry("formOutput", anExpressionData().value("return {\n\tnames: $data.formInput.names\n}").build()));
}
@Test
......
......@@ -39,10 +39,9 @@ public class FormOutputVisitorTest {
contract.accept(visitor);
assertThat(visitor.toJavascriptExpression()).isEqualTo("var output = {\n" +
"\t'accepted': $data.formInput.accepted\n" +
"};\n" +
"return output;");
assertThat(visitor.toJavascriptExpression()).isEqualTo("return {\n" +
"\taccepted: $data.formInput.accepted\n" +
"}");
}
@Test
......@@ -59,11 +58,10 @@ public class FormOutputVisitorTest {
contract.accept(visitor);
assertThat(visitor.toJavascriptExpression())
.isEqualTo("var output = {\n" +
"\t'person': $data.formInput.person,\n" +
"\t'accepted': $data.formInput.accepted\n" +
"};\n" +
"return output;");
.isEqualTo("return {\n" +
"\tperson: $data.formInput.person,\n" +
"\taccepted: $data.formInput.accepted\n" +
"}");
}
......@@ -76,6 +74,9 @@ public class FormOutputVisitorTest {
.withInput(
aStringContractInput("name"),
aNodeContractInput("details")
.mulitple()
.withDataReference(new BusinessDataReference("details", "org.test.Detail",
RelationType.COMPOSITION, LoadingType.EAGER))
.withInput(anIntegerContractInput("age"))
.build())
.build(),
......@@ -85,11 +86,16 @@ public class FormOutputVisitorTest {
assertThat(visitor.toJavascriptExpression())
.isEqualTo("if( $data.person ){\n" +
"\tvar output = {\n" +
"\t\t'person': $data.person,\n" +
"\t\t'accepted': $data.formInput.accepted\n" +
"\t};\n" +
"\treturn output;\n" +
"\treturn {\n" +
"\t\t//map person variable to expected task contract input\n" +
"\t\tperson: {\n" +
"\t\t\tname: $data.person.name,\n" +
"\t\t\tdetails: $data.person.details.map( it => ({\n" +
"\t\t\t\tage: it.age\n" +
"\t\t\t}))\n" +
"\t\t},\n" +
"\t\taccepted: $data.formInput.accepted\n" +
"\t}\n" +
"}");
}
......@@ -104,7 +110,7 @@ public class FormOutputVisitorTest {
aNodeContractInput("detail")
.withDataReference(new BusinessDataReference("detail", "org.test.Detail",
RelationType.COMPOSITION, LoadingType.LAZY))
.withInput(anIntegerContractInput("age"))
.withInput(anIntegerContractInput("age"),aStringContractInput("carnation"))
.build())
.build(),
aBooleanContractInput("accepted")).build();
......@@ -113,12 +119,54 @@ public class FormOutputVisitorTest {
assertThat(visitor.toJavascriptExpression())
.isEqualTo("if( $data.person && $data.person_detail ){\n" +
"\tvar output = {\n" +
"\t\t'personInput': $data.person,\n" +
"\t\t'accepted': $data.formInput.accepted\n" +
"\t};\n" +
"\toutput.personInput.detail = $data.person_detail;\n"+
"\treturn output;\n" +
"\t//attach lazy references variables to parent variables\n"+
"\t$data.person.detail = $data.person_detail;\n"+
"\treturn {\n" +
"\t\t//map person variable to expected task contract input\n"+
"\t\tpersonInput: {\n" +
"\t\t\tname: $data.person.name,\n" +
"\t\t\tdetail: $data.person.detail ? {\n" +
"\t\t\t\tage: $data.person.detail.age,\n" +
"\t\t\t\tcarnation: $data.person.detail.carnation\n" +
"\t\t\t} : null\n" +
"\t\t},\n" +
"\t\taccepted: $data.formInput.accepted\n" +
"\t}\n" +
"}");
}
@Test
public void should_build_form_output_from_multiple_complex_types_with_data_reference() throws Exception {
Contract contract = aContract().inEditMode().withInput(
aNodeContractInput("persons")
.mulitple()
.withDataReference(new BusinessDataReference("persons", "org.test.Person",
RelationType.COMPOSITION, LoadingType.EAGER))
.withInput(
aStringContractInput("name"),
aNodeContractInput("details")
.mulitple()
.withDataReference(new BusinessDataReference("details", "org.test.Detail",
RelationType.COMPOSITION, LoadingType.EAGER))
.withInput(anIntegerContractInput("age"))
.build())
.build(),
aBooleanContractInput("accepted")).build();
contract.accept(visitor);
assertThat(visitor.toJavascriptExpression())
.isEqualTo("if( $data.persons ){\n" +
"\treturn {\n" +
"\t\t//map persons variable to expected task contract input\n" +
"\t\tpersons: $data.persons.map( it => ({\n" +
"\t\t\tname: it.name,\n" +
"\t\t\tdetails: it.details.map( it => ({\n" +
"\t\t\t\tage: it.age\n" +
"\t\t\t}))\n" +
"\t\t})),\n" +
"\t\taccepted: $data.formInput.accepted\n" +
"\t}\n" +
"}");
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment