diff --git a/lib/server-api/src/main/java/org/apache/olingo/server/api/serializer/ODataError.java b/lib/server-api/src/main/java/org/apache/olingo/server/api/serializer/ODataError.java new file mode 100644 index 000000000..a28a84348 --- /dev/null +++ b/lib/server-api/src/main/java/org/apache/olingo/server/api/serializer/ODataError.java @@ -0,0 +1,88 @@ +/* + * 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.olingo.server.api.serializer; + +//TODO: Where to put this class +public class ODataError { + + String code; + String message; + String target; + + /** + * The value for the code name/value pair is a language-independent string. Its value is a service-defined error code. + * This code serves as a sub-status for the HTTP error code specified in the response. MAY be null. + * @return the error code as a string + */ + public String getCode() { + return code; + } + + /** + * The value for the code name/value pair is a language-independent string. Its value is a service-defined error code. + * This code serves as a sub-status for the HTTP error code specified in the response. MAY be null. + * @param code + * @return this for method chaining + */ + public ODataError setCode(String code) { + this.code = code; + return this; + } + + /** + * The value for the message name/value pair MUST be a human-readable, language-dependent representation of the error. + * MUST not be null + * @return the message string + */ + public String getMessage() { + return message; + } + + /** + * The value for the message name/value pair MUST be a human-readable, language-dependent representation of the error. + * MUST not be null + * @param message + * @return this for method chaining + */ + public ODataError setMessage(String message) { + this.message = message; + return this; + } + + /** + * The value for the target name/value pair is the target of the particular error (for example, the name of the + * property in error). MAY be null. + * @return the target string + */ + public String getTarget() { + return target; + } + + /** + * The value for the target name/value pair is the target of the particular error (for example, the name of the + * property in error). MAY be null. + * @param target + * @return this for method chaining + */ + public ODataError setTarget(String target) { + this.target = target; + return this; + } + +} diff --git a/lib/server-api/src/main/java/org/apache/olingo/server/api/serializer/ODataSerializer.java b/lib/server-api/src/main/java/org/apache/olingo/server/api/serializer/ODataSerializer.java index 0eaf44070..6752096b1 100644 --- a/lib/server-api/src/main/java/org/apache/olingo/server/api/serializer/ODataSerializer.java +++ b/lib/server-api/src/main/java/org/apache/olingo/server/api/serializer/ODataSerializer.java @@ -19,6 +19,7 @@ package org.apache.olingo.server.api.serializer; import java.io.InputStream; +import java.util.List; import org.apache.olingo.commons.api.data.ContextURL; import org.apache.olingo.commons.api.data.Entity; @@ -38,4 +39,12 @@ public interface ODataSerializer { InputStream entity(EdmEntityType edmEntityType, Entity entity, ContextURL contextURL); InputStream entitySet(EdmEntitySet edmEntitySet, EntitySet entitySet, ContextURL contextURL); + + /** + * Writes an ODataError into an InputStream + * @param error the main error + * @param details a list of details. MAY be null or empty. + * @return inputStream containing the OData formatted error + */ + InputStream error(ODataError error, List details); } diff --git a/lib/server-core/pom.xml b/lib/server-core/pom.xml index 91de276d2..6bf11a243 100644 --- a/lib/server-core/pom.xml +++ b/lib/server-core/pom.xml @@ -69,6 +69,11 @@ org.slf4j slf4j-simple + + commons-io + commons-io + test + diff --git a/lib/server-core/src/main/java/org/apache/olingo/server/core/serializer/ODataXmlSerializerImpl.java b/lib/server-core/src/main/java/org/apache/olingo/server/core/serializer/ODataXmlSerializerImpl.java index e24a88c27..122af0cba 100644 --- a/lib/server-core/src/main/java/org/apache/olingo/server/core/serializer/ODataXmlSerializerImpl.java +++ b/lib/server-core/src/main/java/org/apache/olingo/server/core/serializer/ODataXmlSerializerImpl.java @@ -19,6 +19,7 @@ package org.apache.olingo.server.core.serializer; import java.io.InputStream; +import java.util.List; import javax.xml.stream.XMLOutputFactory; import javax.xml.stream.XMLStreamException; @@ -31,6 +32,7 @@ import org.apache.olingo.commons.api.data.EntitySet; import org.apache.olingo.commons.api.edm.Edm; import org.apache.olingo.commons.api.edm.EdmEntitySet; import org.apache.olingo.commons.api.edm.EdmEntityType; +import org.apache.olingo.server.api.serializer.ODataError; import org.apache.olingo.server.api.serializer.ODataSerializer; import org.apache.olingo.server.core.serializer.utils.CircleStreamBuffer; import org.apache.olingo.server.core.serializer.xml.MetadataDocumentXmlSerializer; @@ -86,4 +88,9 @@ public class ODataXmlSerializerImpl implements ODataSerializer { throw new ODataRuntimeException("Entityset serialization not implemented for XML format"); } + @Override + public InputStream error(ODataError error, List details) { + throw new ODataRuntimeException("error serialization not implemented for XML format"); + } + } diff --git a/lib/server-core/src/main/java/org/apache/olingo/server/core/serializer/json/ODataErrorSerializer.java b/lib/server-core/src/main/java/org/apache/olingo/server/core/serializer/json/ODataErrorSerializer.java new file mode 100644 index 000000000..232548135 --- /dev/null +++ b/lib/server-core/src/main/java/org/apache/olingo/server/core/serializer/json/ODataErrorSerializer.java @@ -0,0 +1,78 @@ +/* + * 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.olingo.server.core.serializer.json; + +import java.io.IOException; +import java.util.List; + +import org.apache.olingo.commons.api.ODataRuntimeException; +import org.apache.olingo.server.api.serializer.ODataError; + +import com.fasterxml.jackson.core.JsonGenerationException; +import com.fasterxml.jackson.core.JsonGenerator; + +public class ODataErrorSerializer { + + private static final String ERROR = "error"; + private static final String CODE = "code"; + private static final String MESSAGE = "message"; + private static final String TARGET = "target"; + private static final String DETAILS = "details"; + + public void writeErrorDocument(JsonGenerator json, ODataError error, List details) throws IOException { + if (error == null) { + throw new ODataRuntimeException("ODataError object MUST NOT be null!"); + } + json.writeStartObject(); + json.writeFieldName(ERROR); + + json.writeStartObject(); + writeODataError(json, error); + + if(details != null){ + json.writeArrayFieldStart(DETAILS); + for(ODataError detailedError : details){ + json.writeStartObject(); + writeODataError(json, detailedError); + json.writeEndObject(); + } + json.writeEndArray(); + } + + json.writeEndObject(); + json.writeEndObject(); + } + + private void writeODataError(JsonGenerator json, ODataError error) throws IOException, JsonGenerationException { + if (error.getCode() == null) { + json.writeNullField(CODE); + } else { + json.writeStringField(CODE, error.getCode()); + } + if (error.getMessage() == null) { + json.writeNullField(MESSAGE); + } else { + json.writeStringField(MESSAGE, error.getMessage()); + } + + if (error.getTarget() != null) { + json.writeStringField(TARGET, error.getTarget()); + } + } +} diff --git a/lib/server-core/src/main/java/org/apache/olingo/server/core/serializer/json/ODataJsonSerializer.java b/lib/server-core/src/main/java/org/apache/olingo/server/core/serializer/json/ODataJsonSerializer.java index 022b14b53..62b9287c0 100644 --- a/lib/server-core/src/main/java/org/apache/olingo/server/core/serializer/json/ODataJsonSerializer.java +++ b/lib/server-core/src/main/java/org/apache/olingo/server/core/serializer/json/ODataJsonSerializer.java @@ -26,6 +26,7 @@ import org.apache.olingo.commons.api.ODataRuntimeException; import org.apache.olingo.commons.api.data.*; import org.apache.olingo.commons.api.edm.*; import org.apache.olingo.commons.core.edm.primitivetype.EdmPrimitiveTypeFactory; +import org.apache.olingo.server.api.serializer.ODataError; import org.apache.olingo.server.api.serializer.ODataSerializer; import org.apache.olingo.server.core.serializer.utils.CircleStreamBuffer; import org.slf4j.Logger; @@ -172,7 +173,7 @@ public class ODataJsonSerializer implements ODataSerializer { writePrimitive(edmProperty, property, json); } else if (property.isLinkedComplex()) { writeComplexValue(edmProperty, property.asLinkedComplex().getValue(), json); - } else if(property.isComplex()) { + } else if (property.isComplex()) { writeComplexValue(edmProperty, property.asComplex(), json); } else { throw new ODataRuntimeException("Property type not yet supported!"); @@ -180,6 +181,7 @@ public class ODataJsonSerializer implements ODataSerializer { } } + private void writeCollection(EdmProperty edmProperty, Property property, JsonGenerator json) throws IOException, EdmPrimitiveTypeException { json.writeStartArray(); @@ -243,7 +245,7 @@ public class ODataJsonSerializer implements ODataSerializer { } private void writeComplexValue(final EdmProperty edmProperty, final List properties, - JsonGenerator json) throws IOException, EdmPrimitiveTypeException { + JsonGenerator json) throws IOException, EdmPrimitiveTypeException { final EdmComplexType type = (EdmComplexType) edmProperty.getType(); json.writeStartObject(); for (final String propertyName : type.getPropertyNames()) { @@ -261,4 +263,18 @@ public class ODataJsonSerializer implements ODataSerializer { } return null; } + + @Override + public InputStream error(ODataError error, List details) { + CircleStreamBuffer buffer = new CircleStreamBuffer(); + try { + JsonGenerator json = new JsonFactory().createGenerator(buffer.getOutputStream()); + ODataErrorSerializer ser = new ODataErrorSerializer(); + ser.writeErrorDocument(json, error, details); + json.close(); + } catch (final IOException e) { + throw new ODataRuntimeException(e); + } + return buffer.getInputStream(); + } } diff --git a/lib/server-core/src/test/java/org/apache/olingo/server/core/serializer/json/ODataErrorSerializerTest.java b/lib/server-core/src/test/java/org/apache/olingo/server/core/serializer/json/ODataErrorSerializerTest.java new file mode 100644 index 000000000..6e383fc3d --- /dev/null +++ b/lib/server-core/src/test/java/org/apache/olingo/server/core/serializer/json/ODataErrorSerializerTest.java @@ -0,0 +1,134 @@ +/* + * 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.olingo.server.core.serializer.json; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + +import java.io.InputStream; +import java.util.ArrayList; +import java.util.List; + +import org.apache.commons.io.IOUtils; +import org.apache.olingo.commons.api.ODataRuntimeException; +import org.apache.olingo.commons.api.format.ODataFormat; +import org.apache.olingo.server.api.OData; +import org.apache.olingo.server.api.serializer.ODataError; +import org.apache.olingo.server.api.serializer.ODataSerializer; +import org.junit.Before; +import org.junit.Test; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.JsonNodeType; + +public class ODataErrorSerializerTest { + + ODataSerializer ser; + + @Before + public void before() { + ser = OData.newInstance().createSerializer(ODataFormat.JSON); + } + + @Test + public void basicODataErrorNoCode() throws Exception { + ODataError error = new ODataError(); + error.setMessage("ErrorMessage"); + InputStream stream = ser.error(error, null); + String jsonString = IOUtils.toString(stream); + assertEquals("{\"error\":{\"code\":null,\"message\":\"ErrorMessage\"}}", jsonString); + } + + @Test + public void basicODataErrorWithCode() throws Exception { + ODataError error = new ODataError(); + error.setCode("Code").setMessage("ErrorMessage"); + InputStream stream = ser.error(error, null); + String jsonString = IOUtils.toString(stream); + assertEquals("{\"error\":{\"code\":\"Code\",\"message\":\"ErrorMessage\"}}", jsonString); + } + + @Test + public void basicODataErrorWithCodeAndTarget() throws Exception { + ODataError error = new ODataError(); + error.setCode("Code").setMessage("ErrorMessage").setTarget("Target"); + InputStream stream = ser.error(error, null); + String jsonString = IOUtils.toString(stream); + assertEquals("{\"error\":{\"code\":\"Code\",\"message\":\"ErrorMessage\",\"target\":\"Target\"}}", jsonString); + } + + @Test(expected = ODataRuntimeException.class) + public void nullErrorResultsInException() throws Exception { + ser.error(null, null); + } + + @Test + public void emptyDetailsList() throws Exception { + ODataError error = new ODataError(); + error.setMessage("ErrorMessage"); + InputStream stream = ser.error(error, new ArrayList()); + String jsonString = IOUtils.toString(stream); + assertEquals("{\"error\":{\"code\":null,\"message\":\"ErrorMessage\",\"details\":[]}}", jsonString); + } + + @Test + public void nothingSetAtODataErrorObject() throws Exception { + ODataError error = new ODataError(); + InputStream stream = ser.error(error, null); + String jsonString = IOUtils.toString(stream); + assertEquals("{\"error\":{\"code\":null,\"message\":null}}", jsonString); + } + + @Test + public void singleDetailNothingSet() throws Exception { + List details = new ArrayList(); + details.add(new ODataError()); + ODataError error = new ODataError(); + InputStream stream = ser.error(error, details); + String jsonString = IOUtils.toString(stream); + assertEquals("{\"error\":{\"code\":null,\"message\":null,\"details\":[{\"code\":null,\"message\":null}]}}", + jsonString); + } + + @Test + public void verifiedWithJacksonParser() throws Exception { + List details = new ArrayList(); + details.add(new ODataError().setCode("detailCode").setMessage("detailMessage").setTarget("detailTarget")); + ODataError error = new ODataError().setCode("Code").setMessage("Message").setTarget("Target"); + InputStream stream = ser.error(error, details); + JsonNode tree = new ObjectMapper().readTree(stream); + assertNotNull(tree); + tree = tree.get("error"); + assertNotNull(tree); + assertEquals("Code", tree.get("code").textValue()); + assertEquals("Message", tree.get("message").textValue()); + assertEquals("Target", tree.get("target").textValue()); + + tree = tree.get("details"); + assertNotNull(tree); + assertEquals(JsonNodeType.ARRAY, tree.getNodeType()); + + tree = tree.get(0); + assertNotNull(tree); + assertEquals("detailCode", tree.get("code").textValue()); + assertEquals("detailMessage", tree.get("message").textValue()); + assertEquals("detailTarget", tree.get("target").textValue()); + } +}