1
0
mirror of https://github.com/octoleo/plantuml.git synced 2024-06-09 11:42:43 +00:00

Merge pull request #755 from arittner/aritner-loadJSON

Introduction of an internal function %loadJSON to load JSON data
This commit is contained in:
arnaudroques 2021-11-10 11:40:25 +01:00 committed by GitHub
commit dec2eae422
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 328 additions and 0 deletions

View File

@ -106,6 +106,7 @@ import net.sourceforge.plantuml.tim.stdlib.InvokeProcedure;
import net.sourceforge.plantuml.tim.stdlib.IsDark;
import net.sourceforge.plantuml.tim.stdlib.IsLight;
import net.sourceforge.plantuml.tim.stdlib.Lighten;
import net.sourceforge.plantuml.tim.stdlib.LoadJson;
import net.sourceforge.plantuml.tim.stdlib.LogicalNot;
import net.sourceforge.plantuml.tim.stdlib.Lower;
import net.sourceforge.plantuml.tim.stdlib.Newline;
@ -177,6 +178,7 @@ public class TContext {
functionsSet.addFunction(new Hex2dec());
functionsSet.addFunction(new Dec2hex());
functionsSet.addFunction(new HslColor());
functionsSet.addFunction(new LoadJson());
functionsSet.addFunction(new Chr());
functionsSet.addFunction(new Size());
// %standard_exists_function

View File

@ -0,0 +1,186 @@
/* ========================================================================
* PlantUML : a free UML diagram generator
* ========================================================================
*
* (C) Copyright 2009-2021, Arnaud Roques
*
* Project Info: http://plantuml.com
*
* If you like this project or if you find it useful, you can support us at:
*
* http://plantuml.com/patreon (only 1$ per month!)
* http://plantuml.com/paypal
*
* This file is part of PlantUML.
*
* PlantUML 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 3 of the License, or
* (at your option) any later version.
*
* PlantUML 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 library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,
* USA.
*
*
* Original Author: Arnaud Roques
*
*/
package net.sourceforge.plantuml.tim.stdlib;
import net.sourceforge.plantuml.FileSystem;
import net.sourceforge.plantuml.FileUtils;
import net.sourceforge.plantuml.LineLocation;
import net.sourceforge.plantuml.json.Json;
import net.sourceforge.plantuml.json.JsonValue;
import net.sourceforge.plantuml.json.ParseException;
import net.sourceforge.plantuml.security.SFile;
import net.sourceforge.plantuml.security.SURL;
import net.sourceforge.plantuml.tim.*;
import net.sourceforge.plantuml.tim.expression.TValue;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* Loads JSON data from file or URL source.
* <p>
* Supports three parameters for datasource, default JSON value and charset. The datasource will be checked against
* the security rules.
* <p>
* Examples:</br>
* <pre>
* @ startuml
* ' loads a local file
* !$JSON_LOCAL_RELATIVE=%loadJSON("file.json")
*
* ' loads a local file from an absolute file path
* !$JSON_LOCAL_ABS=%loadJSON("c:/loaded/data/file.json")
*
* ' tries to load a local file and returns an empty JSON
* !$JSON_LOCAL_REL_EMPTY=%loadJSON("file-not-existing.json")
*
* ' tries to load a local file and returns an default JSON
* !$DEF_JSON={"status":"No data found"}
* !$JSON_LOCAL_REL_DEF=%loadJSON("file-not-existing.json", $DEF_JSON)
*
* ' loads a local file with a specific charset (default is UTF-8)
* !$JSON_LOCAL_RELATIVE_CHARSET=%loadJSON("file.json", "{}", "iso-8859-1")
*
* ' loads a remote JSON from an endpoint (and default, if not reachable)
* !$STATUS_NO_CONNECTION={"status": "No connection"}
* !$JSON_REMOTE_DEF=%loadJSON("https://localhost:7778/management/health", $STATUS_NO_CONNECTION)
* status -> $JSON_REMOTE_DEF.status
* @ enduml
* </pre>
* @author Aljoscha Rittner
*/
public class LoadJson extends SimpleReturnFunction {
private static final String VALUE_CHARSET_DEFAULT = "UTF-8";
private static final String VALUE_DEFAULT_DEFAULT = "{}";
public TFunctionSignature getSignature() {
return new TFunctionSignature("%loadJSON", 3 );
}
public boolean canCover(int nbArg, Set<String> namedArgument) {
return nbArg == 1 || nbArg == 2 || nbArg == 3;
}
public TValue executeReturnFunction(TContext context, TMemory memory, LineLocation location, List<TValue> values,
Map<String, TValue> named) throws EaterException, EaterExceptionLocated {
String path = values.get(0).toString();
try {
String data = loadStringData ( path, getCharset (values) );
if ( data == null ) {
data = getDefaultJson(values);
}
JsonValue jsonValue = Json.parse(data);
return TValue.fromJson(jsonValue);
} catch (ParseException pe) {
pe.printStackTrace ();
throw EaterException.unlocated ( "JSON parse issue in source "
+ path + " on location " + pe.getLocation () );
} catch (UnsupportedEncodingException e) {
e.printStackTrace ();
throw EaterException.unlocated ( "JSON encoding issue in source "
+ path + ": " + e.getMessage () );
}
}
/**
* Returns the JSON default, if the data source contains no data.
* @param values value parameters
* @return the defined default JSON or {@code "{}"}
*/
private String getDefaultJson(List<TValue> values) {
if (values.size() > 1 ) {
return values.get(1).toString ();
}
return VALUE_DEFAULT_DEFAULT;
}
/**
* Returns the charset name (if set)
* @param values value parameters
* @return defined charset or {@code "UTF-8"}
*/
private String getCharset(List<TValue> values) {
if ( values.size() == 3) {
return values.get (2).toString ();
}
return VALUE_CHARSET_DEFAULT;
}
/**
* Loads String data from a data source {@code path} (file or URL) and expects the data encoded in {@code charset}.
* @param path path to data source (http(s)-URL or file).
* @param charset character set to encode the string data
* @return the decoded String from the data source
* @throws EaterException if something went wrong on reading data
*/
private String loadStringData(String path, String charset) throws EaterException, UnsupportedEncodingException {
byte[] byteData = null;
if (path.startsWith("http://") || path.startsWith("https://")) {
final SURL url = SURL.create(path);
if (url == null) {
throw EaterException.located("load JSON: Invalid URL " + path);
}
byteData = url.getBytes();
if (byteData != null && byteData.length == 0) {
// no length, no data (we want the default)
byteData = null;
}
} else {
try {
SFile file = FileSystem.getInstance().getFile(path);
if (file != null && file.exists() && file.canRead() && !file.isDirectory()) {
ByteArrayOutputStream out = new ByteArrayOutputStream(1024 * 8);
FileUtils.copyToStream(file, out);
byteData = out.toByteArray();
}
} catch (IOException e) {
e.printStackTrace();
throw EaterException.located("load JSON: Cannot read file "
+ path + ". " + e.getMessage());
}
}
if (byteData != null) {
return new String(byteData, charset);
}
return null;
}
}

View File

@ -0,0 +1,140 @@
package net.sourceforge.plantuml;
import net.sourceforge.plantuml.security.SFile;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.PrintStream;
import java.nio.file.Path;
import java.util.Collections;
import java.util.List;
import static java.nio.charset.StandardCharsets.UTF_8;
import static net.sourceforge.plantuml.test.TestUtils.writeUtf8File;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.util.Lists.newArrayList;
/**
* Tests the load of a JSON file.
* <p>
* Current limitation: Only local file tests, not tests to a http rest endpoint
*/
class LoadJsonTest {
private static final String[] COMMON_OPTIONS = {"-tutxt"};
private static final String JSON = "{\"jsonTestKey\": \"exampleValue\"}";
private static final String DEF_JSON = "{\"jsonTestKey\": \"exampleDefaultValue\"}";
// ************ Test DSL data
private static final String DIAGRAM = "" +
"@startuml\n" +
"!$JSON_DATA=%loadJSON(test.json)\n" +
// title should have the value from the JSON file
"title $JSON_DATA.jsonTestKey\n" +
"a -> b\n" +
"@enduml\n";
private static final String DIAGRAM_DEF = "" +
"@startuml\n" +
"!$DEF_JSON=" + DEF_JSON + "\n" +
"!$JSON_DATA=%loadJSON(\"test-notfound.json\", $DEF_JSON)\n" +
// title should have the value from the default (because the file doesn't exist)
"title $JSON_DATA.jsonTestKey\n" +
"a -> b\n" +
"@enduml\n";
private static final String DIAGRAM_DEF_EMPTY = "" +
"@startuml\n" +
"!$JSON_DATA=%loadJSON(\"test-notfound.json\")\n" +
// JSON_DATA is defined, but empty (loadJSON default). So, title contains only "xx yy".
"title xx $JSON_DATA.jsonTestKey yy\n" +
"a -> b\n" +
"@enduml\n";
@TempDir
Path tempDir;
/**
* Resets the current directory.
*/
@AfterAll
static void cleanUp() {
FileSystem.getInstance().reset();
}
/**
* Prepares test JSON file and sets the current directory for each test.
*
* @throws Exception hopefully not
*/
@BeforeEach
public void beforeEach() throws Exception {
writeUtf8File(tempDir.resolve("test.json"), JSON);
FileSystem.getInstance().setCurrentDir(new SFile(tempDir.toFile().getAbsolutePath()));
}
/**
* Tests, if the loadJSON is loading the JSON file from test tmp folder.
*
* @throws Exception if something went wrong in this test
*/
@Test
void testLoadJsonSimple() throws Exception {
String rendered = render(DIAGRAM);
assertThat(rendered).doesNotContain("syntax").contains("exampleValue");
}
/**
* Tests, if the loadJSON is using the default JSON given as parameter.
*
* @throws Exception if something went wrong in this test
*/
@Test
void testLoadJsonNotFoundWithDefaultParameter() throws Exception {
String rendered = render(DIAGRAM_DEF);
assertThat(rendered).doesNotContain("syntax").contains("exampleDefaultValue");
}
/**
* Tests, if the loadJSON is using the default JSON.
*
* @throws Exception if something went wrong in this test
*/
@Test
void testLoadJsonNotFoundWithDefaultEmpty() throws Exception {
String rendered = render(DIAGRAM_DEF_EMPTY);
assertThat(rendered).doesNotContain("syntax").contains("xx yy");
}
private String[] optionArray(String... extraOptions) {
final List<String> list = newArrayList(COMMON_OPTIONS);
Collections.addAll(list, extraOptions);
return list.toArray(new String[0]);
}
private String render(String diagram, String... extraOptions) throws Exception {
final Option option = new Option(optionArray(extraOptions));
final ByteArrayInputStream bais = new ByteArrayInputStream(diagram.getBytes(UTF_8));
final ByteArrayOutputStream baos = new ByteArrayOutputStream();
final Pipe pipe = new Pipe(option, new PrintStream(baos), bais, option.getCharset());
pipe.managePipe(ErrorStatus.init());
return new String(baos.toByteArray(), UTF_8);
}
}