diff options
10 files changed, 374 insertions, 17 deletions
diff --git a/policy-endpoints/src/main/java/org/onap/policy/common/endpoints/http/client/HttpClient.java b/policy-endpoints/src/main/java/org/onap/policy/common/endpoints/http/client/HttpClient.java index 2fe46fb3..ebed1d7e 100644 --- a/policy-endpoints/src/main/java/org/onap/policy/common/endpoints/http/client/HttpClient.java +++ b/policy-endpoints/src/main/java/org/onap/policy/common/endpoints/http/client/HttpClient.java @@ -2,7 +2,7 @@ * ============LICENSE_START======================================================= * ONAP * ================================================================================ - * Copyright (C) 2017-2019 AT&T Intellectual Property. All rights reserved. + * Copyright (C) 2017-2020 AT&T Intellectual Property. All rights reserved. * ================================================================================ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -21,14 +21,16 @@ package org.onap.policy.common.endpoints.http.client; import java.util.Map; - +import java.util.concurrent.Future; import javax.ws.rs.client.Entity; +import javax.ws.rs.client.InvocationCallback; import javax.ws.rs.core.Response; import org.onap.policy.common.capabilities.Startable; /** - * Http Client interface. + * Http Client interface. Supports both synchronous and asynchronous operations. + * */ public interface HttpClient extends Startable { @@ -48,6 +50,24 @@ public interface HttpClient extends Startable { Response get(); /** + * Asynchronous GET request. + * + * @param callback callback to be invoked, asynchronously, when the request completes + * @param path context uri path. + * + * @return future that can be used to cancel the request or await the response + */ + Future<Response> get(InvocationCallback<Response> callback, String path); + + /** + * Asynchronous GET request. + * + * @param callback callback to be invoked, asynchronously, when the request completes + * @return future that can be used to cancel the request or await the response + */ + Future<Response> get(InvocationCallback<Response> callback); + + /** * PUT request. * * @param path context uri path @@ -59,6 +79,19 @@ public interface HttpClient extends Startable { Response put(String path, Entity<?> entity, Map<String, Object> headers); /** + * Asynchronous PUT request. + * + * @param callback callback to be invoked, asynchronously, when the request completes + * @param path context uri path + * @param entity body + * @param headers headers + * + * @return future that can be used to cancel the request or await the response + */ + Future<Response> put(InvocationCallback<Response> callback, String path, Entity<?> entity, + Map<String, Object> headers); + + /** * POST request. * * @param path context uri path @@ -70,6 +103,19 @@ public interface HttpClient extends Startable { Response post(String path, Entity<?> entity, Map<String, Object> headers); /** + * Asynchronous POST request. + * + * @param callback callback to be invoked, asynchronously, when the request completes + * @param path context uri path + * @param entity body + * @param headers headers + * + * @return future that can be used to cancel the request or await the response + */ + Future<Response> post(InvocationCallback<Response> callback, String path, Entity<?> entity, + Map<String, Object> headers); + + /** * DELETE request. * * @param path context uri path @@ -80,6 +126,17 @@ public interface HttpClient extends Startable { Response delete(String path, Map<String, Object> headers); /** + * Asynchronous DELETE request. + * + * @param callback callback to be invoked, asynchronously, when the request completes + * @param path context uri path + * @param headers headers + * + * @return future that can be used to cancel the request or await the response + */ + Future<Response> delete(InvocationCallback<Response> callback, String path, Map<String, Object> headers); + + /** * Retrieve the body from the HTTP transaction. * * @param response response. diff --git a/policy-endpoints/src/main/java/org/onap/policy/common/endpoints/http/client/internal/JerseyClient.java b/policy-endpoints/src/main/java/org/onap/policy/common/endpoints/http/client/internal/JerseyClient.java index 8a717712..1a822ff2 100644 --- a/policy-endpoints/src/main/java/org/onap/policy/common/endpoints/http/client/internal/JerseyClient.java +++ b/policy-endpoints/src/main/java/org/onap/policy/common/endpoints/http/client/internal/JerseyClient.java @@ -2,7 +2,7 @@ * ============LICENSE_START======================================================= * ONAP * ================================================================================ - * Copyright (C) 2017-2019 AT&T Intellectual Property. All rights reserved. + * Copyright (C) 2017-2020 AT&T Intellectual Property. All rights reserved. * Modifications Copyright (C) 2018 Samsung Electronics Co., Ltd. * Modifications Copyright (C) 2019 Nordix Foundation. * ================================================================================ @@ -28,11 +28,13 @@ import java.security.NoSuchAlgorithmException; import java.security.SecureRandom; import java.util.Map; import java.util.Map.Entry; +import java.util.concurrent.Future; import javax.net.ssl.SSLContext; import javax.ws.rs.client.Client; import javax.ws.rs.client.ClientBuilder; import javax.ws.rs.client.Entity; import javax.ws.rs.client.Invocation.Builder; +import javax.ws.rs.client.InvocationCallback; import javax.ws.rs.core.Response; import org.apache.commons.lang3.StringUtils; import org.glassfish.jersey.client.ClientProperties; @@ -169,21 +171,52 @@ public class JerseyClient implements HttpClient { } @Override + public Future<Response> get(InvocationCallback<Response> callback, String path) { + if (!StringUtils.isBlank(path)) { + return this.client.target(this.baseUrl).path(path).request().async().get(callback); + } else { + return this.client.target(this.baseUrl).request().async().get(callback); + } + } + + @Override + public Future<Response> get(InvocationCallback<Response> callback) { + return this.client.target(this.baseUrl).request().async().get(callback); + } + + @Override public Response put(String path, Entity<?> entity, Map<String, Object> headers) { return getBuilder(path, headers).put(entity); } @Override + public Future<Response> put(InvocationCallback<Response> callback, String path, Entity<?> entity, + Map<String, Object> headers) { + return getBuilder(path, headers).async().put(entity, callback); + } + + @Override public Response post(String path, Entity<?> entity, Map<String, Object> headers) { return getBuilder(path, headers).post(entity); } @Override + public Future<Response> post(InvocationCallback<Response> callback, String path, Entity<?> entity, + Map<String, Object> headers) { + return getBuilder(path, headers).async().post(entity, callback); + } + + @Override public Response delete(String path, Map<String, Object> headers) { return getBuilder(path, headers).delete(); } @Override + public Future<Response> delete(InvocationCallback<Response> callback, String path, Map<String, Object> headers) { + return getBuilder(path, headers).async().delete(callback); + } + + @Override public boolean start() { return alive; } diff --git a/policy-endpoints/src/test/java/org/onap/policy/common/endpoints/http/server/test/HttpClientTest.java b/policy-endpoints/src/test/java/org/onap/policy/common/endpoints/http/server/test/HttpClientTest.java index 2aaf1367..d3f94cd0 100644 --- a/policy-endpoints/src/test/java/org/onap/policy/common/endpoints/http/server/test/HttpClientTest.java +++ b/policy-endpoints/src/test/java/org/onap/policy/common/endpoints/http/server/test/HttpClientTest.java @@ -2,7 +2,7 @@ * ============LICENSE_START======================================================= * ONAP * ================================================================================ - * Copyright (C) 2017-2019 AT&T Intellectual Property. All rights reserved. + * Copyright (C) 2017-2020 AT&T Intellectual Property. All rights reserved. * Modifications Copyright (C) 2018 Samsung Electronics Co., Ltd. * ================================================================================ * Licensed under the Apache License, Version 2.0 (the "License"); @@ -23,15 +23,21 @@ package org.onap.policy.common.endpoints.http.server.test; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertSame; import static org.junit.Assert.assertTrue; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Properties; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; import javax.ws.rs.client.Entity; +import javax.ws.rs.client.InvocationCallback; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; +import lombok.Getter; import org.junit.AfterClass; import org.junit.Before; import org.junit.BeforeClass; @@ -194,6 +200,28 @@ public class HttpClientTest { } @Test + public void testHttpGetNoAuthClientAsync() throws Exception { + final HttpClient client = getNoAuthHttpClient(TEST_HTTP_NO_AUTH_CLIENT, false, + 6666); + MyCallback callback = new MyCallback(); + final Response response = client.get(callback, HELLO).get(); + + verifyCallback("testHttpGetNoAuthClientAsync", callback, response); + + final String body = HttpClient.getBody(response, String.class); + + assertEquals(200, response.getStatus()); + assertEquals(HELLO, body); + } + + private void verifyCallback(String testName, MyCallback callback, final Response response) + throws InterruptedException { + assertTrue(testName, callback.await()); + assertNull(testName, callback.getThrowable()); + assertSame(testName, response, callback.getResponse()); + } + + @Test public void testHttpPutNoAuthClient() throws Exception { final HttpClient client = getNoAuthHttpClient(TEST_HTTP_NO_AUTH_CLIENT, false, 6666); @@ -206,6 +234,22 @@ public class HttpClientTest { } @Test + public void testHttpPutNoAuthClientAsync() throws Exception { + final HttpClient client = getNoAuthHttpClient(TEST_HTTP_NO_AUTH_CLIENT, false, 6666); + + Entity<MyEntity> entity = Entity.entity(new MyEntity(MY_VALUE), MediaType.APPLICATION_JSON); + MyCallback callback = new MyCallback(); + final Response response = client.put(callback, HELLO, entity, Collections.emptyMap()).get(); + + verifyCallback("testHttpPutNoAuthClientAsync", callback, response); + + final String body = HttpClient.getBody(response, String.class); + + assertEquals(200, response.getStatus()); + assertEquals(PUT_HELLO, body); + } + + @Test public void testHttpPostNoAuthClient() throws Exception { final HttpClient client = getNoAuthHttpClient(TEST_HTTP_NO_AUTH_CLIENT, false, 6666); @@ -219,6 +263,23 @@ public class HttpClientTest { } @Test + public void testHttpPostNoAuthClientAsync() throws Exception { + final HttpClient client = getNoAuthHttpClient(TEST_HTTP_NO_AUTH_CLIENT, false, + 6666); + + Entity<MyEntity> entity = Entity.entity(new MyEntity(MY_VALUE), MediaType.APPLICATION_JSON); + MyCallback callback = new MyCallback(); + final Response response = client.post(callback, HELLO, entity, Collections.emptyMap()).get(); + + verifyCallback("testHttpPostNoAuthClientAsync", callback, response); + + final String body = HttpClient.getBody(response, String.class); + + assertEquals(200, response.getStatus()); + assertEquals("POST:hello:{myParameter=myValue}", body); + } + + @Test public void testHttpDeletetNoAuthClient() throws Exception { final HttpClient client = getNoAuthHttpClient(TEST_HTTP_NO_AUTH_CLIENT, false, 6666); @@ -231,6 +292,41 @@ public class HttpClientTest { } @Test + public void testHttpDeletetNoAuthClientAsync() throws Exception { + final HttpClient client = getNoAuthHttpClient(TEST_HTTP_NO_AUTH_CLIENT, false, + 6666); + + MyCallback callback = new MyCallback(); + final Response response = client.delete(callback, HELLO, Collections.emptyMap()).get(); + + verifyCallback("testHttpDeletetNoAuthClientAsync", callback, response); + + final String body = HttpClient.getBody(response, String.class); + + assertEquals(200, response.getStatus()); + assertEquals("DELETE:hello", body); + } + + /** + * Perform one asynchronous test with auth client; don't need to test every method. + * @throws Exception if an error occurs + */ + @Test + public void testHttpAsyncAuthClient() throws Exception { + final HttpClient client = getAuthHttpClient(); + + MyCallback callback = new MyCallback(); + final Response response = client.get(callback, HELLO).get(); + + verifyCallback("testHttpAsyncAuthClient", callback, response); + + final String body = HttpClient.getBody(response, String.class); + + assertEquals(200, response.getStatus()); + assertEquals(HELLO, body); + } + + @Test public void testHttpGetAuthClient() throws Exception { final HttpClient client = getAuthHttpClient(); @@ -382,16 +478,33 @@ public class HttpClientTest { server.waitedStart(10000); } + Response response; final HttpClient clientPap = HttpClientFactoryInstance.getClientFactory().get("PAP"); - final Response response = clientPap.get(); + response = clientPap.get(); assertEquals(200, response.getStatus()); final HttpClient clientPdp = HttpClientFactoryInstance.getClientFactory().get("PDP"); - final Response response2 = clientPdp.get("test"); - assertEquals(500, response2.getStatus()); + response = clientPdp.get("test"); + assertEquals(500, response.getStatus()); assertFalse(MyJacksonProvider.hasWrittenSome()); assertFalse(MyGsonProvider.hasWrittenSome()); + + // try with empty path + response = clientPap.get(""); + assertEquals(200, response.getStatus()); + + // try it asynchronously, too + MyCallback callback = new MyCallback(); + response = clientPap.get(callback).get(); + verifyCallback("testHttpAuthClientProps", callback, response); + assertEquals(200, response.getStatus()); + + // try it asynchronously, with empty path + callback = new MyCallback(); + response = clientPap.get(callback, "").get(); + verifyCallback("testHttpAuthClientProps - empty path", callback, response); + assertEquals(200, response.getStatus()); } @Test @@ -497,4 +610,29 @@ public class HttpClientTest { } + class MyCallback implements InvocationCallback<Response> { + @Getter + private Response response; + + @Getter + private Throwable throwable; + + private CountDownLatch latch = new CountDownLatch(1); + + @Override + public void completed(Response response) { + this.response = response; + latch.countDown(); + } + + @Override + public void failed(Throwable throwable) { + this.throwable = throwable; + latch.countDown(); + } + + public boolean await() throws InterruptedException { + return latch.await(5, TimeUnit.SECONDS); + } + } } diff --git a/utils/src/main/java/org/onap/policy/common/utils/coder/Coder.java b/utils/src/main/java/org/onap/policy/common/utils/coder/Coder.java index bb51f2b9..ec0e5e42 100644 --- a/utils/src/main/java/org/onap/policy/common/utils/coder/Coder.java +++ b/utils/src/main/java/org/onap/policy/common/utils/coder/Coder.java @@ -1,8 +1,8 @@ /* * ============LICENSE_START======================================================= - * ONAP PAP + * ONAP * ================================================================================ - * Copyright (C) 2019 AT&T Intellectual Property. All rights reserved. + * Copyright (C) 2019-2020 AT&T Intellectual Property. All rights reserved. * ================================================================================ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -41,6 +41,16 @@ public interface Coder { String encode(Object object) throws CoderException; /** + * Encodes an object into json, optionally making it "pretty". + * + * @param object object to be encoded + * @param pretty {@code true} if it should be encoded as "pretty" json, {@code false} otherwise + * @return a json string representing the object + * @throws CoderException if an error occurs + */ + String encode(Object object, boolean pretty) throws CoderException; + + /** * Encodes an object into json, writing to the given target. * * @param target target to which to write the encoded json diff --git a/utils/src/main/java/org/onap/policy/common/utils/coder/StandardCoder.java b/utils/src/main/java/org/onap/policy/common/utils/coder/StandardCoder.java index 6d0cbc9d..13973f1c 100644 --- a/utils/src/main/java/org/onap/policy/common/utils/coder/StandardCoder.java +++ b/utils/src/main/java/org/onap/policy/common/utils/coder/StandardCoder.java @@ -54,9 +54,21 @@ public class StandardCoder implements Coder { * Gson object used to encode and decode messages. */ @Getter(AccessLevel.PROTECTED) - private static final Gson GSON = GsonMessageBodyHandler.configBuilder( - new GsonBuilder().registerTypeAdapter(StandardCoderObject.class, new StandardTypeAdapter())) - .create(); + private static final Gson GSON; + + /** + * Gson object used to encode messages in "pretty" format. + */ + @Getter(AccessLevel.PROTECTED) + private static final Gson GSON_PRETTY; + + static { + GsonBuilder builder = GsonMessageBodyHandler.configBuilder( + new GsonBuilder().registerTypeAdapter(StandardCoderObject.class, new StandardTypeAdapter())); + + GSON = builder.create(); + GSON_PRETTY = builder.setPrettyPrinting().create(); + } /** * Constructs the object. @@ -67,8 +79,18 @@ public class StandardCoder implements Coder { @Override public String encode(Object object) throws CoderException { + return encode(object, false); + } + + @Override + public String encode(Object object, boolean pretty) throws CoderException { try { - return toJson(object); + if (pretty) { + return toPrettyJson(object); + + } else { + return toJson(object); + } } catch (RuntimeException e) { throw new CoderException(e); @@ -151,6 +173,16 @@ public class StandardCoder implements Coder { } } + /** + * Encodes the object as "pretty" json. + * + * @param object object to be encoded + * @return the encoded object + */ + protected String toPrettyJson(Object object) { + return GSON_PRETTY.toJson(object); + } + @Override public StandardCoderObject toStandard(Object object) throws CoderException { try { diff --git a/utils/src/main/java/org/onap/policy/common/utils/coder/StandardValCoder.java b/utils/src/main/java/org/onap/policy/common/utils/coder/StandardValCoder.java index 378254b8..6e08e722 100644 --- a/utils/src/main/java/org/onap/policy/common/utils/coder/StandardValCoder.java +++ b/utils/src/main/java/org/onap/policy/common/utils/coder/StandardValCoder.java @@ -62,10 +62,21 @@ public class StandardValCoder extends StandardCoder { } @Override + protected String toPrettyJson(Object object) { + /* + * The validator strips off the "pretty" stuff (i.e., spaces), thus we have to validate + * and generate the pretty JSON in separate steps. + */ + getGSON().toJson(object, object.getClass(), validatorApi.createJsonWriter(validator, new StringWriter())); + + return super.toPrettyJson(object); + } + + @Override protected String toJson(@NonNull Object object) { StringWriter output = new StringWriter(); toJson(output, object); - return String.valueOf(output); + return output.toString(); } @Override diff --git a/utils/src/main/java/org/onap/policy/common/utils/coder/StandardYamlCoder.java b/utils/src/main/java/org/onap/policy/common/utils/coder/StandardYamlCoder.java index 36f15b96..1bcf6ac0 100644 --- a/utils/src/main/java/org/onap/policy/common/utils/coder/StandardYamlCoder.java +++ b/utils/src/main/java/org/onap/policy/common/utils/coder/StandardYamlCoder.java @@ -2,7 +2,7 @@ * ============LICENSE_START======================================================= * ONAP * ================================================================================ - * Copyright (C) 2019 AT&T Intellectual Property. All rights reserved. + * Copyright (C) 2019-2020 AT&T Intellectual Property. All rights reserved. * ================================================================================ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -38,6 +38,12 @@ public class StandardYamlCoder extends StandardCoder { } @Override + protected String toPrettyJson(Object object) { + // YAML is already "pretty" + return toJson(object); + } + + @Override protected String toJson(Object object) { return translator.toYaml(object); } diff --git a/utils/src/test/java/org/onap/policy/common/utils/coder/StandardCoderTest.java b/utils/src/test/java/org/onap/policy/common/utils/coder/StandardCoderTest.java index 43a17dec..d5cde55a 100644 --- a/utils/src/test/java/org/onap/policy/common/utils/coder/StandardCoderTest.java +++ b/utils/src/test/java/org/onap/policy/common/utils/coder/StandardCoderTest.java @@ -77,6 +77,32 @@ public class StandardCoderTest { } @Test + public void testEncodeObjectBoolean() throws Exception { + final List<Integer> arr = Arrays.asList(1100, 1110); + + /* + * As plain json. + */ + assertEquals("[1100,1110]", coder.encode(arr, false)); + + // test exception case + coder = spy(new StandardCoder()); + when(coder.toJson(arr)).thenThrow(jpe); + assertThatThrownBy(() -> coder.encode(arr, false)).isInstanceOf(CoderException.class).hasCause(jpe); + + + /* + * As pretty json. + */ + assertEquals("[\n 1100,\n 1110\n]", coder.encode(arr, true)); + + // test exception case + coder = spy(new StandardCoder()); + when(coder.toPrettyJson(arr)).thenThrow(jpe); + assertThatThrownBy(() -> coder.encode(arr, true)).isInstanceOf(CoderException.class).hasCause(jpe); + } + + @Test public void testEncodeWriterObject() throws Exception { List<Integer> arr = Arrays.asList(1200, 1210); StringWriter wtr = new StringWriter(); diff --git a/utils/src/test/java/org/onap/policy/common/utils/coder/StandardValCoderTest.java b/utils/src/test/java/org/onap/policy/common/utils/coder/StandardValCoderTest.java index 38106f57..2fcdb0dd 100644 --- a/utils/src/test/java/org/onap/policy/common/utils/coder/StandardValCoderTest.java +++ b/utils/src/test/java/org/onap/policy/common/utils/coder/StandardValCoderTest.java @@ -20,8 +20,10 @@ package org.onap.policy.common.utils.coder; +import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotEquals; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; @@ -119,6 +121,26 @@ public class StandardValCoderTest { StringWriter writer = new StringWriter(); valCoder.encode(writer, valOuter); assertEquals(valOuterJson, writer.toString()); + + // test exception case with an empty object + assertThatThrownBy(() -> valCoder.encode(new ValOuter())).isInstanceOf(CoderException.class); + } + + @Test + public void testPretty() throws CoderException { + StandardValCoder valCoder = new StandardValCoder(jsonSchema, "test-schema"); + ValOuter valOuter = valCoder.decode(validJson, ValOuter.class); + + String valOuterJson = valCoder.encode(valOuter); + assertEquals(valOuterJson, valCoder.encode(valOuter, false)); + String prettyValOuterJson = valCoder.encode(valOuter, true); + assertNotEquals(valOuterJson, prettyValOuterJson); + + assertEquals(valOuter, valCoder.decode(prettyValOuterJson, ValOuter.class)); + + // test exception cases with an empty object + assertThatThrownBy(() -> valCoder.encode(new ValOuter(), false)).isInstanceOf(CoderException.class); + assertThatThrownBy(() -> valCoder.encode(new ValOuter(), true)).isInstanceOf(CoderException.class); } @Test diff --git a/utils/src/test/java/org/onap/policy/common/utils/coder/StandardYamlCoderTest.java b/utils/src/test/java/org/onap/policy/common/utils/coder/StandardYamlCoderTest.java index e38c5c9c..c770cd3b 100644 --- a/utils/src/test/java/org/onap/policy/common/utils/coder/StandardYamlCoderTest.java +++ b/utils/src/test/java/org/onap/policy/common/utils/coder/StandardYamlCoderTest.java @@ -2,7 +2,7 @@ * ============LICENSE_START======================================================= * ONAP * ================================================================================ - * Copyright (C) 2019 AT&T Intellectual Property. All rights reserved. + * Copyright (C) 2019-2020 AT&T Intellectual Property. All rights reserved. * ================================================================================ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,8 +20,11 @@ package org.onap.policy.common.utils.coder; +import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.when; import java.io.File; import java.io.StringWriter; @@ -45,6 +48,25 @@ public class StandardYamlCoderTest { } @Test + public void testToPrettyJson() throws CoderException { + String expected = coder.encode(cont); + assertEquals(expected, coder.encode(cont, false)); + + String yaml = coder.encode(cont, true); + assertEquals(expected, yaml); + + Container cont2 = coder.decode(yaml, Container.class); + assertEquals(cont, cont2); + + // test exception cases + IllegalArgumentException expex = new IllegalArgumentException("expected exception"); + coder = spy(new StandardYamlCoder()); + when(coder.toJson(cont)).thenThrow(expex); + assertThatThrownBy(() -> coder.encode(cont, false)).isInstanceOf(CoderException.class).hasCause(expex); + assertThatThrownBy(() -> coder.encode(cont, true)).isInstanceOf(CoderException.class).hasCause(expex); + } + + @Test public void testToJsonObject() throws CoderException { String yaml = coder.encode(cont); |