From 2b73abcce03e5b44ce61061c59dccde513265848 Mon Sep 17 00:00:00 2001 From: Ramesh Reddy Date: Mon, 30 Mar 2015 12:40:32 -0500 Subject: [PATCH] OLINGO-573: New processing framework on server side with single interface with TripPin as example --- .../olingo/commons/core/DecoderTest.java | 10 +- .../olingo/commons/core/EncoderTest.java | 10 +- lib/pom.xml | 1 + .../api/deserializer/ODataDeserializer.java | 29 +- .../EntityCollectionSerializerOptions.java | 18 +- .../serializer/EntitySerializerOptions.java | 18 +- .../api/serializer/ODataSerializer.java | 26 +- .../api/serializer/SerializerException.java | 8 +- lib/server-core-ext/pom.xml | 117 +++ .../olingo/server/core/ErrorHandler.java | 125 +++ .../olingo/server/core/MetadataParser.java | 679 ++++++++++++++ .../olingo/server/core/OData4HttpHandler.java | 123 +++ .../apache/olingo/server/core/OData4Impl.java | 45 + .../core/RequestURLHierarchyVisitor.java | 333 +++++++ .../olingo/server/core/RequestURLVisitor.java | 127 +++ .../server/core/ReturnRepresentation.java | 23 + .../server/core/SchemaBasedEdmProvider.java | 315 +++++++ .../olingo/server/core/ServiceDispatcher.java | 227 +++++ .../olingo/server/core/ServiceHandler.java | 263 ++++++ .../olingo/server/core/ServiceRequest.java | 253 ++++++ .../core/legacy/ProcessorServiceHandler.java | 433 +++++++++ .../server/core/requests/ActionRequest.java | 120 +++ .../server/core/requests/BatchRequest.java | 197 ++++ .../server/core/requests/DataRequest.java | 769 ++++++++++++++++ .../server/core/requests/FunctionRequest.java | 122 +++ .../server/core/requests/MediaRequest.java | 99 ++ .../server/core/requests/MetadataRequest.java | 61 ++ .../core/requests/OperationRequest.java | 118 +++ .../core/requests/ServiceDocumentRequest.java | 57 ++ .../server/core/responses/CountResponse.java | 60 ++ .../server/core/responses/EntityResponse.java | 140 +++ .../core/responses/EntitySetResponse.java | 82 ++ .../core/responses/MetadataResponse.java | 62 ++ .../core/responses/NoContentResponse.java | 100 +++ .../responses/PrimitiveValueResponse.java | 105 +++ .../core/responses/PropertyResponse.java | 144 +++ .../responses/ServiceDocumentResponse.java | 63 ++ .../core/responses/ServiceResponse.java | 119 +++ .../core/responses/ServiceResponseVisior.java | 71 ++ .../server/core/responses/StreamResponse.java | 54 ++ .../server/core/MetadataParserTest.java | 185 ++++ .../server/core/ServiceDispatcherTest.java | 417 +++++++++ .../server/example/TripPinDataModel.java | 843 ++++++++++++++++++ .../olingo/server/example/TripPinHandler.java | 546 ++++++++++++ .../server/example/TripPinServiceTest.java | 756 ++++++++++++++++ .../olingo/server/example/TripPinServlet.java | 75 ++ .../src/test/resources/OlingoOrangeTM.png | Bin 0 -> 93316 bytes .../src/test/resources/airlines.json | 64 ++ .../src/test/resources/airports.json | 394 ++++++++ .../src/test/resources/event.json | 157 ++++ .../src/test/resources/flight-links.json | 52 ++ .../src/test/resources/flight.json | 66 ++ .../src/test/resources/people-links.json | 94 ++ .../src/test/resources/people.json | 323 +++++++ .../src/test/resources/photos.json | 64 ++ .../src/test/resources/trip-links.json | 28 + .../src/test/resources/trip.json | 224 +++++ .../src/test/resources/trippin.xml | 356 ++++++++ .../json/ODataJsonDeserializer.java | 86 +- .../serializer/json/ODataJsonSerializer.java | 138 ++- .../serializer/utils/ContextURLBuilder.java | 9 +- .../xml/ODataXmlSerializerImpl.java | 19 +- .../core/uri/UriResourceActionImpl.java | 14 +- .../core/uri/validator/UriValidator.java | 8 +- .../server-core-exceptions-i18n.properties | 2 + .../json/ODataJsonDeserializerBasicTest.java | 38 +- .../json/ODataJsonSerializerTest.java | 13 +- .../server/tecsvc/TechnicalServlet.java | 8 +- .../server/tecsvc/data/DataCreator.java | 134 ++- .../server/tecsvc/data/DataProvider.java | 31 +- .../server/tecsvc/data/FunctionData.java | 7 +- .../processor/TechnicalEntityProcessor.java | 34 +- .../TechnicalPrimitiveComplexProcessor.java | 15 +- .../server/tecsvc/data/DataProviderTest.java | 2 +- .../json/ODataJsonDeserializerEntityTest.java | 4 +- .../json/ODataJsonSerializerTest.java | 60 +- .../core/uri/antlr/TestUriParserImpl.java | 11 +- .../core/uri/validator/UriValidatorTest.java | 18 +- .../server/sample/data/DataProvider.java | 13 +- .../sample/processor/CarsProcessor.java | 16 +- 80 files changed, 10821 insertions(+), 229 deletions(-) create mode 100644 lib/server-core-ext/pom.xml create mode 100644 lib/server-core-ext/src/main/java/org/apache/olingo/server/core/ErrorHandler.java create mode 100644 lib/server-core-ext/src/main/java/org/apache/olingo/server/core/MetadataParser.java create mode 100644 lib/server-core-ext/src/main/java/org/apache/olingo/server/core/OData4HttpHandler.java create mode 100644 lib/server-core-ext/src/main/java/org/apache/olingo/server/core/OData4Impl.java create mode 100644 lib/server-core-ext/src/main/java/org/apache/olingo/server/core/RequestURLHierarchyVisitor.java create mode 100644 lib/server-core-ext/src/main/java/org/apache/olingo/server/core/RequestURLVisitor.java create mode 100644 lib/server-core-ext/src/main/java/org/apache/olingo/server/core/ReturnRepresentation.java create mode 100644 lib/server-core-ext/src/main/java/org/apache/olingo/server/core/SchemaBasedEdmProvider.java create mode 100644 lib/server-core-ext/src/main/java/org/apache/olingo/server/core/ServiceDispatcher.java create mode 100644 lib/server-core-ext/src/main/java/org/apache/olingo/server/core/ServiceHandler.java create mode 100644 lib/server-core-ext/src/main/java/org/apache/olingo/server/core/ServiceRequest.java create mode 100644 lib/server-core-ext/src/main/java/org/apache/olingo/server/core/legacy/ProcessorServiceHandler.java create mode 100644 lib/server-core-ext/src/main/java/org/apache/olingo/server/core/requests/ActionRequest.java create mode 100644 lib/server-core-ext/src/main/java/org/apache/olingo/server/core/requests/BatchRequest.java create mode 100644 lib/server-core-ext/src/main/java/org/apache/olingo/server/core/requests/DataRequest.java create mode 100644 lib/server-core-ext/src/main/java/org/apache/olingo/server/core/requests/FunctionRequest.java create mode 100644 lib/server-core-ext/src/main/java/org/apache/olingo/server/core/requests/MediaRequest.java create mode 100644 lib/server-core-ext/src/main/java/org/apache/olingo/server/core/requests/MetadataRequest.java create mode 100644 lib/server-core-ext/src/main/java/org/apache/olingo/server/core/requests/OperationRequest.java create mode 100644 lib/server-core-ext/src/main/java/org/apache/olingo/server/core/requests/ServiceDocumentRequest.java create mode 100644 lib/server-core-ext/src/main/java/org/apache/olingo/server/core/responses/CountResponse.java create mode 100644 lib/server-core-ext/src/main/java/org/apache/olingo/server/core/responses/EntityResponse.java create mode 100644 lib/server-core-ext/src/main/java/org/apache/olingo/server/core/responses/EntitySetResponse.java create mode 100644 lib/server-core-ext/src/main/java/org/apache/olingo/server/core/responses/MetadataResponse.java create mode 100644 lib/server-core-ext/src/main/java/org/apache/olingo/server/core/responses/NoContentResponse.java create mode 100644 lib/server-core-ext/src/main/java/org/apache/olingo/server/core/responses/PrimitiveValueResponse.java create mode 100644 lib/server-core-ext/src/main/java/org/apache/olingo/server/core/responses/PropertyResponse.java create mode 100644 lib/server-core-ext/src/main/java/org/apache/olingo/server/core/responses/ServiceDocumentResponse.java create mode 100644 lib/server-core-ext/src/main/java/org/apache/olingo/server/core/responses/ServiceResponse.java create mode 100644 lib/server-core-ext/src/main/java/org/apache/olingo/server/core/responses/ServiceResponseVisior.java create mode 100644 lib/server-core-ext/src/main/java/org/apache/olingo/server/core/responses/StreamResponse.java create mode 100644 lib/server-core-ext/src/test/java/org/apache/olingo/server/core/MetadataParserTest.java create mode 100644 lib/server-core-ext/src/test/java/org/apache/olingo/server/core/ServiceDispatcherTest.java create mode 100644 lib/server-core-ext/src/test/java/org/apache/olingo/server/example/TripPinDataModel.java create mode 100644 lib/server-core-ext/src/test/java/org/apache/olingo/server/example/TripPinHandler.java create mode 100644 lib/server-core-ext/src/test/java/org/apache/olingo/server/example/TripPinServiceTest.java create mode 100644 lib/server-core-ext/src/test/java/org/apache/olingo/server/example/TripPinServlet.java create mode 100644 lib/server-core-ext/src/test/resources/OlingoOrangeTM.png create mode 100644 lib/server-core-ext/src/test/resources/airlines.json create mode 100644 lib/server-core-ext/src/test/resources/airports.json create mode 100644 lib/server-core-ext/src/test/resources/event.json create mode 100644 lib/server-core-ext/src/test/resources/flight-links.json create mode 100644 lib/server-core-ext/src/test/resources/flight.json create mode 100644 lib/server-core-ext/src/test/resources/people-links.json create mode 100644 lib/server-core-ext/src/test/resources/people.json create mode 100644 lib/server-core-ext/src/test/resources/photos.json create mode 100644 lib/server-core-ext/src/test/resources/trip-links.json create mode 100644 lib/server-core-ext/src/test/resources/trip.json create mode 100644 lib/server-core-ext/src/test/resources/trippin.xml diff --git a/lib/commons-core/src/test/java/org/apache/olingo/commons/core/DecoderTest.java b/lib/commons-core/src/test/java/org/apache/olingo/commons/core/DecoderTest.java index 019ef5a4f..09f232697 100644 --- a/lib/commons-core/src/test/java/org/apache/olingo/commons/core/DecoderTest.java +++ b/lib/commons-core/src/test/java/org/apache/olingo/commons/core/DecoderTest.java @@ -6,9 +6,9 @@ * 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 @@ -18,13 +18,13 @@ ******************************************************************************/ package org.apache.olingo.commons.core; -import org.junit.Test; - import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNull; +import org.junit.Test; + /** - * + * */ public class DecoderTest { diff --git a/lib/commons-core/src/test/java/org/apache/olingo/commons/core/EncoderTest.java b/lib/commons-core/src/test/java/org/apache/olingo/commons/core/EncoderTest.java index 74eb1af77..7db30cea4 100644 --- a/lib/commons-core/src/test/java/org/apache/olingo/commons/core/EncoderTest.java +++ b/lib/commons-core/src/test/java/org/apache/olingo/commons/core/EncoderTest.java @@ -6,9 +6,9 @@ * 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 @@ -18,16 +18,16 @@ ******************************************************************************/ package org.apache.olingo.commons.core; -import org.junit.Test; +import static org.junit.Assert.assertEquals; import java.net.URI; import java.net.URISyntaxException; -import static org.junit.Assert.assertEquals; +import org.junit.Test; /** * Tests for percent-encoding. - * + * */ public class EncoderTest { diff --git a/lib/pom.xml b/lib/pom.xml index 6843ff376..d1e88649b 100644 --- a/lib/pom.xml +++ b/lib/pom.xml @@ -42,6 +42,7 @@ client-core server-api server-core + server-core-ext server-tecsvc server-test diff --git a/lib/server-api/src/main/java/org/apache/olingo/server/api/deserializer/ODataDeserializer.java b/lib/server-api/src/main/java/org/apache/olingo/server/api/deserializer/ODataDeserializer.java index b361844a7..9aabf332e 100644 --- a/lib/server-api/src/main/java/org/apache/olingo/server/api/deserializer/ODataDeserializer.java +++ b/lib/server-api/src/main/java/org/apache/olingo/server/api/deserializer/ODataDeserializer.java @@ -6,9 +6,9 @@ * 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 @@ -19,13 +19,16 @@ package org.apache.olingo.server.api.deserializer; import java.io.InputStream; +import java.net.URI; import java.util.List; import org.apache.olingo.commons.api.data.Entity; import org.apache.olingo.commons.api.data.EntitySet; import org.apache.olingo.commons.api.data.Parameter; +import org.apache.olingo.commons.api.data.Property; import org.apache.olingo.commons.api.edm.EdmAction; import org.apache.olingo.commons.api.edm.EdmEntityType; +import org.apache.olingo.commons.api.edm.EdmProperty; /** * Deserializer on OData server side. @@ -34,7 +37,7 @@ public interface ODataDeserializer { /** * Deserializes an entity stream into an {@link Entity} object. - * Validates: property types, no double properties, correct json types + * Validates: property types, no double properties, correct json types * @param stream * @param edmEntityType * @return deserialized {@link Entity} object @@ -50,7 +53,7 @@ public interface ODataDeserializer { * @throws DeserializerException */ EntitySet entityCollection(InputStream stream, EdmEntityType edmEntityType) throws DeserializerException; - + /** * Deserializes an action-parameters stream into an {@link Entity} object. * Validates: parameter types, no double parameters, correct json types. @@ -60,4 +63,22 @@ public interface ODataDeserializer { * @throws DeserializerException */ List actionParameters(InputStream stream, EdmAction edmAction) throws DeserializerException; + + /** + * Deserializes the Property or collections of properties (primitive & complex) + * @param stream + * @param edmProperty + * @return deserialized {@link Property} + * @throws DeserializerException + */ + Property property(InputStream stream, EdmProperty edmProperty) throws DeserializerException; + + /** + * Read entity references from the provided document + * @param stream + * @param keys + * @return + * @throws DeserializerException + */ + List entityReferences(InputStream stream) throws DeserializerException; } diff --git a/lib/server-api/src/main/java/org/apache/olingo/server/api/serializer/EntityCollectionSerializerOptions.java b/lib/server-api/src/main/java/org/apache/olingo/server/api/serializer/EntityCollectionSerializerOptions.java index 14c588d25..e5dd6b0bc 100644 --- a/lib/server-api/src/main/java/org/apache/olingo/server/api/serializer/EntityCollectionSerializerOptions.java +++ b/lib/server-api/src/main/java/org/apache/olingo/server/api/serializer/EntityCollectionSerializerOptions.java @@ -6,9 +6,9 @@ * 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 @@ -30,6 +30,7 @@ public class EntityCollectionSerializerOptions { private CountOption count; private ExpandOption expand; private SelectOption select; + private boolean onlyReferences; /** Gets the {@link ContextURL}. */ public ContextURL getContextURL() { @@ -51,6 +52,11 @@ public class EntityCollectionSerializerOptions { return select; } + /** only writes the references of the entities*/ + public boolean onlyReferences() { + return onlyReferences; + } + /** Initializes the options builder. */ public static Builder with() { return new Builder(); @@ -59,7 +65,7 @@ public class EntityCollectionSerializerOptions { /** Builder of OData serializer options. */ public static final class Builder { - private EntityCollectionSerializerOptions options; + private final EntityCollectionSerializerOptions options; private Builder() { options = new EntityCollectionSerializerOptions(); @@ -89,6 +95,12 @@ public class EntityCollectionSerializerOptions { return this; } + /** Sets to serialize only references */ + public Builder setWriteOnlyReferences(final boolean ref) { + options.onlyReferences = ref; + return this; + } + /** Builds the OData serializer options. */ public EntityCollectionSerializerOptions build() { return options; diff --git a/lib/server-api/src/main/java/org/apache/olingo/server/api/serializer/EntitySerializerOptions.java b/lib/server-api/src/main/java/org/apache/olingo/server/api/serializer/EntitySerializerOptions.java index fcbd150fd..0abb31cf2 100644 --- a/lib/server-api/src/main/java/org/apache/olingo/server/api/serializer/EntitySerializerOptions.java +++ b/lib/server-api/src/main/java/org/apache/olingo/server/api/serializer/EntitySerializerOptions.java @@ -6,9 +6,9 @@ * 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 @@ -27,6 +27,7 @@ public class EntitySerializerOptions { private ContextURL contextURL; private ExpandOption expand; private SelectOption select; + private boolean onlyReferences; /** Gets the {@link ContextURL}. */ public ContextURL getContextURL() { @@ -43,6 +44,11 @@ public class EntitySerializerOptions { return select; } + /** only writes the references of the entities*/ + public boolean onlyReferences() { + return onlyReferences; + } + private EntitySerializerOptions() {} /** Initializes the options builder. */ @@ -53,7 +59,7 @@ public class EntitySerializerOptions { /** Builder of OData serializer options. */ public static final class Builder { - private EntitySerializerOptions options; + private final EntitySerializerOptions options; private Builder() { options = new EntitySerializerOptions(); @@ -77,6 +83,12 @@ public class EntitySerializerOptions { return this; } + /** Sets to serialize only references */ + public Builder setWriteOnlyReferences(final boolean ref) { + options.onlyReferences = ref; + return this; + } + /** Builds the OData serializer options. */ public EntitySerializerOptions build() { return options; 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 72f8ee862..55377cea6 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 @@ -6,9 +6,9 @@ * 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 @@ -39,7 +39,7 @@ public interface ODataSerializer { /** * Writes the service document into an InputStream. * @param edm the Entity Data Model - * @param serviceRoot the service-root URI of this OData service + * @param serviceRoot the service-root URI of this OData service */ InputStream serviceDocument(Edm edm, String serviceRoot) throws SerializerException; @@ -58,21 +58,23 @@ public interface ODataSerializer { /** * Writes entity-collection data into an InputStream. + * @param metadata Metadata for the service * @param entityType the {@link EdmEntityType} * @param entitySet the data of the entity set * @param options options for the serializer */ - InputStream entityCollection(EdmEntityType entityType, EntitySet entitySet, - EntityCollectionSerializerOptions options) throws SerializerException; + InputStream entityCollection(ServiceMetadata metadata, EdmEntityType entityType, + EntitySet entitySet, EntityCollectionSerializerOptions options) throws SerializerException; /** * Writes entity data into an InputStream. + * @param metadata Metadata for the service * @param entityType the {@link EdmEntityType} * @param entity the data of the entity * @param options options for the serializer */ - InputStream entity(EdmEntityType entityType, Entity entity, EntitySerializerOptions options) - throws SerializerException; + InputStream entity(ServiceMetadata metadata, EdmEntityType entityType, Entity entity, + EntitySerializerOptions options) throws SerializerException; /** * Writes primitive-type instance data into an InputStream. @@ -85,12 +87,13 @@ public interface ODataSerializer { /** * Writes complex-type instance data into an InputStream. + * @param metadata Metadata for the service * @param type complex type * @param property property value * @param options options for the serializer */ - InputStream complex(EdmComplexType type, Property property, ComplexSerializerOptions options) - throws SerializerException; + InputStream complex(ServiceMetadata metadata, EdmComplexType type, Property property, + ComplexSerializerOptions options) throws SerializerException; /** * Writes data of a collection of primitive-type instances into an InputStream. @@ -103,10 +106,11 @@ public interface ODataSerializer { /** * Writes data of a collection of complex-type instances into an InputStream. + * @param metadata Metadata for the service * @param type complex type * @param property property value * @param options options for the serializer */ - InputStream complexCollection(EdmComplexType type, Property property, ComplexSerializerOptions options) - throws SerializerException; + InputStream complexCollection(ServiceMetadata metadata, EdmComplexType type, Property property, + ComplexSerializerOptions options) throws SerializerException; } diff --git a/lib/server-api/src/main/java/org/apache/olingo/server/api/serializer/SerializerException.java b/lib/server-api/src/main/java/org/apache/olingo/server/api/serializer/SerializerException.java index 1583241b6..a7d067fa6 100644 --- a/lib/server-api/src/main/java/org/apache/olingo/server/api/serializer/SerializerException.java +++ b/lib/server-api/src/main/java/org/apache/olingo/server/api/serializer/SerializerException.java @@ -6,9 +6,9 @@ * 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 @@ -37,7 +37,9 @@ public class SerializerException extends ODataTranslatedException { /** parameter: property name */ INCONSISTENT_PROPERTY_TYPE, /** parameter: property name */ MISSING_PROPERTY, /** parameters: property name, property value */ WRONG_PROPERTY_VALUE, - /** parameters: primitive-type name, value */ WRONG_PRIMITIVE_VALUE; + /** parameters: primitive-type name, value */ WRONG_PRIMITIVE_VALUE, + UNKNOWN_TYPE, + WRONG_BASE_TYPE; @Override public String getKey() { diff --git a/lib/server-core-ext/pom.xml b/lib/server-core-ext/pom.xml new file mode 100644 index 000000000..a5730c01b --- /dev/null +++ b/lib/server-core-ext/pom.xml @@ -0,0 +1,117 @@ + + + + 4.0.0 + + odata-server-core-ext + jar + ${project.artifactId} + + + org.apache.olingo + odata-lib + 4.0.0-beta-03-SNAPSHOT + .. + + + 9.2.7.v20150116 + + + + org.apache.olingo + odata-server-api + ${project.version} + + + org.apache.olingo + odata-server-core + ${project.version} + + + org.apache.olingo + odata-commons-core + ${project.version} + + + org.antlr + antlr4-runtime + + + javax.servlet + javax.servlet-api + provided + + + javax.xml.stream + stax-api + + + junit + junit + + + org.mockito + mockito-all + + + org.slf4j + slf4j-simple + + + commons-io + commons-io + test + + + org.eclipse.jetty + jetty-server + test + ${jetty-version} + + + org.eclipse.jetty + jetty-servlet + ${jetty-version} + test + + + javax.servlet + javax.servlet-api + + + + + org.eclipse.jetty + jetty-http + ${jetty-version} + test + + + org.eclipse.jetty + jetty-client + test + ${jetty-version} + + + + diff --git a/lib/server-core-ext/src/main/java/org/apache/olingo/server/core/ErrorHandler.java b/lib/server-core-ext/src/main/java/org/apache/olingo/server/core/ErrorHandler.java new file mode 100644 index 000000000..199c62d2a --- /dev/null +++ b/lib/server-core-ext/src/main/java/org/apache/olingo/server/core/ErrorHandler.java @@ -0,0 +1,125 @@ +/* + * 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; + +import java.io.ByteArrayInputStream; + +import org.apache.olingo.commons.api.format.ContentType; +import org.apache.olingo.commons.api.format.ODataFormat; +import org.apache.olingo.commons.api.http.HttpHeader; +import org.apache.olingo.commons.api.http.HttpStatusCode; +import org.apache.olingo.server.api.OData; +import org.apache.olingo.server.api.ODataRequest; +import org.apache.olingo.server.api.ODataResponse; +import org.apache.olingo.server.api.ODataServerError; +import org.apache.olingo.server.api.ServiceMetadata; +import org.apache.olingo.server.api.batch.exception.BatchDeserializerException; +import org.apache.olingo.server.api.deserializer.DeserializerException; +import org.apache.olingo.server.api.serializer.CustomContentTypeSupport; +import org.apache.olingo.server.api.serializer.ODataSerializer; +import org.apache.olingo.server.api.serializer.RepresentationType; +import org.apache.olingo.server.api.serializer.SerializerException; +import org.apache.olingo.server.api.uri.UriInfo; +import org.apache.olingo.server.core.uri.parser.Parser; +import org.apache.olingo.server.core.uri.parser.UriParserException; +import org.apache.olingo.server.core.uri.parser.UriParserSemanticException; +import org.apache.olingo.server.core.uri.parser.UriParserSyntaxException; +import org.apache.olingo.server.core.uri.validator.UriValidationException; + +public class ErrorHandler { + private final OData odata; + private final ServiceMetadata metadata; + private final CustomContentTypeSupport customContent; + + public ErrorHandler(OData odata, ServiceMetadata metadata, CustomContentTypeSupport customContent) { + this.odata = odata; + this.metadata = metadata; + this.customContent = customContent; + } + + public void handleException(Exception e, ODataRequest request, ODataResponse response) { + if (e instanceof UriValidationException) { + ODataServerError serverError = ODataExceptionHelper.createServerErrorObject((UriValidationException)e, null); + handleServerError(request, response, serverError); + } else if(e instanceof UriParserSemanticException) { + ODataServerError serverError = ODataExceptionHelper.createServerErrorObject((UriParserSemanticException)e, null); + handleServerError(request, response, serverError); + } else if(e instanceof UriParserSyntaxException) { + ODataServerError serverError = ODataExceptionHelper.createServerErrorObject((UriParserSyntaxException)e, null); + handleServerError(request, response, serverError); + } else if(e instanceof UriParserException) { + ODataServerError serverError = ODataExceptionHelper.createServerErrorObject((UriParserException)e, null); + handleServerError(request, response, serverError); + } else if(e instanceof ContentNegotiatorException) { + ODataServerError serverError = ODataExceptionHelper.createServerErrorObject((ContentNegotiatorException)e, null); + handleServerError(request, response, serverError); + } else if(e instanceof SerializerException) { + ODataServerError serverError = ODataExceptionHelper.createServerErrorObject((SerializerException)e, null); + handleServerError(request, response, serverError); + } else if(e instanceof BatchDeserializerException) { + ODataServerError serverError = ODataExceptionHelper.createServerErrorObject((BatchDeserializerException)e, null); + handleServerError(request, response, serverError); + } else if(e instanceof DeserializerException) { + ODataServerError serverError = ODataExceptionHelper.createServerErrorObject((DeserializerException)e, null); + handleServerError(request, response, serverError); + } else if(e instanceof ODataHandlerException) { + ODataServerError serverError = ODataExceptionHelper.createServerErrorObject((ODataHandlerException)e, null); + handleServerError(request, response, serverError); + } else { + ODataServerError serverError = ODataExceptionHelper.createServerErrorObject(e); + handleServerError(request, response, serverError); + } + } + + void handleServerError(final ODataRequest request, final ODataResponse response, + final ODataServerError serverError) { + ContentType requestedContentType; + try { + UriInfo uriInfo = new Parser().parseUri(request.getRawODataPath(), request.getRawQueryPath(), + null, this.metadata.getEdm()); + requestedContentType = ContentNegotiator.doContentNegotiation(uriInfo.getFormatOption(), + request, this.customContent, RepresentationType.ERROR); + } catch (final ContentNegotiatorException e) { + requestedContentType = ODataFormat.JSON.getContentType(); + } catch (UriParserException e) { + requestedContentType = ODataFormat.JSON.getContentType(); + } + processError(response, serverError, requestedContentType); + } + + void processError(ODataResponse response, ODataServerError serverError, + ContentType requestedContentType) { + try { + ODataSerializer serializer = this.odata.createSerializer(ODataFormat + .fromContentType(requestedContentType)); + response.setContent(serializer.error(serverError)); + response.setStatusCode(serverError.getStatusCode()); + response.setHeader(HttpHeader.CONTENT_TYPE, requestedContentType.toContentTypeString()); + } catch (Exception e) { + // This should never happen but to be sure we have this catch here + // to prevent sending a stacktrace to a client. + String responseContent = "{\"error\":{\"code\":null,\"message\":\"An unexpected exception occurred during " + + "error processing with message: " + e.getMessage() + "\"}}"; //$NON-NLS-1$ //$NON-NLS-2$ + response.setContent(new ByteArrayInputStream(responseContent.getBytes())); + response.setStatusCode(HttpStatusCode.INTERNAL_SERVER_ERROR.getStatusCode()); + response.setHeader(HttpHeader.CONTENT_TYPE, + ContentType.APPLICATION_JSON.toContentTypeString()); + } + } +} diff --git a/lib/server-core-ext/src/main/java/org/apache/olingo/server/core/MetadataParser.java b/lib/server-core-ext/src/main/java/org/apache/olingo/server/core/MetadataParser.java new file mode 100644 index 000000000..e34a28a6f --- /dev/null +++ b/lib/server-core-ext/src/main/java/org/apache/olingo/server/core/MetadataParser.java @@ -0,0 +1,679 @@ +/* + * 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; + +import java.io.Reader; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import javax.xml.namespace.QName; +import javax.xml.stream.XMLEventReader; +import javax.xml.stream.XMLInputFactory; +import javax.xml.stream.XMLStreamException; +import javax.xml.stream.events.Attribute; +import javax.xml.stream.events.EndElement; +import javax.xml.stream.events.StartElement; +import javax.xml.stream.events.XMLEvent; + +import org.apache.olingo.commons.api.edm.FullQualifiedName; +import org.apache.olingo.commons.api.edm.provider.Action; +import org.apache.olingo.commons.api.edm.provider.ActionImport; +import org.apache.olingo.commons.api.edm.provider.ComplexType; +import org.apache.olingo.commons.api.edm.provider.EdmProvider; +import org.apache.olingo.commons.api.edm.provider.EntityContainer; +import org.apache.olingo.commons.api.edm.provider.EntitySet; +import org.apache.olingo.commons.api.edm.provider.EntityType; +import org.apache.olingo.commons.api.edm.provider.EnumMember; +import org.apache.olingo.commons.api.edm.provider.EnumType; +import org.apache.olingo.commons.api.edm.provider.Function; +import org.apache.olingo.commons.api.edm.provider.FunctionImport; +import org.apache.olingo.commons.api.edm.provider.NavigationProperty; +import org.apache.olingo.commons.api.edm.provider.NavigationPropertyBinding; +import org.apache.olingo.commons.api.edm.provider.OnDelete; +import org.apache.olingo.commons.api.edm.provider.OnDeleteAction; +import org.apache.olingo.commons.api.edm.provider.Operation; +import org.apache.olingo.commons.api.edm.provider.Parameter; +import org.apache.olingo.commons.api.edm.provider.Property; +import org.apache.olingo.commons.api.edm.provider.PropertyRef; +import org.apache.olingo.commons.api.edm.provider.ReferentialConstraint; +import org.apache.olingo.commons.api.edm.provider.ReturnType; +import org.apache.olingo.commons.api.edm.provider.Schema; +import org.apache.olingo.commons.api.edm.provider.Singleton; +import org.apache.olingo.commons.api.edm.provider.Term; +import org.apache.olingo.commons.api.edm.provider.TypeDefinition; + +/** + * This class can convert a CSDL document into EDMProvider object + */ +public class MetadataParser { + + public EdmProvider buildEdmProvider(Reader csdl) throws XMLStreamException { + XMLInputFactory xmlInputFactory = XMLInputFactory.newInstance(); + XMLEventReader reader = xmlInputFactory.createXMLEventReader(csdl); + + SchemaBasedEdmProvider provider = new SchemaBasedEdmProvider(); + new ElementReader() { + @Override + void build(XMLEventReader reader, StartElement element, SchemaBasedEdmProvider provider, + String name) throws XMLStreamException { + String version = attr(element, "Version"); + if (version.equals("4.0")) { + readDataServicesAndReference(reader, element, provider); + } + } + }.read(reader, null, provider, "Edmx"); + + return provider; + } + + private void readDataServicesAndReference(XMLEventReader reader, StartElement element, + SchemaBasedEdmProvider provider) throws XMLStreamException { + new ElementReader() { + @Override + void build(XMLEventReader reader, StartElement element, SchemaBasedEdmProvider provider, + String name) throws XMLStreamException { + if (name.equals("DataServices")) { + readSchema(reader, element, provider); + } else if (name.equals("Reference")) { + readReference(reader, element, provider, "Reference"); + } + } + }.read(reader, element, provider, "DataServices", "Reference"); + } + + private void readReference(XMLEventReader reader, StartElement element, + SchemaBasedEdmProvider provider, String name) throws XMLStreamException { + new ElementReader() { + @Override + void build(XMLEventReader reader, StartElement element, SchemaBasedEdmProvider t, String name) + throws XMLStreamException { + // TODO: + } + }.read(reader, element, provider, name); + } + + private void readSchema(XMLEventReader reader, StartElement element, + SchemaBasedEdmProvider provider) throws XMLStreamException { + + Schema schema = new Schema(); + schema.setComplexTypes(new ArrayList()); + schema.setActions(new ArrayList()); + schema.setEntityTypes(new ArrayList()); + schema.setEnumTypes(new ArrayList()); + schema.setFunctions(new ArrayList()); + schema.setTerms(new ArrayList()); + schema.setTypeDefinitions(new ArrayList()); + + new ElementReader() { + @Override + void build(XMLEventReader reader, StartElement element, Schema schema, String name) + throws XMLStreamException { + schema.setNamespace(attr(element, "Namespace")); + schema.setAlias(attr(element, "Alias")); + readSchemaContents(reader, schema); + } + }.read(reader, element, schema, "Schema"); + provider.addSchema(schema); + } + + private void readSchemaContents(XMLEventReader reader, Schema schema) throws XMLStreamException { + new ElementReader() { + @Override + void build(XMLEventReader reader, StartElement element, Schema schema, String name) + throws XMLStreamException { + if (name.equals("Action")) { + readAction(reader, element, schema); + } else if (name.equals("Annotations")) { + // TODO: + } else if (name.equals("Annotation")) { + // TODO: + } else if (name.equals("ComplexType")) { + readComplexType(reader, element, schema); + } else if (name.equals("EntityContainer")) { + readEntityContainer(reader, element, schema); + } else if (name.equals("EntityType")) { + readEntityType(reader, element, schema); + } else if (name.equals("EnumType")) { + readEnumType(reader, element, schema); + } else if (name.equals("Function")) { + readFunction(reader, element, schema); + } else if (name.equals("Term")) { + schema.getTerms().add(readTerm(element)); + } else if (name.equals("TypeDefinition")) { + schema.getTypeDefinitions().add(readTypeDefinition(element)); + } + } + }.read(reader, null, schema, "Action", "Annotations", "Annotation", "ComplexType", + "EntityContainer", "EntityType", "EnumType", "Function", "Term", "TypeDefinition"); + } + + private void readAction(XMLEventReader reader, StartElement element, Schema schema) + throws XMLStreamException { + + Action action = new Action(); + action.setParameters(new ArrayList()); + action.setName(attr(element, "Name")); + action.setBound(Boolean.parseBoolean(attr(element, "IsBound"))); + String entitySetPath = attr(element, "EntitySetPath"); + if (entitySetPath != null) { + // TODO: need to parse into binding and path. + action.setEntitySetPath(entitySetPath); + } + readOperationParameters(reader, action); + schema.getActions().add(action); + } + + private FullQualifiedName readType(StartElement element) { + String type = attr(element, "Type"); + if (type.startsWith("Collection(") && type.endsWith(")")) { + return new FullQualifiedName(type.substring(11, type.length() - 1)); + } + return new FullQualifiedName(type); + } + + private boolean isCollectionType(StartElement element) { + String type = attr(element, "Type"); + if (type.startsWith("Collection(") && type.endsWith(")")) { + return true; + } + return false; + } + + private void readReturnType(StartElement element, Operation operation) { + ReturnType returnType = new ReturnType(); + returnType.setType(readType(element)); + returnType.setCollection(isCollectionType(element)); + returnType.setNullable(Boolean.parseBoolean(attr(element, "Nullable"))); + + String maxLength = attr(element, "MaxLength"); + if (maxLength != null) { + returnType.setMaxLength(Integer.parseInt(maxLength)); + } + String precision = attr(element, "Precision"); + if (precision != null) { + returnType.setPrecision(Integer.parseInt(precision)); + } + String scale = attr(element, "Scale"); + if (scale != null) { + returnType.setScale(Integer.parseInt(scale)); + } + String srid = attr(element, "SRID"); + if (srid != null) { + // TODO: no olingo support yet. + } + operation.setReturnType(returnType); + } + + private void readParameter(StartElement element, Operation operation) { + Parameter parameter = new Parameter(); + parameter.setName(attr(element, "Name")); + parameter.setType(readType(element)); + parameter.setCollection(isCollectionType(element)); + parameter.setNullable(Boolean.parseBoolean(attr(element, "Nullable"))); + + String maxLength = attr(element, "MaxLength"); + if (maxLength != null) { + parameter.setMaxLength(Integer.parseInt(maxLength)); + } + String precision = attr(element, "Precision"); + if (precision != null) { + parameter.setPrecision(Integer.parseInt(precision)); + } + String scale = attr(element, "Scale"); + if (scale != null) { + parameter.setScale(Integer.parseInt(scale)); + } + String srid = attr(element, "SRID"); + if (srid != null) { + // TODO: no olingo support yet. + } + operation.getParameters().add(parameter); + } + + private TypeDefinition readTypeDefinition(StartElement element) { + TypeDefinition td = new TypeDefinition(); + td.setName(attr(element, "Name")); + td.setUnderlyingType(new FullQualifiedName(attr(element, "UnderlyingType"))); + td.setUnicode(Boolean.parseBoolean(attr(element, "Unicode"))); + + String maxLength = attr(element, "MaxLength"); + if (maxLength != null) { + td.setMaxLength(Integer.parseInt(maxLength)); + } + String precision = attr(element, "Precision"); + if (precision != null) { + td.setPrecision(Integer.parseInt(precision)); + } + String scale = attr(element, "Scale"); + if (scale != null) { + td.setScale(Integer.parseInt(scale)); + } + String srid = attr(element, "SRID"); + if (srid != null) { + // TODO: no olingo support yet. + } + return td; + } + + private Term readTerm(StartElement element) { + Term term = new Term(); + term.setName(attr(element, "Name")); + term.setType(attr(element, "Type")); + if (attr(element, "BaseTerm") != null) { + term.setBaseTerm(attr(element, "BaseTerm")); + } + if (attr(element, "DefaultValue") != null) { + term.setDefaultValue(attr(element, "DefaultValue")); + } + if (attr(element, "AppliesTo") != null) { + term.setAppliesTo(Arrays.asList(attr(element, "AppliesTo"))); + } + term.setNullable(Boolean.parseBoolean(attr(element, "Nullable"))); + String maxLength = attr(element, "MaxLength"); + if (maxLength != null) { + term.setMaxLength(Integer.parseInt(maxLength)); + } + String precision = attr(element, "Precision"); + if (precision != null) { + term.setPrecision(Integer.parseInt(precision)); + } + String scale = attr(element, "Scale"); + if (scale != null) { + term.setScale(Integer.parseInt(scale)); + } + String srid = attr(element, "SRID"); + if (srid != null) { + // TODO: no olingo support yet. + } + return term; + } + + private void readFunction(XMLEventReader reader, StartElement element, Schema schema) + throws XMLStreamException { + Function function = new Function(); + function.setParameters(new ArrayList()); + function.setName(attr(element, "Name")); + function.setBound(Boolean.parseBoolean(attr(element, "IsBound"))); + function.setComposable(Boolean.parseBoolean(attr(element, "IsComposable"))); + String entitySetPath = attr(element, "EntitySetPath"); + if (entitySetPath != null) { + // TODO: need to parse into binding and path. + function.setEntitySetPath(entitySetPath); + } + readOperationParameters(reader, function); + schema.getFunctions().add(function); + } + + private void readOperationParameters(XMLEventReader reader, final Operation operation) + throws XMLStreamException { + new ElementReader() { + @Override + void build(XMLEventReader reader, StartElement element, Operation operation, String name) + throws XMLStreamException { + if (name.equals("Parameter")) { + readParameter(element, operation); + } else if (name.equals("ReturnType")) { + readReturnType(element, operation); + } + } + }.read(reader, null, operation, "Parameter", "ReturnType"); + } + + private void readEnumType(XMLEventReader reader, StartElement element, Schema schema) + throws XMLStreamException { + EnumType type = new EnumType(); + type.setMembers(new ArrayList()); + type.setName(attr(element, "Name")); + if (attr(element, "UnderlyingType") != null) { + type.setUnderlyingType(new FullQualifiedName(attr(element, "UnderlyingType"))); + } + type.setFlags(Boolean.parseBoolean(attr(element, "IsFlags"))); + + readEnumMembers(reader, element, type); + schema.getEnumTypes().add(type); + } + + private void readEnumMembers(XMLEventReader reader, StartElement element, EnumType type) + throws XMLStreamException { + new ElementReader() { + @Override + void build(XMLEventReader reader, StartElement element, EnumType type, String name) + throws XMLStreamException { + EnumMember member = new EnumMember(); + member.setName(attr(element, "Name")); + member.setValue(attr(element, "Value")); + type.getMembers().add(member); + } + }.read(reader, element, type, "Member"); + } + + private void readEntityType(XMLEventReader reader, StartElement element, Schema schema) + throws XMLStreamException { + EntityType entityType = new EntityType(); + entityType.setProperties(new ArrayList()); + entityType.setNavigationProperties(new ArrayList()); + entityType.setKey(new ArrayList()); + entityType.setName(attr(element, "Name")); + if (attr(element, "BaseType") != null) { + entityType.setBaseType(new FullQualifiedName(attr(element, "BaseType"))); + } + entityType.setAbstract(Boolean.parseBoolean(attr(element, "Abstract"))); + entityType.setOpenType(Boolean.parseBoolean(attr(element, "OpenType"))); + entityType.setHasStream(Boolean.parseBoolean(attr(element, "HasStream"))); + readEntityProperties(reader, entityType); + schema.getEntityTypes().add(entityType); + } + + private void readEntityProperties(XMLEventReader reader, EntityType entityType) + throws XMLStreamException { + new ElementReader() { + @Override + void build(XMLEventReader reader, StartElement element, EntityType entityType, String name) + throws XMLStreamException { + if (name.equals("Property")) { + entityType.getProperties().add(readProperty(element)); + } else if (name.equals("NavigationProperty")) { + entityType.getNavigationProperties().add(readNavigationProperty(reader, element)); + } else if (name.equals("Key")) { + readKey(reader, element, entityType); + } + } + }.read(reader, null, entityType, "Property", "NavigationProperty", "Key"); + } + + private void readKey(XMLEventReader reader, StartElement element, EntityType entityType) + throws XMLStreamException { + new ElementReader() { + @Override + void build(XMLEventReader reader, StartElement element, EntityType entityType, String name) + throws XMLStreamException { + PropertyRef ref = new PropertyRef(); + ref.setName(attr(element, "Name")); + ref.setAlias(attr(element, "Alias")); + entityType.getKey().add(ref); + } + }.read(reader, element, entityType, "PropertyRef"); + } + + private NavigationProperty readNavigationProperty(XMLEventReader reader, StartElement element) + throws XMLStreamException { + NavigationProperty property = new NavigationProperty(); + property.setReferentialConstraints(new ArrayList()); + + property.setName(attr(element, "Name")); + property.setType(readType(element)); + property.setCollection(isCollectionType(element)); + property.setNullable(Boolean.parseBoolean(attr(element, "Nullable"))); + property.setPartner(attr(element, "Partner")); + property.setContainsTarget(Boolean.parseBoolean(attr(element, "ContainsTarget"))); + + new ElementReader() { + @Override + void build(XMLEventReader reader, StartElement element, NavigationProperty property, + String name) throws XMLStreamException { + if (name.equals("ReferentialConstraint")) { + ReferentialConstraint constraint = new ReferentialConstraint(); + constraint.setProperty(attr(element, "Property")); + constraint.setReferencedProperty(attr(element, "ReferencedProperty")); + property.getReferentialConstraints().add(constraint); + } else if (name.equals("OnDelete")) { + property.setOnDelete(new OnDelete().setAction(OnDeleteAction.valueOf(attr(element, "Action")))); + } + } + }.read(reader, element, property, "ReferentialConstraint", "OnDelete"); + return property; + } + + private String attr(StartElement element, String name) { + Attribute attr = element.getAttributeByName(new QName(name)); + if (attr != null) { + return attr.getValue(); + } + return null; + } + + private Property readProperty(StartElement element) { + Property property = new Property(); + property.setName(attr(element, "Name")); + property.setType(readType(element)); + property.setCollection(isCollectionType(element)); + property.setNullable(Boolean.parseBoolean(attr(element, "Nullable") == null ? "true" : attr( + element, "Nullable"))); + property.setUnicode(Boolean.parseBoolean(attr(element, "Unicode"))); + + String maxLength = attr(element, "MaxLength"); + if (maxLength != null) { + property.setMaxLength(Integer.parseInt(maxLength)); + } + String precision = attr(element, "Precision"); + if (precision != null) { + property.setPrecision(Integer.parseInt(precision)); + } + String scale = attr(element, "Scale"); + if (scale != null) { + property.setScale(Integer.parseInt(scale)); + } + String srid = attr(element, "SRID"); + if (srid != null) { + // TODO: no olingo support yet. + } + String defaultValue = attr(element, "DefaultValue"); + if (defaultValue != null) { + property.setDefaultValue(defaultValue); + } + return property; + } + + private void readEntityContainer(XMLEventReader reader, StartElement element, Schema schema) + throws XMLStreamException { + final EntityContainer container = new EntityContainer(); + container.setName(attr(element, "Name")); + if (attr(element, "Extends") != null) { + container.setExtendsContainer(attr(element, "Extends")); + } + container.setActionImports(new ArrayList()); + container.setFunctionImports(new ArrayList()); + container.setEntitySets(new ArrayList()); + container.setSingletons(new ArrayList()); + + new ElementReader() { + @Override + void build(XMLEventReader reader, StartElement element, Schema schema, String name) + throws XMLStreamException { + if (name.equals("EntitySet")) { + readEntitySet(reader, element, container); + } else if (name.equals("Singleton")) { + readSingleton(reader, element, container); + } else if (name.equals("ActionImport")) { + readActionImport(element, container); + } else if (name.equals("FunctionImport")) { + readFunctionImport(element, container); + } + } + + private void readFunctionImport(StartElement element, EntityContainer container) { + FunctionImport functionImport = new FunctionImport(); + functionImport.setName(attr(element, "Name")); + functionImport.setFunction(new FullQualifiedName(attr(element, "Function"))); + functionImport.setIncludeInServiceDocument(Boolean.parseBoolean(attr(element, + "IncludeInServiceDocument"))); + + String entitySet = attr(element, "EntitySet"); + if (entitySet != null) { + functionImport.setEntitySet(entitySet); + } + container.getFunctionImports().add(functionImport); + } + + private void readActionImport(StartElement element, EntityContainer container) { + ActionImport actionImport = new ActionImport(); + actionImport.setName(attr(element, "Name")); + actionImport.setAction(new FullQualifiedName(attr(element, "Action"))); + + String entitySet = attr(element, "EntitySet"); + if (entitySet != null) { + actionImport.setEntitySet(entitySet); + } + container.getActionImports().add(actionImport); + } + + private void readSingleton(XMLEventReader reader, StartElement element, + EntityContainer container) throws XMLStreamException { + Singleton singleton = new Singleton(); + singleton.setNavigationPropertyBindings(new ArrayList()); + singleton.setName(attr(element, "Name")); + singleton.setType(new FullQualifiedName(attr(element, "Type"))); + singleton.setNavigationPropertyBindings(new ArrayList()); + readNavigationPropertyBindings(reader, element, singleton.getNavigationPropertyBindings()); + container.getSingletons().add(singleton); + } + + private void readEntitySet(XMLEventReader reader, StartElement element, + EntityContainer container) throws XMLStreamException { + EntitySet entitySet = new EntitySet(); + entitySet.setName(attr(element, "Name")); + entitySet.setType(new FullQualifiedName(attr(element, "EntityType"))); + entitySet.setIncludeInServiceDocument(Boolean.parseBoolean(attr(element, + "IncludeInServiceDocument"))); + entitySet.setNavigationPropertyBindings(new ArrayList()); + readNavigationPropertyBindings(reader, element, entitySet.getNavigationPropertyBindings()); + container.getEntitySets().add(entitySet); + } + + private void readNavigationPropertyBindings(XMLEventReader reader, StartElement element, + List bindings) throws XMLStreamException { + new ElementReader>() { + @Override + void build(XMLEventReader reader, StartElement element, + List bindings, String name) throws XMLStreamException { + NavigationPropertyBinding binding = new NavigationPropertyBinding(); + binding.setPath(attr(element, "Path")); + binding.setTarget(attr(element, "Target")); + bindings.add(binding); + } + + }.read(reader, element, bindings, "NavigationPropertyBinding"); + ; + } + }.read(reader, element, schema, "EntitySet", "Singleton", "ActionImport", "FunctionImport"); + schema.setEntityContainer(container); + } + + private void readComplexType(XMLEventReader reader, StartElement element, Schema schema) + throws XMLStreamException { + ComplexType complexType = new ComplexType(); + complexType.setProperties(new ArrayList()); + complexType.setNavigationProperties(new ArrayList()); + complexType.setName(attr(element, "Name")); + if (attr(element, "BaseType") != null) { + complexType.setBaseType(new FullQualifiedName(attr(element, "BaseType"))); + } + complexType.setAbstract(Boolean.parseBoolean(attr(element, "Abstract"))); + complexType.setOpenType(Boolean.parseBoolean(attr(element, "OpenType"))); + readProperties(reader, complexType); + + schema.getComplexTypes().add(complexType); + } + + private void readProperties(XMLEventReader reader, ComplexType complexType) + throws XMLStreamException { + new ElementReader() { + @Override + void build(XMLEventReader reader, StartElement element, ComplexType complexType, String name) + throws XMLStreamException { + if (name.equals("Property")) { + complexType.getProperties().add(readProperty(element)); + } else if (name.equals("NavigationProperty")) { + complexType.getNavigationProperties().add(readNavigationProperty(reader, element)); + } + } + }.read(reader, null, complexType, "Property", "NavigationProperty"); + } + + abstract class ElementReader { + void read(XMLEventReader reader, StartElement element, T t, String... names) + throws XMLStreamException { + while (reader.hasNext()) { + XMLEvent event = reader.peek(); + + event = skipAnnotations(reader, event); + + if (!event.isStartElement() && !event.isEndElement()) { + reader.nextEvent(); + continue; + } + + boolean hit = false; + + for (int i = 0; i < names.length; i++) { + if (event.isStartElement()) { + element = event.asStartElement(); + if (element.getName().getLocalPart().equals(names[i])) { + reader.nextEvent(); // advance cursor + // System.out.println("reading = "+names[i]); + build(reader, element, t, names[i]); + hit = true; + } + } + if (event.isEndElement()) { + EndElement e = event.asEndElement(); + if (e.getName().getLocalPart().equals(names[i])) { + reader.nextEvent(); // advance cursor + // System.out.println("done reading = "+names[i]); + hit = true; + } + } + } + if (!hit) { + break; + } + } + } + + private XMLEvent skipAnnotations(XMLEventReader reader, XMLEvent event) + throws XMLStreamException { + boolean skip = false; + + while (reader.hasNext()) { + if (event.isStartElement()) { + StartElement element = event.asStartElement(); + if (element.getName().getLocalPart().equals("Annotation")) { + skip = true; + } + } + if (event.isEndElement()) { + EndElement element = event.asEndElement(); + if (element.getName().getLocalPart().equals("Annotation")) { + return reader.peek(); + } + } + if (skip) { + event = reader.nextEvent(); + } else { + return event; + } + } + return event; + } + + abstract void build(XMLEventReader reader, StartElement element, T t, String name) + throws XMLStreamException; + } +} diff --git a/lib/server-core-ext/src/main/java/org/apache/olingo/server/core/OData4HttpHandler.java b/lib/server-core-ext/src/main/java/org/apache/olingo/server/core/OData4HttpHandler.java new file mode 100644 index 000000000..ddb8e6b78 --- /dev/null +++ b/lib/server-core-ext/src/main/java/org/apache/olingo/server/core/OData4HttpHandler.java @@ -0,0 +1,123 @@ +/* + * 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; + +import java.io.IOException; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.apache.olingo.commons.api.edm.constants.ODataServiceVersion; +import org.apache.olingo.commons.api.http.HttpHeader; +import org.apache.olingo.server.api.OData; +import org.apache.olingo.server.api.ODataRequest; +import org.apache.olingo.server.api.ODataResponse; +import org.apache.olingo.server.api.ODataTranslatedException; +import org.apache.olingo.server.api.ServiceMetadata; +import org.apache.olingo.server.api.processor.Processor; +import org.apache.olingo.server.api.serializer.CustomContentTypeSupport; +import org.apache.olingo.server.api.serializer.SerializerException; +import org.apache.olingo.server.core.legacy.ProcessorServiceHandler; + +public class OData4HttpHandler extends ODataHttpHandlerImpl { + private ServiceHandler handler; + private final ServiceMetadata serviceMetadata; + private final OData odata; + private CustomContentTypeSupport customContentTypeSupport; + + + public OData4HttpHandler(OData odata, ServiceMetadata serviceMetadata) { + super(odata, serviceMetadata); + this.odata = odata; + this.serviceMetadata = serviceMetadata; + // this is support old interfaces + this.handler = new ProcessorServiceHandler(); + this.handler.init(odata, serviceMetadata); + } + + @Override + public void process(final HttpServletRequest httpRequest, final HttpServletResponse httpResponse) { + ODataRequest request = null; + ODataResponse response = new ODataResponse(); + + try { + request = createODataRequest(httpRequest, 0); + validateODataVersion(request, response); + + ServiceDispatcher dispatcher = new ServiceDispatcher(this.odata, this.serviceMetadata, + handler, this.customContentTypeSupport); + dispatcher.execute(request, response); + + } catch (Exception e) { + ErrorHandler handler = new ErrorHandler(this.odata, this.serviceMetadata, + this.customContentTypeSupport); + handler.handleException(e, request, response); + } + convertToHttp(httpResponse, response); + } + + + ODataRequest createODataRequest(final HttpServletRequest httpRequest, final int split) + throws ODataTranslatedException { + try { + ODataRequest odRequest = new ODataRequest(); + + odRequest.setBody(httpRequest.getInputStream()); + extractHeaders(odRequest, httpRequest); + extractMethod(odRequest, httpRequest); + extractUri(odRequest, httpRequest, split); + + return odRequest; + } catch (final IOException e) { + throw new SerializerException( + "An I/O exception occurred.", e, SerializerException.MessageKeys.IO_EXCEPTION); //$NON-NLS-1$ + } + } + + void validateODataVersion(final ODataRequest request, final ODataResponse response) + throws ODataHandlerException { + final String maxVersion = request.getHeader(HttpHeader.ODATA_MAX_VERSION); + response.setHeader(HttpHeader.ODATA_VERSION, ODataServiceVersion.V40.toString()); + + if (maxVersion != null) { + if (ODataServiceVersion.isBiggerThan(ODataServiceVersion.V40.toString(), maxVersion)) { + throw new ODataHandlerException("ODataVersion not supported: " + maxVersion, //$NON-NLS-1$ + ODataHandlerException.MessageKeys.ODATA_VERSION_NOT_SUPPORTED, maxVersion); + } + } + } + + @Override + public void register(final Processor processor) { + + if (processor instanceof ServiceHandler) { + this.handler = (ServiceHandler) processor; + this.handler.init(this.odata, this.serviceMetadata); + } + + if (this.handler instanceof ProcessorServiceHandler) { + ((ProcessorServiceHandler)this.handler).register(processor); + } + } + + @Override + public void register(final CustomContentTypeSupport customContentTypeSupport) { + this.customContentTypeSupport = customContentTypeSupport; + } +} diff --git a/lib/server-core-ext/src/main/java/org/apache/olingo/server/core/OData4Impl.java b/lib/server-core-ext/src/main/java/org/apache/olingo/server/core/OData4Impl.java new file mode 100644 index 000000000..bde9c966c --- /dev/null +++ b/lib/server-core-ext/src/main/java/org/apache/olingo/server/core/OData4Impl.java @@ -0,0 +1,45 @@ +/* + * 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; + +import org.apache.olingo.commons.api.ODataRuntimeException; +import org.apache.olingo.server.api.OData; +import org.apache.olingo.server.api.ODataHttpHandler; +import org.apache.olingo.server.api.ServiceMetadata; + +public class OData4Impl extends ODataImpl { + + public static OData newInstance() { + try { + final Class clazz = Class.forName(OData4Impl.class.getName()); + final Object object = clazz.newInstance(); + return (OData) object; + } catch (final Exception e) { + throw new ODataRuntimeException(e); + } + } + + private OData4Impl() { + } + + @Override + public ODataHttpHandler createHandler(final ServiceMetadata edm) { + return new OData4HttpHandler(this, edm); + } +} diff --git a/lib/server-core-ext/src/main/java/org/apache/olingo/server/core/RequestURLHierarchyVisitor.java b/lib/server-core-ext/src/main/java/org/apache/olingo/server/core/RequestURLHierarchyVisitor.java new file mode 100644 index 000000000..ee0063819 --- /dev/null +++ b/lib/server-core-ext/src/main/java/org/apache/olingo/server/core/RequestURLHierarchyVisitor.java @@ -0,0 +1,333 @@ +/* + * 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; + +import java.util.List; + +import org.apache.olingo.commons.api.edm.EdmEntityType; +import org.apache.olingo.server.api.uri.UriInfo; +import org.apache.olingo.server.api.uri.UriInfoAll; +import org.apache.olingo.server.api.uri.UriInfoBatch; +import org.apache.olingo.server.api.uri.UriInfoCrossjoin; +import org.apache.olingo.server.api.uri.UriInfoEntityId; +import org.apache.olingo.server.api.uri.UriInfoKind; +import org.apache.olingo.server.api.uri.UriInfoMetadata; +import org.apache.olingo.server.api.uri.UriInfoResource; +import org.apache.olingo.server.api.uri.UriInfoService; +import org.apache.olingo.server.api.uri.UriResource; +import org.apache.olingo.server.api.uri.UriResourceAction; +import org.apache.olingo.server.api.uri.UriResourceComplexProperty; +import org.apache.olingo.server.api.uri.UriResourceCount; +import org.apache.olingo.server.api.uri.UriResourceEntitySet; +import org.apache.olingo.server.api.uri.UriResourceFunction; +import org.apache.olingo.server.api.uri.UriResourceIt; +import org.apache.olingo.server.api.uri.UriResourceLambdaAll; +import org.apache.olingo.server.api.uri.UriResourceLambdaAny; +import org.apache.olingo.server.api.uri.UriResourceLambdaVariable; +import org.apache.olingo.server.api.uri.UriResourceNavigation; +import org.apache.olingo.server.api.uri.UriResourcePrimitiveProperty; +import org.apache.olingo.server.api.uri.UriResourceRef; +import org.apache.olingo.server.api.uri.UriResourceRoot; +import org.apache.olingo.server.api.uri.UriResourceSingleton; +import org.apache.olingo.server.api.uri.UriResourceValue; +import org.apache.olingo.server.api.uri.queryoption.CountOption; +import org.apache.olingo.server.api.uri.queryoption.ExpandOption; +import org.apache.olingo.server.api.uri.queryoption.FilterOption; +import org.apache.olingo.server.api.uri.queryoption.FormatOption; +import org.apache.olingo.server.api.uri.queryoption.IdOption; +import org.apache.olingo.server.api.uri.queryoption.OrderByOption; +import org.apache.olingo.server.api.uri.queryoption.SearchOption; +import org.apache.olingo.server.api.uri.queryoption.SelectOption; +import org.apache.olingo.server.api.uri.queryoption.SkipOption; +import org.apache.olingo.server.api.uri.queryoption.SkipTokenOption; +import org.apache.olingo.server.api.uri.queryoption.TopOption; + +public class RequestURLHierarchyVisitor implements RequestURLVisitor { + + private UriInfo uriInfo; + + public UriInfo getUriInfo() { + return this.uriInfo; + } + + @Override + public void visit(UriInfo info) { + this.uriInfo = info; + + UriInfoKind kind = info.getKind(); + switch (kind) { + case all: + visit(info.asUriInfoAll()); + break; + case batch: + visit(info.asUriInfoBatch()); + break; + case crossjoin: + visit(info.asUriInfoCrossjoin()); + break; + case entityId: + visit(info.asUriInfoEntityId()); + break; + case metadata: + visit(info.asUriInfoMetadata()); + break; + case resource: + visit(info.asUriInfoResource()); + break; + case service: + visit(info.asUriInfoService()); + break; + } + } + + @Override + public void visit(UriInfoService info) { + } + + @Override + public void visit(UriInfoAll info) { + } + + @Override + public void visit(UriInfoBatch info) { + } + + @Override + public void visit(UriInfoCrossjoin info) { + } + + @Override + public void visit(UriInfoEntityId info) { + visit(info.getSelectOption()); + + if (info.getExpandOption() != null) { + visit(info.getExpandOption()); + } + if (info.getFormatOption() != null) { + visit(info.getFormatOption()); + } + if (info.getIdOption() != null) { + visit(info.getIdOption(), info.getEntityTypeCast()); + } + } + + @Override + public void visit(UriInfoMetadata info) { + } + + @Override + public void visit(UriInfoResource info) { + List parts = info.getUriResourceParts(); + for (UriResource resource : parts) { + switch (resource.getKind()) { + case action: + visit((UriResourceAction) resource); + break; + case complexProperty: + visit((UriResourceComplexProperty) resource); + break; + case count: + visit((UriResourceCount) resource); + break; + case entitySet: + visit((UriResourceEntitySet) resource); + break; + case function: + visit((UriResourceFunction) resource); + break; + case it: + visit((UriResourceIt) resource); + break; + case lambdaAll: + visit((UriResourceLambdaAll) resource); + break; + case lambdaAny: + visit((UriResourceLambdaAny) resource); + break; + case lambdaVariable: + visit((UriResourceLambdaVariable) resource); + break; + case navigationProperty: + visit((UriResourceNavigation) resource); + break; + case ref: + visit((UriResourceRef) resource); + break; + case root: + visit((UriResourceRoot) resource); + break; + case primitiveProperty: + visit((UriResourcePrimitiveProperty) resource); + break; + case singleton: + visit((UriResourceSingleton) resource); + break; + case value: + visit((UriResourceValue) resource); + break; + } + } + + // http://docs.oasis-open.org/odata/odata/v4.0/os/part1-protocol/odata-v4.0-os-part1-protocol.html#_Toc372793682 + if (info.getSearchOption() != null) { + visit(info.getSearchOption()); + } + + if (info.getFilterOption() != null) { + visit(info.getFilterOption()); + } + + if (info.getCountOption() != null) { + visit(info.getCountOption()); + } + + visit(info.getOrderByOption()); + + if (info.getSkipOption() != null) { + visit(info.getSkipOption()); + } + + if (info.getTopOption() != null) { + visit(info.getTopOption()); + } + + if (info.getExpandOption() != null) { + visit(info.getExpandOption()); + } + + visit(info.getSelectOption()); + + if (info.getFormatOption() != null) { + visit(info.getFormatOption()); + } + + if (info.getIdOption() != null) { + visit(info.getIdOption(), null); + } + + if (info.getSkipTokenOption() != null) { + visit(info.getSkipTokenOption()); + } + + } + + @Override + public void visit(ExpandOption option) { + } + + @Override + public void visit(FilterOption info) { + } + + @Override + public void visit(FormatOption info) { + } + + @Override + public void visit(IdOption info, EdmEntityType type) { + } + + @Override + public void visit(CountOption info) { + } + + @Override + public void visit(OrderByOption option) { + } + + @Override + public void visit(SearchOption option) { + } + + @Override + public void visit(SelectOption option) { + } + + @Override + public void visit(SkipOption option) { + } + + @Override + public void visit(SkipTokenOption option) { + } + + @Override + public void visit(TopOption option) { + } + + @Override + public void visit(UriResourceCount option) { + } + + @Override + public void visit(UriResourceRef info) { + } + + @Override + public void visit(UriResourceRoot info) { + } + + @Override + public void visit(UriResourceValue info) { + } + + @Override + public void visit(UriResourceAction info) { + } + + @Override + public void visit(UriResourceEntitySet info) { + } + + @Override + public void visit(UriResourceFunction info) { + } + + @Override + public void visit(UriResourceIt info) { + } + + @Override + public void visit(UriResourceLambdaAll info) { + } + + @Override + public void visit(UriResourceLambdaAny info) { + } + + @Override + public void visit(UriResourceLambdaVariable info) { + } + + @Override + public void visit(UriResourceNavigation info) { + } + + @Override + public void visit(UriResourceSingleton info) { + } + + @Override + public void visit(UriResourceComplexProperty info) { + } + + @Override + public void visit(UriResourcePrimitiveProperty info) { + } +} diff --git a/lib/server-core-ext/src/main/java/org/apache/olingo/server/core/RequestURLVisitor.java b/lib/server-core-ext/src/main/java/org/apache/olingo/server/core/RequestURLVisitor.java new file mode 100644 index 000000000..f3f4027f4 --- /dev/null +++ b/lib/server-core-ext/src/main/java/org/apache/olingo/server/core/RequestURLVisitor.java @@ -0,0 +1,127 @@ +/* + * 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; + +import org.apache.olingo.commons.api.edm.EdmEntityType; +import org.apache.olingo.server.api.uri.UriInfo; +import org.apache.olingo.server.api.uri.UriInfoAll; +import org.apache.olingo.server.api.uri.UriInfoBatch; +import org.apache.olingo.server.api.uri.UriInfoCrossjoin; +import org.apache.olingo.server.api.uri.UriInfoEntityId; +import org.apache.olingo.server.api.uri.UriInfoMetadata; +import org.apache.olingo.server.api.uri.UriInfoResource; +import org.apache.olingo.server.api.uri.UriInfoService; +import org.apache.olingo.server.api.uri.UriResourceAction; +import org.apache.olingo.server.api.uri.UriResourceComplexProperty; +import org.apache.olingo.server.api.uri.UriResourceCount; +import org.apache.olingo.server.api.uri.UriResourceEntitySet; +import org.apache.olingo.server.api.uri.UriResourceFunction; +import org.apache.olingo.server.api.uri.UriResourceIt; +import org.apache.olingo.server.api.uri.UriResourceLambdaAll; +import org.apache.olingo.server.api.uri.UriResourceLambdaAny; +import org.apache.olingo.server.api.uri.UriResourceLambdaVariable; +import org.apache.olingo.server.api.uri.UriResourceNavigation; +import org.apache.olingo.server.api.uri.UriResourcePrimitiveProperty; +import org.apache.olingo.server.api.uri.UriResourceRef; +import org.apache.olingo.server.api.uri.UriResourceRoot; +import org.apache.olingo.server.api.uri.UriResourceSingleton; +import org.apache.olingo.server.api.uri.UriResourceValue; +import org.apache.olingo.server.api.uri.queryoption.CountOption; +import org.apache.olingo.server.api.uri.queryoption.ExpandOption; +import org.apache.olingo.server.api.uri.queryoption.FilterOption; +import org.apache.olingo.server.api.uri.queryoption.FormatOption; +import org.apache.olingo.server.api.uri.queryoption.IdOption; +import org.apache.olingo.server.api.uri.queryoption.OrderByOption; +import org.apache.olingo.server.api.uri.queryoption.SearchOption; +import org.apache.olingo.server.api.uri.queryoption.SelectOption; +import org.apache.olingo.server.api.uri.queryoption.SkipOption; +import org.apache.olingo.server.api.uri.queryoption.SkipTokenOption; +import org.apache.olingo.server.api.uri.queryoption.TopOption; + +public interface RequestURLVisitor { + + void visit(UriInfo info); + + void visit(UriInfoService info); + + void visit(UriInfoAll info); + + void visit(UriInfoBatch info); + + void visit(UriInfoCrossjoin info); + + void visit(UriInfoEntityId info); + + void visit(UriInfoMetadata info); + + void visit(UriInfoResource info); + + // Walk UriInfoResource + void visit(ExpandOption option); + + void visit(FilterOption info); + + void visit(FormatOption info); + + void visit(IdOption info, EdmEntityType type); + + void visit(CountOption info); + + void visit(OrderByOption option); + + void visit(SearchOption option); + + void visit(SelectOption option); + + void visit(SkipOption option); + + void visit(SkipTokenOption option); + + void visit(TopOption option); + + void visit(UriResourceCount option); + + void visit(UriResourceRef info); + + void visit(UriResourceRoot info); + + void visit(UriResourceValue info); + + void visit(UriResourceAction info); + + void visit(UriResourceEntitySet info); + + void visit(UriResourceFunction info); + + void visit(UriResourceIt info); + + void visit(UriResourceLambdaAll info); + + void visit(UriResourceLambdaAny info); + + void visit(UriResourceLambdaVariable info); + + void visit(UriResourceNavigation info); + + void visit(UriResourceSingleton info); + + void visit(UriResourceComplexProperty info); + + void visit(UriResourcePrimitiveProperty info); +} diff --git a/lib/server-core-ext/src/main/java/org/apache/olingo/server/core/ReturnRepresentation.java b/lib/server-core-ext/src/main/java/org/apache/olingo/server/core/ReturnRepresentation.java new file mode 100644 index 000000000..e9a213ec3 --- /dev/null +++ b/lib/server-core-ext/src/main/java/org/apache/olingo/server/core/ReturnRepresentation.java @@ -0,0 +1,23 @@ +/* + * 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; + +public enum ReturnRepresentation { + REPRESENTATION, MINIMAL +} \ No newline at end of file diff --git a/lib/server-core-ext/src/main/java/org/apache/olingo/server/core/SchemaBasedEdmProvider.java b/lib/server-core-ext/src/main/java/org/apache/olingo/server/core/SchemaBasedEdmProvider.java new file mode 100644 index 000000000..8641a6941 --- /dev/null +++ b/lib/server-core-ext/src/main/java/org/apache/olingo/server/core/SchemaBasedEdmProvider.java @@ -0,0 +1,315 @@ +/* + * 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; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import org.apache.olingo.commons.api.ODataException; +import org.apache.olingo.commons.api.edm.FullQualifiedName; +import org.apache.olingo.commons.api.edm.provider.Action; +import org.apache.olingo.commons.api.edm.provider.ActionImport; +import org.apache.olingo.commons.api.edm.provider.AliasInfo; +import org.apache.olingo.commons.api.edm.provider.Annotatable; +import org.apache.olingo.commons.api.edm.provider.Annotations; +import org.apache.olingo.commons.api.edm.provider.ComplexType; +import org.apache.olingo.commons.api.edm.provider.EdmProvider; +import org.apache.olingo.commons.api.edm.provider.EntityContainer; +import org.apache.olingo.commons.api.edm.provider.EntityContainerInfo; +import org.apache.olingo.commons.api.edm.provider.EntitySet; +import org.apache.olingo.commons.api.edm.provider.EntityType; +import org.apache.olingo.commons.api.edm.provider.EnumType; +import org.apache.olingo.commons.api.edm.provider.Function; +import org.apache.olingo.commons.api.edm.provider.FunctionImport; +import org.apache.olingo.commons.api.edm.provider.Schema; +import org.apache.olingo.commons.api.edm.provider.Singleton; +import org.apache.olingo.commons.api.edm.provider.Term; +import org.apache.olingo.commons.api.edm.provider.TypeDefinition; + +public class SchemaBasedEdmProvider implements EdmProvider { + private final List edmSchemas = new ArrayList(); + + protected void addSchema(Schema schema) { + this.edmSchemas.add(schema); + } + + private Schema getSchema(String ns) { + for (Schema s : this.edmSchemas) { + if (s.getNamespace().equals(ns)) { + return s; + } + } + return null; + } + + @Override + public EnumType getEnumType(FullQualifiedName fqn) throws ODataException { + Schema schema = getSchema(fqn.getNamespace()); + if (schema != null) { + List types = schema.getEnumTypes(); + if (types != null) { + for (EnumType type : types) { + if (type.getName().equals(fqn.getName())) { + return type; + } + } + } + } + return null; + } + + @Override + public TypeDefinition getTypeDefinition(FullQualifiedName fqn) throws ODataException { + Schema schema = getSchema(fqn.getNamespace()); + if (schema != null) { + List types = schema.getTypeDefinitions(); + if (types != null) { + for (TypeDefinition type : types) { + if (type.getName().equals(fqn.getName())) { + return type; + } + } + } + } + return null; + } + + @Override + public List getFunctions(FullQualifiedName fqn) throws ODataException { + ArrayList foundFuncs = new ArrayList(); + Schema schema = getSchema(fqn.getNamespace()); + if (schema != null) { + List functions = schema.getFunctions(); + if (functions != null) { + for (Function func : functions) { + if (func.getName().equals(fqn.getName())) { + foundFuncs.add(func); + } + } + } + } + return foundFuncs; + } + + @Override + public Term getTerm(FullQualifiedName fqn) throws ODataException { + Schema schema = getSchema(fqn.getNamespace()); + if (schema != null) { + List terms = schema.getTerms(); + if (terms != null) { + for (Term term : terms) { + if (term.getName().equals(fqn.getName())) { + return term; + } + } + } + } + return null; + } + + @Override + public EntitySet getEntitySet(FullQualifiedName fqn, String entitySetName) throws ODataException { + Schema schema = getSchema(fqn.getFullQualifiedNameAsString()); + if (schema != null) { + EntityContainer ec = schema.getEntityContainer(); + if (ec != null && ec.getEntitySets() != null) { + for (EntitySet es : ec.getEntitySets()) { + if (es.getName().equals(entitySetName)) { + return es; + } + } + } + } + return null; + } + + @Override + public Singleton getSingleton(FullQualifiedName fqn, String singletonName) throws ODataException { + Schema schema = getSchema(fqn.getFullQualifiedNameAsString()); + if (schema != null) { + EntityContainer ec = schema.getEntityContainer(); + if (ec != null && ec.getSingletons() != null) { + for (Singleton es : ec.getSingletons()) { + if (es.getName().equals(singletonName)) { + return es; + } + } + } + } + return null; + } + + @Override + public ActionImport getActionImport(FullQualifiedName fqn, String actionImportName) + throws ODataException { + Schema schema = getSchema(fqn.getFullQualifiedNameAsString()); + if (schema != null) { + EntityContainer ec = schema.getEntityContainer(); + if (ec != null && ec.getActionImports() != null) { + for (ActionImport es : ec.getActionImports()) { + if (es.getName().equals(actionImportName)) { + return es; + } + } + } + } + return null; + } + + @Override + public FunctionImport getFunctionImport(FullQualifiedName fqn, String functionImportName) + throws ODataException { + Schema schema = getSchema(fqn.getFullQualifiedNameAsString()); + if (schema != null) { + EntityContainer ec = schema.getEntityContainer(); + if (ec != null && ec.getFunctionImports() != null) { + for (FunctionImport es : ec.getFunctionImports()) { + if (es.getName().equals(functionImportName)) { + return es; + } + } + } + } + return null; + } + + @Override + public EntityContainerInfo getEntityContainerInfo(FullQualifiedName fqn) throws ODataException { + Schema schema = null; + + if (fqn == null) { + for (Schema s : this.edmSchemas) { + if (s.getEntityContainer() != null) { + schema = s; + break; + } + } + } else { + schema = getSchema(fqn.getFullQualifiedNameAsString()); + } + + if (schema != null) { + EntityContainer ec = schema.getEntityContainer(); + if (ec != null) { + EntityContainerInfo info = new EntityContainerInfo(); + info.setContainerName(new FullQualifiedName(schema.getNamespace())); + if (schema.getEntityContainer().getExtendsContainer() != null) { + info.setExtendsContainer(new FullQualifiedName(schema.getEntityContainer().getExtendsContainer())); + } + return info; + } + } + return null; + } + + @Override + public List getAliasInfos() throws ODataException { + Schema schema = null; + for (Schema s : this.edmSchemas) { + if (s.getEntityContainer() != null) { + schema = s; + break; + } + } + + if (schema == null) { + schema = this.edmSchemas.get(0); + } + + AliasInfo ai = new AliasInfo(); + ai.setAlias(schema.getAlias()); + ai.setNamespace(schema.getNamespace()); + return Arrays.asList(ai); + } + + @Override + public EntityContainer getEntityContainer() throws ODataException { + // note that there can be many schemas, but only one needs to contain the + // entity container in a given metadata document. + for (Schema s : this.edmSchemas) { + if (s.getEntityContainer() != null) { + return s.getEntityContainer(); + } + } + return null; + } + + @Override + public List getSchemas() throws ODataException { + return new ArrayList(this.edmSchemas); + } + + @Override + public EntityType getEntityType(final FullQualifiedName fqn) throws ODataException { + Schema schema = getSchema(fqn.getNamespace()); + if (schema != null) { + if (schema.getEntityTypes() != null) { + for (EntityType type : schema.getEntityTypes()) { + if (type.getName().equals(fqn.getName())) { + return type; + } + } + } + } + return null; + } + + @Override + public ComplexType getComplexType(final FullQualifiedName fqn) throws ODataException { + Schema schema = getSchema(fqn.getNamespace()); + if (schema != null) { + if (schema.getComplexTypes() != null) { + for (ComplexType type : schema.getComplexTypes()) { + if (type.getName().equals(fqn.getName())) { + return type; + } + } + } + } + return null; + } + + @Override + public List getActions(final FullQualifiedName fqn) throws ODataException { + ArrayList actions = new ArrayList(); + Schema schema = getSchema(fqn.getNamespace()); + if (schema != null) { + List types = schema.getActions(); + if (types != null) { + for (Action type : types) { + if (type.getName().equals(fqn.getName())) { + actions.add(type); + } + } + } + } + return actions; + } + + @Override + public Annotations getAnnotationsGroup(FullQualifiedName targetName) throws ODataException { + return null; + } + + @Override + public Annotatable getAnnoatatable(FullQualifiedName annotatedName) throws ODataException { + return null; + } +} diff --git a/lib/server-core-ext/src/main/java/org/apache/olingo/server/core/ServiceDispatcher.java b/lib/server-core-ext/src/main/java/org/apache/olingo/server/core/ServiceDispatcher.java new file mode 100644 index 000000000..839d87780 --- /dev/null +++ b/lib/server-core-ext/src/main/java/org/apache/olingo/server/core/ServiceDispatcher.java @@ -0,0 +1,227 @@ +/* + * 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; + +import java.net.MalformedURLException; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.URL; + +import org.apache.olingo.commons.api.format.ContentType; +import org.apache.olingo.server.api.OData; +import org.apache.olingo.server.api.ODataApplicationException; +import org.apache.olingo.server.api.ODataRequest; +import org.apache.olingo.server.api.ODataResponse; +import org.apache.olingo.server.api.ODataTranslatedException; +import org.apache.olingo.server.api.ServiceMetadata; +import org.apache.olingo.server.api.serializer.CustomContentTypeSupport; +import org.apache.olingo.server.api.uri.UriInfo; +import org.apache.olingo.server.api.uri.UriInfoBatch; +import org.apache.olingo.server.api.uri.UriInfoCrossjoin; +import org.apache.olingo.server.api.uri.UriInfoEntityId; +import org.apache.olingo.server.api.uri.UriInfoMetadata; +import org.apache.olingo.server.api.uri.UriInfoService; +import org.apache.olingo.server.api.uri.UriResourceAction; +import org.apache.olingo.server.api.uri.UriResourceComplexProperty; +import org.apache.olingo.server.api.uri.UriResourceCount; +import org.apache.olingo.server.api.uri.UriResourceEntitySet; +import org.apache.olingo.server.api.uri.UriResourceFunction; +import org.apache.olingo.server.api.uri.UriResourceNavigation; +import org.apache.olingo.server.api.uri.UriResourcePrimitiveProperty; +import org.apache.olingo.server.api.uri.UriResourceRef; +import org.apache.olingo.server.api.uri.UriResourceSingleton; +import org.apache.olingo.server.api.uri.UriResourceValue; +import org.apache.olingo.server.core.requests.ActionRequest; +import org.apache.olingo.server.core.requests.BatchRequest; +import org.apache.olingo.server.core.requests.DataRequest; +import org.apache.olingo.server.core.requests.FunctionRequest; +import org.apache.olingo.server.core.requests.MediaRequest; +import org.apache.olingo.server.core.requests.MetadataRequest; +import org.apache.olingo.server.core.requests.ServiceDocumentRequest; +import org.apache.olingo.server.core.uri.parser.Parser; +import org.apache.olingo.server.core.uri.validator.UriValidator; + +public class ServiceDispatcher extends RequestURLHierarchyVisitor { + private final OData odata; + protected ServiceMetadata metadata; + protected ServiceHandler handler; + protected CustomContentTypeSupport customContentSupport; + private String idOption; + protected ServiceRequest request; + + public ServiceDispatcher(OData odata, ServiceMetadata metadata, ServiceHandler handler, + CustomContentTypeSupport customContentSupport) { + this.odata = odata; + this.metadata = metadata; + this.handler = handler; + this.customContentSupport = customContentSupport; + } + + public void execute(ODataRequest odRequest, ODataResponse odResponse) + throws ODataTranslatedException, ODataApplicationException { + + UriInfo uriInfo = new Parser().parseUri(odRequest.getRawODataPath(), odRequest.getRawQueryPath(), null, + this.metadata.getEdm()); + + new UriValidator().validate(uriInfo, odRequest.getMethod()); + + visit(uriInfo); + + // this should cover for any unsupported calls until they are implemented + if (this.request == null) { + this.request = new ServiceRequest(this.odata, this.metadata) { + @Override + public ContentType getResponseContentType() throws ContentNegotiatorException { + return ContentType.APPLICATION_JSON; + } + + @Override + public void execute(ServiceHandler handler, ODataResponse response) + throws ODataTranslatedException, ODataApplicationException { + handler.anyUnsupported(getODataRequest(), response); + } + }; + } + + // To handle $entity?$id=http://localhost/EntitySet(key) as + // http://localhost/EntitySet(key) + if (this.idOption != null) { + try { + this.request = this.request.parseLink(new URI(this.idOption)); + } catch (URISyntaxException e) { + throw new ODataHandlerException("Invalid $id value", + ODataHandlerException.MessageKeys.FUNCTIONALITY_NOT_IMPLEMENTED, this.idOption); + } + } + + this.request.setODataRequest(odRequest); + this.request.setUriInfo(uriInfo); + this.request.setCustomContentTypeSupport(this.customContentSupport); + this.request.execute(this.handler, odResponse); + } + + @Override + public void visit(UriInfoMetadata info) { + this.request = new MetadataRequest(this.odata, this.metadata); + } + + @Override + public void visit(UriInfoService info) { + this.request = new ServiceDocumentRequest(this.odata, this.metadata); + } + + @Override + public void visit(UriResourceEntitySet info) { + DataRequest dataRequest = new DataRequest(this.odata, this.metadata); + dataRequest.setUriResourceEntitySet(info); + this.request = dataRequest; + } + + @Override + public void visit(UriResourceCount option) { + DataRequest dataRequest = (DataRequest) this.request; + dataRequest.setCountRequest(option != null); + } + + @Override + public void visit(UriResourceComplexProperty info) { + DataRequest dataRequest = (DataRequest) this.request; + dataRequest.setUriResourceProperty(info); + } + + @Override + public void visit(UriResourcePrimitiveProperty info) { + DataRequest dataRequest = (DataRequest) this.request; + dataRequest.setUriResourceProperty(info); + } + + @Override + public void visit(UriResourceValue info) { + DataRequest dataRequest = (DataRequest) this.request; + if (dataRequest.isPropertyRequest()) { + dataRequest.setValueRequest(info != null); + } else { + MediaRequest mediaRequest = new MediaRequest(this.odata, this.metadata); + mediaRequest.setUriResourceEntitySet(dataRequest.getUriResourceEntitySet()); + this.request = mediaRequest; + } + } + + @Override + public void visit(UriResourceAction info) { + ActionRequest actionRequest = new ActionRequest(this.odata, this.metadata); + actionRequest.setUriResourceAction(info); + this.request = actionRequest; + } + + @Override + public void visit(UriResourceFunction info) { + FunctionRequest functionRequest = new FunctionRequest(this.odata, this.metadata); + functionRequest.setUriResourceFunction(info); + this.request = functionRequest; + } + + @Override + public void visit(UriResourceNavigation info) { + DataRequest dataRequest = (DataRequest) this.request; + dataRequest.addUriResourceNavigation(info); + } + + @Override + public void visit(UriResourceRef info) { + // this is same as data, but return is just entity references. + DataRequest dataRequest = (DataRequest) this.request; + dataRequest.setReferenceRequest(info != null); + } + + @Override + public void visit(UriInfoBatch info) { + this.request = new BatchRequest(this.odata, this.metadata); + } + + @Override + public void visit(UriResourceSingleton info) { + DataRequest dataRequest = new DataRequest(this.odata, this.metadata); + dataRequest.setUriResourceSingleton(info); + this.request = dataRequest; + } + + @Override + public void visit(UriInfoEntityId info) { + DataRequest dataRequest = new DataRequest(this.odata, this.metadata); + this.request = dataRequest; + + // this can relative or absolute form + String id = info.getIdOption().getValue(); + try { + URL url = new URL(id); + this.idOption = url.getPath(); + } catch (MalformedURLException e) { + this.idOption = id; + } + super.visit(info); + } + + @Override + public void visit(UriInfoCrossjoin info) { + DataRequest dataRequest = new DataRequest(this.odata, this.metadata); + dataRequest.setCrossJoin(info); + this.request = dataRequest; + } +} diff --git a/lib/server-core-ext/src/main/java/org/apache/olingo/server/core/ServiceHandler.java b/lib/server-core-ext/src/main/java/org/apache/olingo/server/core/ServiceHandler.java new file mode 100644 index 000000000..8a9c6108b --- /dev/null +++ b/lib/server-core-ext/src/main/java/org/apache/olingo/server/core/ServiceHandler.java @@ -0,0 +1,263 @@ +/* + * 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; + +import java.io.InputStream; +import java.net.URI; +import java.util.List; + +import org.apache.olingo.commons.api.data.Entity; +import org.apache.olingo.commons.api.data.Property; +import org.apache.olingo.commons.api.http.HttpMethod; +import org.apache.olingo.server.api.ODataApplicationException; +import org.apache.olingo.server.api.ODataRequest; +import org.apache.olingo.server.api.ODataResponse; +import org.apache.olingo.server.api.ODataTranslatedException; +import org.apache.olingo.server.api.processor.Processor; +import org.apache.olingo.server.core.requests.ActionRequest; +import org.apache.olingo.server.core.requests.DataRequest; +import org.apache.olingo.server.core.requests.FunctionRequest; +import org.apache.olingo.server.core.requests.MediaRequest; +import org.apache.olingo.server.core.requests.MetadataRequest; +import org.apache.olingo.server.core.requests.ServiceDocumentRequest; +import org.apache.olingo.server.core.responses.EntityResponse; +import org.apache.olingo.server.core.responses.MetadataResponse; +import org.apache.olingo.server.core.responses.NoContentResponse; +import org.apache.olingo.server.core.responses.PropertyResponse; +import org.apache.olingo.server.core.responses.ServiceDocumentResponse; +import org.apache.olingo.server.core.responses.ServiceResponse; +import org.apache.olingo.server.core.responses.StreamResponse; + +public interface ServiceHandler extends Processor { + + /** + * Read CSDL document of the Service + * @param request + * @param response + * @throws ODataTranslatedException + * @throws ODataApplicationException + */ + void readMetadata(MetadataRequest request, MetadataResponse response) + throws ODataTranslatedException, ODataApplicationException; + + /** + * Read ServiceDocument of the service + * @param request + * @param response + * @throws ODataTranslatedException + * @throws ODataApplicationException + */ + void readServiceDocument(ServiceDocumentRequest request, ServiceDocumentResponse response) + throws ODataTranslatedException, ODataApplicationException; + + /** + * Read operation for EntitySets, Entities, Properties, Media etc. Based on the type of request + * the response object is different. Even the navigation based queries are handled by this method. + * @param request + * @param response + * @throws ODataTranslatedException + * @throws ODataApplicationException + */ + void read(DataRequest request, T response) + throws ODataTranslatedException, ODataApplicationException; + + /** + * Create new entity in the service based on the entity object provided + * @param request + * @param entity + * @param response + * @throws ODataTranslatedException + * @throws ODataApplicationException + */ + void createEntity(DataRequest request, Entity entity, EntityResponse response) + throws ODataTranslatedException, ODataApplicationException; + + /** + * Update the entity object. + * @param request + * @param entity + * @param merge - true if merge operation, false it needs to be replaced + * @param entityETag - previous entity tag if provided by the user. "*" means allow. + * @param response + * @throws ODataTranslatedException + * @throws ODataApplicationException + */ + void updateEntity(DataRequest request, Entity entity, boolean merge, String entityETag, + EntityResponse response) throws ODataTranslatedException, ODataApplicationException; + + /** + * Delete the Entity + * @param request + * @param entityETag - entity tag to match, if provided by the user. "*" means allow + * @param response + * @throws ODataTranslatedException + * @throws ODataApplicationException + */ + void deleteEntity(DataRequest request, String entityETag, EntityResponse response) + throws ODataTranslatedException, ODataApplicationException; + + /** + * Update a non-media/stream property.if the value of property NULL, it should be treated as + * DeleteProperty 11.4.9.2 + * @param request + * @param property - Updated property. + * @param merge - if the property is complex, true here means merge, false is replace + * @param entityETag - entity tag to match before update operation, "*" allows all. + * @param response + * @throws ODataTranslatedException + * @throws ODataApplicationException + */ + void updateProperty(DataRequest request, Property property, boolean merge, String entityETag, + PropertyResponse response) throws ODataTranslatedException, ODataApplicationException; + + /** + * Update Stream property, if StreamContent is null, it should treated as delete request + * @param request + * @param entityETag - entity tag to match before update operation, "*" allows all. + * @param streamContent - updated stream content + * @param response + * @throws ODataTranslatedException + * @throws ODataApplicationException + */ + void upsertStreamProperty(DataRequest request, String entityETag, InputStream streamContent, + NoContentResponse response) throws ODataTranslatedException, ODataApplicationException; + + /** + * Invocation of a Function. The response object will be based on metadata defined for service + * @param request + * @param method + * @param response + * @throws ODataTranslatedException + * @throws ODataApplicationException + */ + void invoke(FunctionRequest request, HttpMethod method, T response) + throws ODataTranslatedException, ODataApplicationException; + + /** + * Invocation of a Function. The response object will be based on metadata defined for service + * @param request + * @param eTag + * @param response + * @throws ODataTranslatedException + * @throws ODataApplicationException + */ + void invoke(ActionRequest request, String eTag, T response) + throws ODataTranslatedException, ODataApplicationException; + + /** + * Read media stream content of a Entity + * @param request + * @param response + * @throws ODataTranslatedException + * @throws ODataApplicationException + */ + void readMediaStream(MediaRequest request, StreamResponse response) + throws ODataTranslatedException, ODataApplicationException; + + /** + * Update of Media Stream Content of a Entity. If the mediaContent is null it should be treated + * as delete request. + * @param request + * @param entityETag - entity etag to match before update operation, "*" allows all. + * @param mediaContent - if null, must be treated as delete request + * @param response + * @throws ODataTranslatedException + * @throws ODataApplicationException + */ + void upsertMediaStream(MediaRequest request, String entityETag, InputStream mediaContent, + NoContentResponse response) throws ODataTranslatedException, ODataApplicationException; + + /** + * Any Unsupported one will be directed here. + * @param request + * @param response + * @throws ODataTranslatedException + * @throws ODataApplicationException + */ + void anyUnsupported(ODataRequest request, ODataResponse response) + throws ODataTranslatedException, ODataApplicationException; + + /** + * Add references (relationships) to Entity. + * @param request + * @param entityETag - entity etag to match before add operation, "*" allows all. + * @param idReferences - references to add + * @param response - return always should be 204 + * @throws ODataTranslatedException + * @throws ODataApplicationException + */ + void addReference(DataRequest request, String entityETag, List idReferences, NoContentResponse response) + throws ODataTranslatedException, ODataApplicationException; + + /** + * Update references (relationships) in an Entity + * @param request + * @param entityETag + * @param referenceId + * @param response - always should be 204 + * @throws ODataTranslatedException + * @throws ODataApplicationException + */ + void updateReference(DataRequest request, String entityETag, URI referenceId, NoContentResponse response) + throws ODataTranslatedException, ODataApplicationException; + + /** + * Delete references (relationships) in an Entity + * @param request + * @param deleteId + * @param entityETag + * @param response - always should be 204 + * @throws ODataTranslatedException + * @throws ODataApplicationException + */ + void deleteReference(DataRequest request, URI deleteId, String entityETag, NoContentResponse response) + throws ODataTranslatedException, ODataApplicationException; + + + /** + * During a batch operation, this method starts the transaction (if any) before any operation is handled + * by the service. No nested transactions. + * @return must return a unique transaction id that references a atomic operation. + */ + String startTransaction(); + + /** + * When a batch operation is complete and all the intermediate service requests are successful, then + * commit is called with transaction id returned in the startTransaction method. + * @param txnId + */ + void commit(String txnId); + /** + * When a batch operation is in-complete due to an error in the middle of changeset, then rollback is + * called with transaction id, that returned from startTransaction method. + * @param txnId + */ + void rollback(String txnId); + + /** + * This is not complete, more URL parsing changes required. Cross join between two entities. + * @param dataRequest + * @param entitySetNames + * @param response + * @throws ODataTranslatedException + * @throws ODataApplicationException + */ + void crossJoin(DataRequest dataRequest, List entitySetNames, ODataResponse response) + throws ODataTranslatedException, ODataApplicationException; +} diff --git a/lib/server-core-ext/src/main/java/org/apache/olingo/server/core/ServiceRequest.java b/lib/server-core-ext/src/main/java/org/apache/olingo/server/core/ServiceRequest.java new file mode 100644 index 000000000..e9a8cfe4f --- /dev/null +++ b/lib/server-core-ext/src/main/java/org/apache/olingo/server/core/ServiceRequest.java @@ -0,0 +1,253 @@ +/* + * 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; + +import java.net.URI; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.StringTokenizer; + +import org.apache.olingo.commons.api.data.ContextURL; +import org.apache.olingo.commons.api.format.ContentType; +import org.apache.olingo.commons.api.format.ODataFormat; +import org.apache.olingo.commons.api.http.HttpHeader; +import org.apache.olingo.commons.api.http.HttpMethod; +import org.apache.olingo.server.api.OData; +import org.apache.olingo.server.api.ODataApplicationException; +import org.apache.olingo.server.api.ODataRequest; +import org.apache.olingo.server.api.ODataResponse; +import org.apache.olingo.server.api.ODataTranslatedException; +import org.apache.olingo.server.api.ServiceMetadata; +import org.apache.olingo.server.api.serializer.ComplexSerializerOptions; +import org.apache.olingo.server.api.serializer.CustomContentTypeSupport; +import org.apache.olingo.server.api.serializer.EntityCollectionSerializerOptions; +import org.apache.olingo.server.api.serializer.EntitySerializerOptions; +import org.apache.olingo.server.api.serializer.ODataSerializer; +import org.apache.olingo.server.api.serializer.SerializerException; +import org.apache.olingo.server.api.uri.UriInfo; +import org.apache.olingo.server.core.requests.DataRequest; +import org.apache.olingo.server.core.uri.parser.Parser; +import org.apache.olingo.server.core.uri.parser.UriParserException; + +public abstract class ServiceRequest { + protected OData odata; + protected UriInfo uriInfo; + protected ServiceMetadata serviceMetadata; + protected CustomContentTypeSupport customContentType; + protected ODataRequest request; + + public ServiceRequest(OData odata, ServiceMetadata serviceMetadata) { + this.odata = odata; + this.serviceMetadata = serviceMetadata; + } + + public OData getOdata() { + return odata; + } + + public ServiceMetadata getServiceMetaData() { + return this.serviceMetadata; + } + + public UriInfo getUriInfo() { + return uriInfo; + } + + protected void setUriInfo(UriInfo uriInfo) { + this.uriInfo = uriInfo; + } + + public boolean allowedMethod() { + return isGET(); + } + + public CustomContentTypeSupport getCustomContentTypeSupport() { + return this.customContentType; + } + + public void setCustomContentTypeSupport(CustomContentTypeSupport support) { + this.customContentType = support; + } + + public ODataRequest getODataRequest() { + return this.request; + } + + protected void setODataRequest(ODataRequest request) { + this.request = request; + } + + public ContentType getRequestContentType() { + if (this.request.getHeader(HttpHeader.CONTENT_TYPE) != null) { + return ContentType.parse(this.request.getHeader(HttpHeader.CONTENT_TYPE)); + } + return ContentType.APPLICATION_OCTET_STREAM; + } + + public abstract void execute(ServiceHandler handler, ODataResponse response) + throws ODataTranslatedException, ODataApplicationException; + + public abstract ContentType getResponseContentType() throws ContentNegotiatorException; + + public void methodNotAllowed() throws ODataHandlerException { + throw new ODataHandlerException("HTTP method " + this.request.getMethod() + " is not allowed.", + ODataHandlerException.MessageKeys.HTTP_METHOD_NOT_ALLOWED, this.request.getMethod() + .toString()); + } + + public void notImplemented() throws ODataHandlerException { + throw new ODataHandlerException("not implemented", //$NON-NLS-1$ + ODataHandlerException.MessageKeys.FUNCTIONALITY_NOT_IMPLEMENTED); + } + + protected boolean isGET() { + return this.request.getMethod() == HttpMethod.GET; + } + + protected boolean isPUT() { + return this.request.getMethod() == HttpMethod.PUT; + } + + protected boolean isDELETE() { + return this.request.getMethod() == HttpMethod.DELETE; + } + + protected boolean isPATCH() { + return this.request.getMethod() == HttpMethod.PATCH; + } + + protected boolean isPOST() { + return this.request.getMethod() == HttpMethod.POST; + } + + public T getSerializerOptions(Class serilizerOptions, ContextURL contextUrl, + boolean references) throws ContentNegotiatorException { + final ODataFormat format = ODataFormat.fromContentType(getResponseContentType()); + + if (serilizerOptions.isAssignableFrom(EntitySerializerOptions.class)) { + return (T) EntitySerializerOptions.with() + .contextURL(format == ODataFormat.JSON_NO_METADATA ? null : contextUrl) + .expand(uriInfo.getExpandOption()).select(this.uriInfo.getSelectOption()) + .setWriteOnlyReferences(references).build(); + } else if (serilizerOptions.isAssignableFrom(EntityCollectionSerializerOptions.class)) { + return (T) EntityCollectionSerializerOptions.with() + .contextURL(format == ODataFormat.JSON_NO_METADATA ? null : contextUrl) + .count(uriInfo.getCountOption()).expand(uriInfo.getExpandOption()) + .select(uriInfo.getSelectOption()).setWriteOnlyReferences(references).build(); + } else if (serilizerOptions.isAssignableFrom(ComplexSerializerOptions.class)) { + return (T) ComplexSerializerOptions.with().contextURL(contextUrl) + .expand(this.uriInfo.getExpandOption()).select(this.uriInfo.getSelectOption()).build(); + } + return null; + } + + public ReturnRepresentation getReturnRepresentation() { + String prefer = this.request.getHeader(HttpHeader.PREFER); + if (prefer == null) { + return ReturnRepresentation.REPRESENTATION; + } + if (prefer.contains("return=minimal")) { //$NON-NLS-1$ + return ReturnRepresentation.MINIMAL; + } + return ReturnRepresentation.REPRESENTATION; + } + + public String getHeader(String key) { + return this.request.getHeader(key); + } + + public String getETag() { + String etag = getHeader(HttpHeader.IF_MATCH); + if (etag == null) { + etag = getHeader(HttpHeader.IF_NONE_MATCH); + } + return ((etag == null) ? "*" : etag); //$NON-NLS-1$ + } + + public ODataSerializer getSerializer() throws ContentNegotiatorException, + SerializerException { + ODataFormat format = ODataFormat.fromContentType(getResponseContentType()); + return this.odata.createSerializer(format); + } + + public Map getPreferences(){ + HashMap map = new HashMap(); + List headers = request.getHeaders(HttpHeader.PREFER); + if (headers != null) { + for (String header:headers) { + int idx = header.indexOf('='); + if (idx != -1) { + String key = header.substring(0, idx); + String value = header.substring(idx+1); + if (value.startsWith("\"")) { + value = value.substring(1); + } + if (value.endsWith("\"")) { + value = value.substring(0, value.length()-1); + } + map.put(key, value); + } else { + map.put(header, "true"); + } + } + } + return map; + } + + public String getPreference(String key) { + return getPreferences().get(key); + } + + public String getQueryParameter(String param) { + String queryPath = getODataRequest().getRawQueryPath(); + if (queryPath != null) { + StringTokenizer st = new StringTokenizer(queryPath, ","); + while (st.hasMoreTokens()) { + String token = st.nextToken(); + int index = token.indexOf('='); + if (index != -1) { + String key = token.substring(0, index); + String value = token.substring(index+1); + if (key.equals(param)) { + return value; + } + } + } + } + return null; + } + + public DataRequest parseLink(URI uri) throws UriParserException { + String rawPath = uri.getPath(); + int e = rawPath.indexOf("/", 1); + if (-1 == e) { + rawPath = uri.getPath(); + } else { + rawPath = rawPath.substring(e); + } + + UriInfo uriInfo = new Parser().parseUri(rawPath, uri.getQuery(), null, + this.serviceMetadata.getEdm()); + ServiceDispatcher dispatcher = new ServiceDispatcher(odata, serviceMetadata, null, customContentType); + dispatcher.visit(uriInfo); + return (DataRequest)dispatcher.request; + } +} diff --git a/lib/server-core-ext/src/main/java/org/apache/olingo/server/core/legacy/ProcessorServiceHandler.java b/lib/server-core-ext/src/main/java/org/apache/olingo/server/core/legacy/ProcessorServiceHandler.java new file mode 100644 index 000000000..fa8f44557 --- /dev/null +++ b/lib/server-core-ext/src/main/java/org/apache/olingo/server/core/legacy/ProcessorServiceHandler.java @@ -0,0 +1,433 @@ +/* + * 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.legacy; + +import java.io.InputStream; +import java.net.URI; +import java.util.LinkedList; +import java.util.List; + +import org.apache.olingo.commons.api.data.Entity; +import org.apache.olingo.commons.api.data.Property; +import org.apache.olingo.commons.api.edm.EdmProperty; +import org.apache.olingo.commons.api.http.HttpMethod; +import org.apache.olingo.server.api.OData; +import org.apache.olingo.server.api.ODataApplicationException; +import org.apache.olingo.server.api.ODataRequest; +import org.apache.olingo.server.api.ODataResponse; +import org.apache.olingo.server.api.ODataTranslatedException; +import org.apache.olingo.server.api.ServiceMetadata; +import org.apache.olingo.server.api.processor.ComplexCollectionProcessor; +import org.apache.olingo.server.api.processor.ComplexProcessor; +import org.apache.olingo.server.api.processor.CountComplexCollectionProcessor; +import org.apache.olingo.server.api.processor.CountEntityCollectionProcessor; +import org.apache.olingo.server.api.processor.CountPrimitiveCollectionProcessor; +import org.apache.olingo.server.api.processor.EntityCollectionProcessor; +import org.apache.olingo.server.api.processor.EntityProcessor; +import org.apache.olingo.server.api.processor.MediaEntityProcessor; +import org.apache.olingo.server.api.processor.MetadataProcessor; +import org.apache.olingo.server.api.processor.PrimitiveCollectionProcessor; +import org.apache.olingo.server.api.processor.PrimitiveProcessor; +import org.apache.olingo.server.api.processor.PrimitiveValueProcessor; +import org.apache.olingo.server.api.processor.Processor; +import org.apache.olingo.server.api.processor.ReferenceProcessor; +import org.apache.olingo.server.api.processor.ServiceDocumentProcessor; +import org.apache.olingo.server.core.ODataHandlerException; +import org.apache.olingo.server.core.ServiceHandler; +import org.apache.olingo.server.core.requests.ActionRequest; +import org.apache.olingo.server.core.requests.DataRequest; +import org.apache.olingo.server.core.requests.FunctionRequest; +import org.apache.olingo.server.core.requests.MediaRequest; +import org.apache.olingo.server.core.requests.MetadataRequest; +import org.apache.olingo.server.core.requests.ServiceDocumentRequest; +import org.apache.olingo.server.core.responses.CountResponse; +import org.apache.olingo.server.core.responses.EntityResponse; +import org.apache.olingo.server.core.responses.EntitySetResponse; +import org.apache.olingo.server.core.responses.MetadataResponse; +import org.apache.olingo.server.core.responses.NoContentResponse; +import org.apache.olingo.server.core.responses.PrimitiveValueResponse; +import org.apache.olingo.server.core.responses.PropertyResponse; +import org.apache.olingo.server.core.responses.ServiceDocumentResponse; +import org.apache.olingo.server.core.responses.ServiceResponse; +import org.apache.olingo.server.core.responses.ServiceResponseVisior; +import org.apache.olingo.server.core.responses.StreamResponse; + +public class ProcessorServiceHandler implements ServiceHandler { + private final List processors = new LinkedList(); + private OData odata; + private ServiceMetadata serviceMetadata; + + @Override + public void init(OData odata, ServiceMetadata serviceMetadata) { + this.odata = odata; + this.serviceMetadata = serviceMetadata; + } + + public void register(Processor processor) { + this.processors.add(processor); + processor.init(odata, serviceMetadata); + } + + private T selectProcessor(final Class cls) throws ODataHandlerException { + for (final Processor processor : processors) { + if (cls.isAssignableFrom(processor.getClass())) { + processor.init(odata, serviceMetadata); + return cls.cast(processor); + } + } + throw new ODataHandlerException("Processor: " + cls.getSimpleName() + " not registered.", + ODataHandlerException.MessageKeys.PROCESSOR_NOT_IMPLEMENTED, cls.getSimpleName()); + } + + @Override + public void readMetadata(MetadataRequest request, MetadataResponse response) + throws ODataTranslatedException, ODataApplicationException { + selectProcessor(MetadataProcessor.class).readMetadata(request.getODataRequest(), + response.getODataResponse(), request.getUriInfo(), request.getResponseContentType()); + } + + @Override + public void readServiceDocument(ServiceDocumentRequest request, ServiceDocumentResponse response) + throws ODataTranslatedException, ODataApplicationException { + selectProcessor(ServiceDocumentProcessor.class).readServiceDocument(request.getODataRequest(), + response.getODataResponse(), request.getUriInfo(), request.getResponseContentType()); + } + + @Override + public void read(final DataRequest request, final T response) + throws ODataTranslatedException, ODataApplicationException { + response.accepts(new ServiceResponseVisior() { + @Override + public void visit(CountResponse response) throws ODataTranslatedException, ODataApplicationException { + if (request.getUriResourceProperty() != null) { + EdmProperty edmProperty = request.getUriResourceProperty().getProperty(); + if (edmProperty.isPrimitive()) { + selectProcessor(CountPrimitiveCollectionProcessor.class).countPrimitiveCollection( + request.getODataRequest(), response.getODataResponse(), request.getUriInfo()); + } else { + selectProcessor(CountComplexCollectionProcessor.class).countComplexCollection( + request.getODataRequest(), response.getODataResponse(), request.getUriInfo()); + } + } else { + selectProcessor(CountEntityCollectionProcessor.class).countEntityCollection( + request.getODataRequest(), response.getODataResponse(), request.getUriInfo()); + } + } + + @Override + public void visit(EntityResponse response) throws ODataTranslatedException, + ODataApplicationException { + selectProcessor(EntityProcessor.class).readEntity(request.getODataRequest(), + response.getODataResponse(), request.getUriInfo(), request.getResponseContentType()); + } + + @Override + public void visit(PrimitiveValueResponse response) throws ODataTranslatedException, + ODataApplicationException { + selectProcessor(PrimitiveValueProcessor.class).readPrimitiveValue( + request.getODataRequest(), response.getODataResponse(), request.getUriInfo(), + request.getResponseContentType()); + } + + @Override + public void visit(PropertyResponse response) throws ODataTranslatedException, + ODataApplicationException { + EdmProperty edmProperty = request.getUriResourceProperty().getProperty(); + if (edmProperty.isPrimitive()) { + if(edmProperty.isCollection()) { + selectProcessor(PrimitiveCollectionProcessor.class).readPrimitiveCollection( + request.getODataRequest(), response.getODataResponse(), request.getUriInfo(), + request.getResponseContentType()); + + } else { + selectProcessor(PrimitiveProcessor.class).readPrimitive( + request.getODataRequest(), response.getODataResponse(), request.getUriInfo(), + request.getResponseContentType()); + } + } else { + if(edmProperty.isCollection()) { + selectProcessor(ComplexCollectionProcessor.class).readComplexCollection( + request.getODataRequest(), response.getODataResponse(), request.getUriInfo(), + request.getResponseContentType()); + + } else { + selectProcessor(ComplexProcessor.class).readComplex( + request.getODataRequest(), response.getODataResponse(), request.getUriInfo(), + request.getResponseContentType()); + + } + } + } + + @Override + public void visit(StreamResponse response) throws ODataTranslatedException, + ODataApplicationException { + response.writeServerError(true); + } + + @Override + public void visit(EntitySetResponse response) throws ODataTranslatedException, + ODataApplicationException { + selectProcessor(EntityCollectionProcessor.class).readEntityCollection(request.getODataRequest(), + response.getODataResponse(), request.getUriInfo(), request.getResponseContentType()); + } + }); + } + + @Override + public void createEntity(DataRequest request, Entity entity, EntityResponse response) + throws ODataTranslatedException, ODataApplicationException { + if (request.getEntitySet().getEntityType().hasStream()) { + selectProcessor(MediaEntityProcessor.class).createMediaEntity( + request.getODataRequest(), response.getODataResponse(), request.getUriInfo(), + request.getRequestContentType(),request.getResponseContentType()); + } else { + selectProcessor(EntityProcessor.class).createEntity(request.getODataRequest(), + response.getODataResponse(), request.getUriInfo(), request.getRequestContentType(), + request.getResponseContentType()); + } + } + + @Override + public void updateEntity(DataRequest request, Entity entity, boolean merge, String entityETag, + EntityResponse response) throws ODataTranslatedException, ODataApplicationException { + if (request.getEntitySet().getEntityType().hasStream()) { + selectProcessor(MediaEntityProcessor.class).updateMediaEntity( + request.getODataRequest(), response.getODataResponse(), request.getUriInfo(), + request.getRequestContentType(),request.getResponseContentType()); + } else { + selectProcessor(EntityProcessor.class).updateEntity(request.getODataRequest(), + response.getODataResponse(), request.getUriInfo(), request.getRequestContentType(), + request.getResponseContentType()); + } + } + + @Override + public void deleteEntity(DataRequest request, String entityETag, EntityResponse response) + throws ODataTranslatedException, ODataApplicationException { + selectProcessor(EntityProcessor.class).deleteEntity(request.getODataRequest(), + response.getODataResponse(), request.getUriInfo()); + } + + @Override + public void updateProperty(DataRequest request, Property property, boolean merge, + String entityETag, PropertyResponse response) throws ODataTranslatedException, + ODataApplicationException { + if (property.isPrimitive()) { + if (property.isCollection()) { + selectProcessor(PrimitiveCollectionProcessor.class).updatePrimitiveCollection( + request.getODataRequest(), response.getODataResponse(), request.getUriInfo(), + request.getRequestContentType(), request.getResponseContentType()); + } else { + selectProcessor(PrimitiveProcessor.class).updatePrimitive( + request.getODataRequest(), response.getODataResponse(), request.getUriInfo(), + request.getRequestContentType(), request.getResponseContentType()); + } + } else { + if (property.isCollection()) { + selectProcessor(ComplexCollectionProcessor.class).updateComplexCollection( + request.getODataRequest(), response.getODataResponse(), request.getUriInfo(), + request.getRequestContentType(), request.getResponseContentType()); + } else { + selectProcessor(ComplexProcessor.class).updateComplex( + request.getODataRequest(), response.getODataResponse(), request.getUriInfo(), + request.getRequestContentType(), request.getResponseContentType()); + } + } + } + + @Override + public void upsertStreamProperty(DataRequest request, String entityETag, + InputStream streamContent, NoContentResponse response) throws ODataTranslatedException, + ODataApplicationException { + throw new ODataHandlerException("not implemented", + ODataHandlerException.MessageKeys.FUNCTIONALITY_NOT_IMPLEMENTED); + } + + @Override + public void invoke(final FunctionRequest request, HttpMethod method, + final T response) throws ODataTranslatedException, ODataApplicationException { + if (method != HttpMethod.GET) { + throw new ODataHandlerException("HTTP method " + method + " is not allowed.", + ODataHandlerException.MessageKeys.HTTP_METHOD_NOT_ALLOWED, method.toString()); + } + + response.accepts(new ServiceResponseVisior() { + @Override + public void visit(EntityResponse response) throws ODataTranslatedException, + ODataApplicationException { + selectProcessor(EntityProcessor.class).readEntity(request.getODataRequest(), + response.getODataResponse(), request.getUriInfo(), request.getResponseContentType()); + } + + @Override + public void visit(PropertyResponse response) throws ODataTranslatedException, + ODataApplicationException { + if (request.isReturnTypePrimitive()) { + if(request.isCollection()) { + selectProcessor(PrimitiveCollectionProcessor.class).readPrimitiveCollection( + request.getODataRequest(), response.getODataResponse(), request.getUriInfo(), + request.getResponseContentType()); + + } else { + selectProcessor(PrimitiveProcessor.class).readPrimitive( + request.getODataRequest(), response.getODataResponse(), request.getUriInfo(), + request.getResponseContentType()); + } + } else { + if(request.isCollection()) { + selectProcessor(ComplexCollectionProcessor.class).readComplexCollection( + request.getODataRequest(), response.getODataResponse(), request.getUriInfo(), + request.getResponseContentType()); + + } else { + selectProcessor(ComplexProcessor.class).readComplex( + request.getODataRequest(), response.getODataResponse(), request.getUriInfo(), + request.getResponseContentType()); + } + } + } + @Override + public void visit(EntitySetResponse response) throws ODataTranslatedException, + ODataApplicationException { + selectProcessor(EntityCollectionProcessor.class).readEntityCollection(request.getODataRequest(), + response.getODataResponse(), request.getUriInfo(), request.getResponseContentType()); + } + }); + } + + @Override + public void invoke(final ActionRequest request, String eTag, final T response) + throws ODataTranslatedException, ODataApplicationException { + final HttpMethod method = request.getODataRequest().getMethod(); + if (method != HttpMethod.POST) { + throw new ODataHandlerException("HTTP method " + method + " is not allowed.", + ODataHandlerException.MessageKeys.HTTP_METHOD_NOT_ALLOWED, method.toString()); + } + response.accepts(new ServiceResponseVisior() { + @Override + public void visit(EntityResponse response) throws ODataTranslatedException, + ODataApplicationException { + selectProcessor(EntityProcessor.class).readEntity(request.getODataRequest(), + response.getODataResponse(), request.getUriInfo(), request.getResponseContentType()); + } + + @Override + public void visit(PropertyResponse response) throws ODataTranslatedException, + ODataApplicationException { + if (request.isReturnTypePrimitive()) { + if(request.isCollection()) { + selectProcessor(PrimitiveCollectionProcessor.class).readPrimitiveCollection( + request.getODataRequest(), response.getODataResponse(), request.getUriInfo(), + request.getResponseContentType()); + + } else { + selectProcessor(PrimitiveProcessor.class).readPrimitive( + request.getODataRequest(), response.getODataResponse(), request.getUriInfo(), + request.getResponseContentType()); + } + } else { + if(request.isCollection()) { + selectProcessor(ComplexCollectionProcessor.class).readComplexCollection( + request.getODataRequest(), response.getODataResponse(), request.getUriInfo(), + request.getResponseContentType()); + + } else { + selectProcessor(ComplexProcessor.class).readComplex( + request.getODataRequest(), response.getODataResponse(), request.getUriInfo(), + request.getResponseContentType()); + } + } + } + @Override + public void visit(EntitySetResponse response) throws ODataTranslatedException, + ODataApplicationException { + selectProcessor(EntityCollectionProcessor.class).readEntityCollection(request.getODataRequest(), + response.getODataResponse(), request.getUriInfo(), request.getResponseContentType()); + } + }); + } + + + @Override + public void readMediaStream(MediaRequest request, StreamResponse response) + throws ODataTranslatedException, ODataApplicationException { + selectProcessor(MediaEntityProcessor.class).readMediaEntity( + request.getODataRequest(), response.getODataResponse(), request.getUriInfo(), + request.getResponseContentType()); + } + + @Override + public void upsertMediaStream(MediaRequest request, String entityETag, InputStream mediaContent, + NoContentResponse response) throws ODataTranslatedException, ODataApplicationException { + selectProcessor(MediaEntityProcessor.class).updateMediaEntity( + request.getODataRequest(), response.getODataResponse(), request.getUriInfo(), + request.getRequestContentType(), request.getResponseContentType()); + } + + @Override + public void anyUnsupported(ODataRequest request, ODataResponse response) + throws ODataTranslatedException, ODataApplicationException { + throw new ODataHandlerException("not implemented", + ODataHandlerException.MessageKeys.FUNCTIONALITY_NOT_IMPLEMENTED); + } + + @Override + public void addReference(DataRequest request, String entityETag, List idReferences, + NoContentResponse response) throws ODataTranslatedException, ODataApplicationException { + selectProcessor(ReferenceProcessor.class).createReference( + request.getODataRequest(), response.getODataResponse(), request.getUriInfo(), + request.getResponseContentType()); + } + + @Override + public void updateReference(DataRequest request, String entityETag, URI referenceId, + NoContentResponse response) throws ODataTranslatedException, ODataApplicationException { + selectProcessor(ReferenceProcessor.class).updateReference( + request.getODataRequest(), response.getODataResponse(), request.getUriInfo(), + request.getResponseContentType()); + } + + @Override + public void deleteReference(DataRequest request, URI deleteId, String entityETag, + NoContentResponse response) throws ODataTranslatedException, ODataApplicationException { + selectProcessor(ReferenceProcessor.class).deleteReference( + request.getODataRequest(), response.getODataResponse(), request.getUriInfo()); + } + + @Override + public String startTransaction() { + return null; + } + + @Override + public void commit(String txnId) { + } + + @Override + public void rollback(String txnId) { + } + + @Override + public void crossJoin(DataRequest dataRequest, List entitySetNames, ODataResponse response) + throws ODataTranslatedException, ODataApplicationException { + throw new ODataHandlerException("not implemented", + ODataHandlerException.MessageKeys.FUNCTIONALITY_NOT_IMPLEMENTED); + } +} diff --git a/lib/server-core-ext/src/main/java/org/apache/olingo/server/core/requests/ActionRequest.java b/lib/server-core-ext/src/main/java/org/apache/olingo/server/core/requests/ActionRequest.java new file mode 100644 index 000000000..133ee3e0c --- /dev/null +++ b/lib/server-core-ext/src/main/java/org/apache/olingo/server/core/requests/ActionRequest.java @@ -0,0 +1,120 @@ +/* + * 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.requests; + +import org.apache.olingo.commons.api.edm.EdmAction; +import org.apache.olingo.commons.api.edm.EdmReturnType; +import org.apache.olingo.server.api.OData; +import org.apache.olingo.server.api.ODataApplicationException; +import org.apache.olingo.server.api.ODataResponse; +import org.apache.olingo.server.api.ODataTranslatedException; +import org.apache.olingo.server.api.ServiceMetadata; +import org.apache.olingo.server.api.uri.UriResourceAction; +import org.apache.olingo.server.core.ServiceHandler; +import org.apache.olingo.server.core.responses.EntityResponse; +import org.apache.olingo.server.core.responses.EntitySetResponse; +import org.apache.olingo.server.core.responses.NoContentResponse; +import org.apache.olingo.server.core.responses.PrimitiveValueResponse; +import org.apache.olingo.server.core.responses.PropertyResponse; + +public class ActionRequest extends OperationRequest { + private UriResourceAction uriResourceAction; + + public ActionRequest(OData odata, ServiceMetadata serviceMetadata) { + super(odata, serviceMetadata); + } + + @Override + public void execute(ServiceHandler handler, ODataResponse response) + throws ODataTranslatedException, ODataApplicationException { + + if (!allowedMethod()) { + methodNotAllowed(); + } + // Actions MAY return data but MUST NOT be further composed with additional + // path segments. + // On success, the response is 201 Created for actions that create entities, + // 200 OK for actions + // that return results or 204 No Content for action without a return type. + // The client can request + // whether any results from the action be returned using the Prefer header. + + if (!hasReturnType()) { + handler.invoke(this, getETag(), new NoContentResponse(getServiceMetaData(), response)); + } else { + if (isReturnTypePrimitive()) { + handler.invoke(this, getETag(), + PrimitiveValueResponse.getInstance(this, response, isCollection(), getReturnType())); + } else if (isReturnTypeComplex()) { + handler.invoke(this, getETag(), PropertyResponse.getInstance(this, response, + getReturnType().getType(), getContextURL(this.odata), isCollection())); + } else { + // EdmTypeKind.ENTITY + if (isCollection()) { + handler.invoke(this, getETag(), + EntitySetResponse.getInstance(this, getContextURL(odata), false, response)); + } else { + handler.invoke(this, getETag(), + EntityResponse.getInstance(this, getContextURL(odata), false, response)); + } + } + } + } + + @Override + public boolean allowedMethod() { + // 11.5.4.1 Invoking an Action - only allows POST + return (isPOST()); + } + + public UriResourceAction getUriResourceAction() { + return uriResourceAction; + } + + public void setUriResourceAction(UriResourceAction uriResourceAction) { + this.uriResourceAction = uriResourceAction; + } + + @Override + public boolean isBound() { + return this.uriResourceAction.getActionImport() != null; + } + + public EdmAction getAction() { + return this.uriResourceAction.getAction(); + } + + @Override + public boolean isCollection() { + assert (hasReturnType()); + return getAction().getReturnType().isCollection(); + } + + @Override + public EdmReturnType getReturnType() { + assert (hasReturnType()); + return getAction().getReturnType(); + } + + @Override + public boolean hasReturnType() { + return getAction().getReturnType() != null; + } +} \ No newline at end of file diff --git a/lib/server-core-ext/src/main/java/org/apache/olingo/server/core/requests/BatchRequest.java b/lib/server-core-ext/src/main/java/org/apache/olingo/server/core/requests/BatchRequest.java new file mode 100644 index 000000000..25af023e0 --- /dev/null +++ b/lib/server-core-ext/src/main/java/org/apache/olingo/server/core/requests/BatchRequest.java @@ -0,0 +1,197 @@ +/* + * 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.requests; + +import java.io.InputStream; +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; +import java.util.UUID; + +import org.apache.olingo.commons.api.format.ContentType; +import org.apache.olingo.commons.api.http.HttpHeader; +import org.apache.olingo.commons.api.http.HttpStatusCode; +import org.apache.olingo.server.api.OData; +import org.apache.olingo.server.api.ODataApplicationException; +import org.apache.olingo.server.api.ODataRequest; +import org.apache.olingo.server.api.ODataResponse; +import org.apache.olingo.server.api.ODataTranslatedException; +import org.apache.olingo.server.api.ServiceMetadata; +import org.apache.olingo.server.api.batch.exception.BatchDeserializerException; +import org.apache.olingo.server.api.deserializer.batch.BatchOptions; +import org.apache.olingo.server.api.deserializer.batch.BatchRequestPart; +import org.apache.olingo.server.api.deserializer.batch.ODataResponsePart; +import org.apache.olingo.server.core.ContentNegotiatorException; +import org.apache.olingo.server.core.ErrorHandler; +import org.apache.olingo.server.core.ServiceDispatcher; +import org.apache.olingo.server.core.ServiceHandler; +import org.apache.olingo.server.core.ServiceRequest; +import org.apache.olingo.server.core.batchhandler.referenceRewriting.BatchReferenceRewriter; +import org.apache.olingo.server.core.deserializer.batch.BatchParserCommon; + +public class BatchRequest extends ServiceRequest { + private static final String PREFERENCE_CONTINUE_ON_ERROR = "odata.continue-on-error"; + private final BatchReferenceRewriter rewriter; + + public BatchRequest(OData odata, ServiceMetadata serviceMetadata) { + super(odata, serviceMetadata); + this.rewriter = new BatchReferenceRewriter(); + } + + @Override + public void execute(ServiceHandler handler, ODataResponse response) + throws ODataTranslatedException, ODataApplicationException { + + if (!allowedMethod()) { + methodNotAllowed(); + } + + validateContentType(); + boolean continueOnError = isContinueOnError(); + final String boundary = extractBoundary(getRequestContentType()); + + final BatchOptions options = BatchOptions.with().rawBaseUri(request.getRawBaseUri()) + .rawServiceResolutionUri(this.request.getRawServiceResolutionUri()).build(); + + final List parts = this.odata.createFixedFormatDeserializer() + .parseBatchRequest(request.getBody(), boundary, options); + + ODataResponsePart partResponse = null; + final List responseParts = new ArrayList(); + + for (BatchRequestPart part : parts) { + if (part.isChangeSet()) { + String txnId = handler.startTransaction(); + partResponse = processChangeSet(part, handler); + if (partResponse.getResponses().get(0).getStatusCode() > 400) { + handler.rollback(txnId); + } + handler.commit(txnId); + } else { + // single request, a static request + ODataRequest partRequest = part.getRequests().get(0); + partResponse = process(partRequest, handler); + } + responseParts.add(partResponse); + + // on error, should we continue? + final int statusCode = partResponse.getResponses().get(0).getStatusCode(); + if ((statusCode >= 400 && statusCode <= 600) && !continueOnError) { + break; + } + } + + // send response + final String responseBoundary = "batch_" + UUID.randomUUID().toString(); + ; + final InputStream responseContent = odata.createFixedFormatSerializer().batchResponse( + responseParts, responseBoundary); + response.setHeader(HttpHeader.CONTENT_TYPE, ContentType.MULTIPART_MIXED + ";boundary=" + + responseBoundary); + response.setContent(responseContent); + response.setStatusCode(HttpStatusCode.ACCEPTED.getStatusCode()); + } + + ODataResponsePart process(ODataRequest partRequest, ServiceHandler serviceHandler) { + ODataResponse partResponse = executeSingleRequest(partRequest, serviceHandler); + addContentID(partRequest, partResponse); + return new ODataResponsePart(partResponse, false); + } + + ODataResponsePart processChangeSet(BatchRequestPart partRequest, ServiceHandler serviceHandler) + throws BatchDeserializerException { + List changeSetResponses = new ArrayList(); + // change set need to be a in a atomic operation + for (ODataRequest changeSetPartRequest : partRequest.getRequests()) { + + this.rewriter.replaceReference(changeSetPartRequest); + + ODataResponse partResponse = executeSingleRequest(changeSetPartRequest, serviceHandler); + + this.rewriter.addMapping(changeSetPartRequest, partResponse); + addContentID(changeSetPartRequest, partResponse); + + if (partResponse.getStatusCode() < 400) { + changeSetResponses.add(partResponse); + } else { + // 11.7.4 Responding to a Batch Request + return new ODataResponsePart(partResponse, false); + } + } + return new ODataResponsePart(changeSetResponses, true); + } + + ODataResponse executeSingleRequest(ODataRequest singleRequest, ServiceHandler handler) { + ServiceDispatcher dispatcher = new ServiceDispatcher(this.odata, this.serviceMetadata, handler, + this.customContentType); + ODataResponse res = new ODataResponse(); + try { + dispatcher.execute(singleRequest, res); + } catch (Exception e) { + ErrorHandler ehandler = new ErrorHandler(this.odata, this.serviceMetadata, + getCustomContentTypeSupport()); + ehandler.handleException(e, singleRequest, res); + } + return res; + } + + private void addContentID(ODataRequest batchPartRequest, ODataResponse batchPartResponse) { + final String contentId = batchPartRequest.getHeader(BatchParserCommon.HTTP_CONTENT_ID); + if (contentId != null) { + batchPartResponse.setHeader(BatchParserCommon.HTTP_CONTENT_ID, contentId); + } + } + + @Override + public boolean allowedMethod() { + return isPOST(); + } + + private void validateContentType() throws ODataApplicationException { + final String contentType = getRequestContentType().toContentTypeString(); + + if (contentType == null + || !BatchParserCommon.PATTERN_MULTIPART_BOUNDARY.matcher(contentType).matches()) { + throw new ODataApplicationException("Invalid content type", + HttpStatusCode.PRECONDITION_FAILED.getStatusCode(), Locale.getDefault()); + } + } + + @Override + public ContentType getResponseContentType() throws ContentNegotiatorException { + return null; + } + + private boolean isContinueOnError() { + final List preferValues = this.request.getHeaders(HttpHeader.PREFER); + + if (preferValues != null) { + for (final String preference : preferValues) { + if (PREFERENCE_CONTINUE_ON_ERROR.equals(preference)) { + return true; + } + } + } + return false; + } + + private String extractBoundary(ContentType contentType) throws BatchDeserializerException { + return BatchParserCommon.getBoundary(contentType.toContentTypeString(), 0); + } +} diff --git a/lib/server-core-ext/src/main/java/org/apache/olingo/server/core/requests/DataRequest.java b/lib/server-core-ext/src/main/java/org/apache/olingo/server/core/requests/DataRequest.java new file mode 100644 index 000000000..32bf26a57 --- /dev/null +++ b/lib/server-core-ext/src/main/java/org/apache/olingo/server/core/requests/DataRequest.java @@ -0,0 +1,769 @@ +/* + * 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.requests; + +import java.io.InputStream; +import java.net.URI; +import java.net.URISyntaxException; +import java.util.LinkedList; +import java.util.List; + +import org.apache.olingo.commons.api.data.ContextURL; +import org.apache.olingo.commons.api.data.ContextURL.Suffix; +import org.apache.olingo.commons.api.data.Entity; +import org.apache.olingo.commons.api.data.Property; +import org.apache.olingo.commons.api.edm.EdmBindingTarget; +import org.apache.olingo.commons.api.edm.EdmComplexType; +import org.apache.olingo.commons.api.edm.EdmEntitySet; +import org.apache.olingo.commons.api.edm.EdmEntityType; +import org.apache.olingo.commons.api.edm.EdmNavigationProperty; +import org.apache.olingo.commons.api.edm.EdmPrimitiveTypeKind; +import org.apache.olingo.commons.api.edm.EdmProperty; +import org.apache.olingo.commons.api.format.ContentType; +import org.apache.olingo.commons.api.format.ODataFormat; +import org.apache.olingo.commons.api.http.HttpHeader; +import org.apache.olingo.commons.core.data.PropertyImpl; +import org.apache.olingo.commons.core.edm.primitivetype.EdmPrimitiveTypeFactory; +import org.apache.olingo.commons.core.edm.primitivetype.EdmStream; +import org.apache.olingo.server.api.OData; +import org.apache.olingo.server.api.ODataApplicationException; +import org.apache.olingo.server.api.ODataResponse; +import org.apache.olingo.server.api.ODataTranslatedException; +import org.apache.olingo.server.api.ServiceMetadata; +import org.apache.olingo.server.api.deserializer.DeserializerException; +import org.apache.olingo.server.api.deserializer.DeserializerException.MessageKeys; +import org.apache.olingo.server.api.deserializer.ODataDeserializer; +import org.apache.olingo.server.api.serializer.PrimitiveSerializerOptions; +import org.apache.olingo.server.api.serializer.RepresentationType; +import org.apache.olingo.server.api.serializer.SerializerException; +import org.apache.olingo.server.api.uri.UriHelper; +import org.apache.olingo.server.api.uri.UriInfo; +import org.apache.olingo.server.api.uri.UriInfoCrossjoin; +import org.apache.olingo.server.api.uri.UriInfoResource; +import org.apache.olingo.server.api.uri.UriParameter; +import org.apache.olingo.server.api.uri.UriResource; +import org.apache.olingo.server.api.uri.UriResourceComplexProperty; +import org.apache.olingo.server.api.uri.UriResourceEntitySet; +import org.apache.olingo.server.api.uri.UriResourceNavigation; +import org.apache.olingo.server.api.uri.UriResourcePrimitiveProperty; +import org.apache.olingo.server.api.uri.UriResourceProperty; +import org.apache.olingo.server.api.uri.UriResourceSingleton; +import org.apache.olingo.server.core.ContentNegotiator; +import org.apache.olingo.server.core.ContentNegotiatorException; +import org.apache.olingo.server.core.ServiceHandler; +import org.apache.olingo.server.core.ServiceRequest; +import org.apache.olingo.server.core.responses.CountResponse; +import org.apache.olingo.server.core.responses.EntityResponse; +import org.apache.olingo.server.core.responses.EntitySetResponse; +import org.apache.olingo.server.core.responses.NoContentResponse; +import org.apache.olingo.server.core.responses.PrimitiveValueResponse; +import org.apache.olingo.server.core.responses.PropertyResponse; +import org.apache.olingo.server.core.responses.StreamResponse; + +public class DataRequest extends ServiceRequest { + protected UriResourceEntitySet uriResourceEntitySet; + private boolean countRequest; + private UriResourceProperty uriResourceProperty; + private boolean valueRequest; + private final LinkedList uriNavigations = new LinkedList(); + private boolean references; + + private RequestType type; + private UriResourceSingleton uriResourceSingleton; + + /** + * This sub-categorizes the request so that code can be simplified + */ + interface RequestType { + public boolean allowedMethod(); + + public ContentType getResponseContentType() throws ContentNegotiatorException; + + public ContextURL getContextURL(OData odata) throws SerializerException; + + public void execute(ServiceHandler handler, ODataResponse response) + throws ODataTranslatedException, ODataApplicationException; + } + + public DataRequest(OData odata, ServiceMetadata serviceMetadata) { + super(odata, serviceMetadata); + } + + public UriResourceEntitySet getUriResourceEntitySet() { + return uriResourceEntitySet; + } + + public void setUriResourceEntitySet(UriResourceEntitySet uriResourceEntitySet) { + this.uriResourceEntitySet = uriResourceEntitySet; + this.type = new EntityRequest(); + } + + public void setCrossJoin(UriInfoCrossjoin info) { + this.type = new CrossJoinRequest(info.getEntitySetNames()); + } + + public boolean isSingleton() { + return this.uriResourceSingleton != null; + } + + public boolean isCollection() { + if (!this.uriNavigations.isEmpty()) { + return this.uriNavigations.getLast().isCollection(); + } + return this.uriResourceEntitySet != null && this.uriResourceEntitySet.isCollection(); + } + + public EdmEntitySet getEntitySet() { + return this.uriResourceEntitySet.getEntitySet(); + } + + public boolean isCountRequest() { + return countRequest; + } + + public void setCountRequest(boolean countRequest) { + this.countRequest = countRequest; + this.type = new CountRequest(); + } + + public boolean isPropertyRequest() { + return this.uriResourceProperty != null; + } + + public boolean isPropertyComplex() { + return (this.uriResourceProperty instanceof UriResourceComplexProperty); + } + + public boolean isPropertyStream() { + if (isPropertyComplex()) { + return false; + } + EdmProperty property = ((UriResourcePrimitiveProperty)this.uriResourceProperty).getProperty(); + return (property.getType() instanceof EdmStream); + } + + public UriResourceProperty getUriResourceProperty() { + return uriResourceProperty; + } + + public void setUriResourceProperty(UriResourceProperty uriResourceProperty) { + this.uriResourceProperty = uriResourceProperty; + this.type = new PropertyRequest(); + } + + public LinkedList getNavigations() { + return this.uriNavigations; + } + + public void addUriResourceNavigation(UriResourceNavigation uriResourceNavigation) { + this.uriNavigations.add(uriResourceNavigation); + } + + public UriResourceSingleton getUriResourceSingleton() { + return this.uriResourceSingleton; + } + + public void setUriResourceSingleton(UriResourceSingleton info) { + this.uriResourceSingleton = info; + this.type = new SingletonRequest(); + } + + public List getKeyPredicates() { + if (this.uriResourceEntitySet != null) { + return this.uriResourceEntitySet.getKeyPredicates(); + } + return null; + } + + public boolean isReferenceRequest() { + return this.references; + } + + public void setReferenceRequest(boolean ref) { + this.references = ref; + this.type = new ReferenceRequest(); + } + + public boolean isValueRequest() { + return valueRequest; + } + + private boolean hasMediaStream() { + return this.uriResourceEntitySet != null && this.uriResourceEntitySet.getEntityType().hasStream(); + } + + private InputStream getMediaStream() { + return this.request.getBody(); + } + + public void setValueRequest(boolean valueRequest) { + this.valueRequest = valueRequest; + this.type = new ValueRequest(); + } + + @Override + public boolean allowedMethod() { + return this.type.allowedMethod(); + } + + public ContextURL getContextURL(OData odata) throws SerializerException { + return type.getContextURL(odata); + } + + @Override + public void execute(ServiceHandler handler, ODataResponse response) + throws ODataTranslatedException, ODataApplicationException { + + if (!this.type.allowedMethod()) { + methodNotAllowed(); + } + + this.type.execute(handler, response); + } + + @Override + public T getSerializerOptions(Class serilizerOptions, ContextURL contextUrl, boolean references) + throws ContentNegotiatorException { + if (serilizerOptions.isAssignableFrom(PrimitiveSerializerOptions.class)) { + return (T) PrimitiveSerializerOptions.with().contextURL(contextUrl) + .facetsFrom(getUriResourceProperty().getProperty()).build(); + } + return super.getSerializerOptions(serilizerOptions, contextUrl, references); + } + + @Override + public ContentType getResponseContentType() throws ContentNegotiatorException { + return type.getResponseContentType(); + } + + class EntityRequest implements RequestType { + + @Override + public boolean allowedMethod() { + // the create/update/delete to navigation property is done through references + // see # 11.4.6 + if (!getNavigations().isEmpty() && !isGET()) { + return false; + } + return true; + } + + @Override + public ContentType getResponseContentType() throws ContentNegotiatorException { + return ContentNegotiator.doContentNegotiation(uriInfo.getFormatOption(), getODataRequest(), + getCustomContentTypeSupport(), isCollection() ? RepresentationType.COLLECTION_ENTITY + : RepresentationType.ENTITY); + } + + @Override + public void execute(ServiceHandler handler, ODataResponse response) + throws ODataTranslatedException, ODataApplicationException { + + EntityResponse entityResponse = EntityResponse.getInstance(DataRequest.this, + getContextURL(odata), false, response); + + if (isGET()) { + if (isCollection()) { + handler.read(DataRequest.this, + EntitySetResponse.getInstance(DataRequest.this, getContextURL(odata), false, response)); + } else { + handler.read(DataRequest.this,entityResponse); + } + } else if (isPUT() || isPATCH()) { + // RFC 2616: he result of a request having both an If-Match header field and either + // an If-None-Match or an If-Modified-Since header fields is undefined + // by this specification. + boolean ifMatch = getHeader(HttpHeader.IF_MATCH) != null; + boolean ifNoneMatch = getHeader(HttpHeader.IF_NONE_MATCH).equals("*"); + if(ifMatch) { + handler.updateEntity(DataRequest.this, getEntityFromClient(), isPATCH(), getETag(), + entityResponse); + } else if (ifNoneMatch) { + // 11.4.4 + entityResponse = EntityResponse.getInstance(DataRequest.this, + getContextURL(odata), false, response, getReturnRepresentation()); + handler.createEntity(DataRequest.this, getEntityFromClient(), entityResponse); + } else { + handler.updateEntity(DataRequest.this, getEntityFromClient(), isPATCH(), getETag(), + entityResponse); + } + } else if (isPOST()) { + entityResponse = EntityResponse.getInstance(DataRequest.this, + getContextURL(odata), false, response, getReturnRepresentation()); + handler.createEntity(DataRequest.this, getEntityFromClient(),entityResponse); + } else if (isDELETE()) { + handler.deleteEntity(DataRequest.this, getETag(), entityResponse); + } + } + + private Entity getEntityFromClient() throws DeserializerException { + ODataDeserializer deserializer = odata.createDeserializer(ODataFormat + .fromContentType(getRequestContentType())); + return deserializer.entity(getODataRequest().getBody(), getEntitySet().getEntityType()); + } + + @Override + public ContextURL getContextURL(OData odata) throws SerializerException { + // EntitySet based return + final UriHelper helper = odata.createUriHelper(); + ContextURL.Builder builder = buildEntitySetContextURL(helper, getEntitySet(), + getKeyPredicates(), getUriInfo(), getNavigations(), isCollection(), false); + return builder.build(); + } + } + + class CountRequest implements RequestType { + + @Override + public boolean allowedMethod() { + return isGET(); + } + + @Override + public ContentType getResponseContentType() throws ContentNegotiatorException { + return ContentType.TEXT_PLAIN; + } + + @Override + public void execute(ServiceHandler handler, ODataResponse response) + throws ODataTranslatedException, ODataApplicationException { + handler.read(DataRequest.this, CountResponse.getInstance(DataRequest.this, response)); + } + + @Override + public ContextURL getContextURL(OData odata) throws SerializerException { + return null; + } + } + + /** + * Is NavigationProperty Reference. + */ + class ReferenceRequest implements RequestType { + + @Override + public boolean allowedMethod() { + // references are only allowed on the navigation properties + if (getNavigations().isEmpty()) { + return false; + } + + // 11.4.6.1 - post allowed on only collection valued navigation + if (isPOST() && !getNavigations().getLast().isCollection()) { + return false; + } + + // 11.4.6.3 - PUT allowed on single valued navigation + if (isPUT() && getNavigations().getLast().isCollection()) { + return false; + } + + // No defined behavior in spec + if (isPATCH()) { + return false; + } + + return true; + } + + @Override + public ContentType getResponseContentType() throws ContentNegotiatorException { + return ContentNegotiator.doContentNegotiation(uriInfo.getFormatOption(), getODataRequest(), + getCustomContentTypeSupport(), isCollection() ? RepresentationType.COLLECTION_REFERENCE + : RepresentationType.REFERENCE); + } + + @Override + public void execute(ServiceHandler handler, ODataResponse response) + throws ODataTranslatedException, ODataApplicationException { + if (isGET()) { + if (isCollection()) { + handler.read(DataRequest.this, + EntitySetResponse.getInstance(DataRequest.this, getContextURL(odata), true, response)); + } else { + handler.read(DataRequest.this, + EntityResponse.getInstance(DataRequest.this, getContextURL(odata), true, response)); + } + } else if (isDELETE()) { + // if this against the collection, user need to look at $id param for entity ref #11.4.6.2 + String id = getQueryParameter("$id"); + if (id == null) { + handler.deleteReference(DataRequest.this, null, getETag(), new NoContentResponse( + getServiceMetaData(), response)); + } else { + try { + handler.deleteReference(DataRequest.this, new URI(id), getETag(), new NoContentResponse( + getServiceMetaData(), response)); + } catch (URISyntaxException e) { + throw new DeserializerException("failed to read $id", e, MessageKeys.UNKOWN_CONTENT); + } + } + } else if (isPUT()) { + // note this is always against single reference + handler.updateReference(DataRequest.this, getETag(), getPayload().get(0), new NoContentResponse( + getServiceMetaData(), response)); + } else if (isPOST()) { + // this needs to be against collection of references + handler.addReference(DataRequest.this, getETag(), getPayload(), new NoContentResponse( + getServiceMetaData(), response)); + } + } + + // http://docs.oasis-open.org/odata/odata-json-format/v4.0/errata02/os + // /odata-json-format-v4.0-errata02-os-complete.html#_Toc403940643 + // The below code reads as property and converts to an URI + private List getPayload() throws DeserializerException { + ODataDeserializer deserializer = odata.createDeserializer(ODataFormat + .fromContentType(getRequestContentType())); + return deserializer.entityReferences(getODataRequest().getBody()); + } + + @Override + public ContextURL getContextURL(OData odata) throws SerializerException { + ContextURL.Builder builder = ContextURL.with().suffix(Suffix.REFERENCE); + if (isCollection()) { + builder.asCollection(); + } + return builder.build(); + } + } + + class PropertyRequest implements RequestType { + + @Override + public boolean allowedMethod() { + // create of properties is not allowed, + // only read, update, delete. Note that delete is + // same as update with null + if (isPOST()) { + return false; + } + + // 11.4.9.4, collection properties are not supported with merge + if (isPATCH() && (isCollection() || isPropertyStream())) { + return false; + } + return true; + } + + @Override + public ContentType getResponseContentType() throws ContentNegotiatorException { + if (isPropertyComplex()) { + return ContentNegotiator.doContentNegotiation(getUriInfo().getFormatOption(), + getODataRequest(), getCustomContentTypeSupport(), + isCollection() ? RepresentationType.COLLECTION_COMPLEX : RepresentationType.COMPLEX); + } else if (isPropertyStream()) { + return ContentNegotiator.doContentNegotiation(uriInfo.getFormatOption(), request, + getCustomContentTypeSupport(), RepresentationType.BINARY); + } + return ContentNegotiator.doContentNegotiation(uriInfo.getFormatOption(), getODataRequest(), + getCustomContentTypeSupport(), isCollection() ? RepresentationType.COLLECTION_PRIMITIVE + : RepresentationType.PRIMITIVE); + } + + @Override + public void execute(ServiceHandler handler, ODataResponse response) + throws ODataTranslatedException, ODataApplicationException { + + EdmProperty edmProperty = getUriResourceProperty().getProperty(); + + if (isGET()) { + if (isPropertyStream()) { + handler.read(DataRequest.this, new StreamResponse(getServiceMetaData(), response)); + } else { + handler.read(DataRequest.this, buildResponse(response, edmProperty)); + } + } else if (isPATCH()) { + handler.updateProperty(DataRequest.this, getPropertyValueFromClient(edmProperty), true, + getETag(), buildResponse(response, edmProperty)); + } else if (isPUT()) { + if (isPropertyStream()) { + handler.upsertStreamProperty(DataRequest.this, getETag(), request.getBody(), + new NoContentResponse(getServiceMetaData(), response)); + } else { + handler.updateProperty(DataRequest.this, getPropertyValueFromClient(edmProperty), false, + getETag(), buildResponse(response, edmProperty)); + } + } else if (isDELETE()) { + if (isPropertyStream()) { + handler.upsertStreamProperty(DataRequest.this, getETag(), request.getBody(), + new NoContentResponse(getServiceMetaData(), response)); + } else { + Property property = new PropertyImpl(); + property.setName(edmProperty.getName()); + property.setType(edmProperty.getType().getFullQualifiedName() + .getFullQualifiedNameAsString()); + handler.updateProperty(DataRequest.this, property, false, getETag(), + buildResponse(response, edmProperty)); + } + } + } + + private PropertyResponse buildResponse(ODataResponse response, EdmProperty edmProperty) + throws ContentNegotiatorException, SerializerException { + PropertyResponse propertyResponse = PropertyResponse.getInstance(DataRequest.this, response, + edmProperty.getType(), getContextURL(odata), edmProperty.isCollection()); + return propertyResponse; + } + + @Override + public ContextURL getContextURL(OData odata) throws SerializerException { + final UriHelper helper = odata.createUriHelper(); + EdmProperty edmProperty = getUriResourceProperty().getProperty(); + + ContextURL.Builder builder = ContextURL.with().entitySet(getEntitySet()); + builder = ContextURL.with().entitySet(getEntitySet()); + builder.keyPath(helper.buildContextURLKeyPredicate(getUriResourceEntitySet() + .getKeyPredicates())); + String navPath = buildNavPath(helper, getEntitySet().getEntityType(), getNavigations(), true); + if (navPath != null && !navPath.isEmpty()) { + builder.navOrPropertyPath(navPath+"/"+edmProperty.getName()); + } else { + builder.navOrPropertyPath(edmProperty.getName()); + } + if (isPropertyComplex()) { + EdmComplexType type = ((UriResourceComplexProperty) uriResourceProperty).getComplexType(); + String select = helper.buildContextURLSelectList(type, getUriInfo().getExpandOption(), + getUriInfo().getSelectOption()); + builder.selectList(select); + } + return builder.build(); + } + } + + class ValueRequest extends PropertyRequest { + + @Override + public boolean allowedMethod() { + //part2-url-conventions # 4.2 + if (isPropertyStream() && isGET()) { + return false; + } + + return isGET() || isDELETE() || isPUT(); + } + + @Override + public ContentType getResponseContentType() throws ContentNegotiatorException { + RepresentationType valueRepresentationType = uriResourceProperty.getType() == EdmPrimitiveTypeFactory + .getInstance(EdmPrimitiveTypeKind.Binary) ? RepresentationType.BINARY + : RepresentationType.VALUE; + return ContentNegotiator.doContentNegotiation(uriInfo.getFormatOption(), request, + getCustomContentTypeSupport(), valueRepresentationType); + } + + @Override + public void execute(ServiceHandler handler, ODataResponse response) + throws ODataTranslatedException, ODataApplicationException { + EdmProperty edmProperty = getUriResourceProperty().getProperty(); + if (isGET()) { + handler.read(DataRequest.this, PrimitiveValueResponse.getInstance(DataRequest.this, + response, isCollection(), getUriResourceProperty().getProperty())); + } else if (isDELETE()) { + Property property = new PropertyImpl(); + property.setName(edmProperty.getName()); + property.setType(edmProperty.getType().getFullQualifiedName().getFullQualifiedNameAsString()); + + PropertyResponse propertyResponse = PropertyResponse.getInstance(DataRequest.this, response, + edmProperty.getType(), getContextURL(odata), edmProperty.isCollection()); + handler.updateProperty(DataRequest.this, property, false, getETag(), propertyResponse); + } else if (isPUT()) { + PropertyResponse propertyResponse = PropertyResponse.getInstance(DataRequest.this, response, + edmProperty.getType(), getContextURL(odata), edmProperty.isCollection()); + handler.updateProperty(DataRequest.this, getPropertyValueFromClient(edmProperty), false, + getETag(), propertyResponse); + } + } + + @Override + public ContextURL getContextURL(OData odata) throws SerializerException { + return null; + } + } + + class SingletonRequest implements RequestType { + + @Override + public boolean allowedMethod() { + return isGET(); + } + + @Override + public ContentType getResponseContentType() throws ContentNegotiatorException { + return ContentNegotiator.doContentNegotiation(uriInfo.getFormatOption(), getODataRequest(), + getCustomContentTypeSupport(), RepresentationType.ENTITY); + } + + @Override + public ContextURL getContextURL(OData odata) throws SerializerException { + final UriHelper helper = odata.createUriHelper(); + ContextURL.Builder builder = buildEntitySetContextURL(helper, + uriResourceSingleton.getSingleton(), null, getUriInfo(), getNavigations(), isCollection(), true); + return builder.build(); + } + + @Override + public void execute(ServiceHandler handler, ODataResponse response) + throws ODataTranslatedException, ODataApplicationException { + handler.read(DataRequest.this, + EntityResponse.getInstance(DataRequest.this, getContextURL(odata), false, response)); + } + } + + class CrossJoinRequest implements RequestType { + private final List entitySetNames; + + public CrossJoinRequest(List entitySetNames) { + this.entitySetNames = entitySetNames; + } + + @Override + public boolean allowedMethod() { + return isGET(); + } + + @Override + public ContentType getResponseContentType() throws ContentNegotiatorException { + return ContentNegotiator.doContentNegotiation(getUriInfo().getFormatOption(), + getODataRequest(), getCustomContentTypeSupport(), RepresentationType.COLLECTION_COMPLEX); + } + + @Override + public void execute(ServiceHandler handler, ODataResponse response) + throws ODataTranslatedException, ODataApplicationException { + handler.crossJoin(DataRequest.this, this.entitySetNames, response); + } + + @Override + public ContextURL getContextURL(OData odata) throws SerializerException { + ContextURL.Builder builder = ContextURL.with().asCollection(); + return builder.build(); + } + } + + private org.apache.olingo.commons.api.data.Property getPropertyValueFromClient( + EdmProperty edmProperty) throws DeserializerException { + // TODO:this is not right, we should be deserializing the property + // (primitive, complex, collection of) + // for now it is responsibility of the user + ODataDeserializer deserializer = odata.createDeserializer(ODataFormat + .fromContentType(getRequestContentType())); + return deserializer.property(getODataRequest().getBody(), edmProperty); + } + + static ContextURL.Builder buildEntitySetContextURL(UriHelper helper, + EdmBindingTarget edmEntitySet, List keyPredicates, UriInfo uriInfo, + LinkedList navigations, boolean collectionReturn, boolean singleton) + throws SerializerException { + + ContextURL.Builder builder = ContextURL.with().entitySetOrSingletonOrType(edmEntitySet.getName()); + String select = helper.buildContextURLSelectList(edmEntitySet.getEntityType(), + uriInfo.getExpandOption(), uriInfo.getSelectOption()); + if (!singleton) { + builder.suffix(collectionReturn ? null : Suffix.ENTITY); + } + + builder.selectList(select); + + final UriInfoResource resource = uriInfo.asUriInfoResource(); + final List resourceParts = resource.getUriResourceParts(); + final List path = getPropertyPath(resourceParts); + String propertyPath = buildPropertyPath(path); + final String navPath = buildNavPath(helper, edmEntitySet.getEntityType(), navigations, collectionReturn); + if (navPath != null && !navPath.isEmpty()) { + if (keyPredicates != null) { + builder.keyPath(helper.buildContextURLKeyPredicate(keyPredicates)); + } + if (propertyPath != null) { + propertyPath = navPath+"/"+propertyPath; + } else { + propertyPath = navPath; + } + } + builder.navOrPropertyPath(propertyPath); + return builder; + } + + private static List getPropertyPath(final List path) { + List result = new LinkedList(); + int index = 1; + while (index < path.size() && path.get(index) instanceof UriResourceProperty) { + result.add(((UriResourceProperty) path.get(index)).getProperty().getName()); + index++; + } + return result; + } + + private static String buildPropertyPath(final List path) { + StringBuilder result = new StringBuilder(); + for (final String segment : path) { + result.append(result.length() == 0 ? "" : '/').append(segment); //$NON-NLS-1$ + } + return result.length() == 0?null:result.toString(); + } + + static String buildNavPath(UriHelper helper, EdmEntityType rootType, + LinkedList navigations, boolean includeLastPredicates) + throws SerializerException { + if (navigations.isEmpty()) { + return null; + } + StringBuilder sb = new StringBuilder(); + boolean containsTarget = false; + EdmEntityType type = rootType; + for (UriResourceNavigation nav:navigations) { + String name = nav.getProperty().getName(); + EdmNavigationProperty property = type.getNavigationProperty(name); + if (property.containsTarget()) { + containsTarget = true; + } + type = nav.getProperty().getType(); + } + + if (containsTarget) { + for (int i = 0; i < navigations.size(); i++) { + UriResourceNavigation nav = navigations.get(i); + if (i > 0) { + sb.append("/"); + } + sb.append(nav.getProperty().getName()); + + boolean skipKeys = false; + if (navigations.size() == i+1 && !includeLastPredicates ) { + skipKeys = true; + } + + if (!skipKeys && !nav.getKeyPredicates().isEmpty()) { + sb.append("("); + sb.append(helper.buildContextURLKeyPredicate(nav.getKeyPredicates())); + sb.append(")"); + } + + if (nav.getTypeFilterOnCollection() != null) { + sb.append("/") + .append(nav.getTypeFilterOnCollection().getFullQualifiedName().getFullQualifiedNameAsString()); + } else if (nav.getTypeFilterOnEntry() != null) { + sb.append("/") + .append(nav.getTypeFilterOnEntry().getFullQualifiedName().getFullQualifiedNameAsString()); + } + } + } + return sb.toString(); + } +} diff --git a/lib/server-core-ext/src/main/java/org/apache/olingo/server/core/requests/FunctionRequest.java b/lib/server-core-ext/src/main/java/org/apache/olingo/server/core/requests/FunctionRequest.java new file mode 100644 index 000000000..a9f9341d6 --- /dev/null +++ b/lib/server-core-ext/src/main/java/org/apache/olingo/server/core/requests/FunctionRequest.java @@ -0,0 +1,122 @@ +/* + * 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.requests; + +import java.util.List; + +import org.apache.olingo.commons.api.edm.EdmFunction; +import org.apache.olingo.commons.api.edm.EdmReturnType; +import org.apache.olingo.server.api.OData; +import org.apache.olingo.server.api.ODataApplicationException; +import org.apache.olingo.server.api.ODataResponse; +import org.apache.olingo.server.api.ODataTranslatedException; +import org.apache.olingo.server.api.ServiceMetadata; +import org.apache.olingo.server.api.uri.UriParameter; +import org.apache.olingo.server.api.uri.UriResourceFunction; +import org.apache.olingo.server.core.ServiceHandler; +import org.apache.olingo.server.core.responses.EntityResponse; +import org.apache.olingo.server.core.responses.EntitySetResponse; +import org.apache.olingo.server.core.responses.PrimitiveValueResponse; +import org.apache.olingo.server.core.responses.PropertyResponse; + +public class FunctionRequest extends OperationRequest { + private UriResourceFunction uriResourceFunction; + + public FunctionRequest(OData odata, ServiceMetadata serviceMetadata) { + super(odata, serviceMetadata); + } + + @Override + public void execute(ServiceHandler handler, ODataResponse response) + throws ODataTranslatedException, ODataApplicationException { + + if (!allowedMethod()) { + methodNotAllowed(); + } + + // Functions always have return per 11.5.3 + if (isReturnTypePrimitive()) { + // functions can not return a typed property in the context of entity, so + // it must be treated + // as value based response + handler.invoke(this, getODataRequest().getMethod(), + PrimitiveValueResponse.getInstance(this, response, isCollection(), getReturnType())); + } else if (isReturnTypeComplex()) { + handler.invoke(this, getODataRequest().getMethod(), PropertyResponse.getInstance(this, response, + getReturnType().getType(), getContextURL(this.odata), isCollection())); + } else { + // returnType.getType().getKind() == EdmTypeKind.ENTITY + if (isCollection()) { + handler.invoke(this, getODataRequest().getMethod(), + EntitySetResponse.getInstance(this, getContextURL(odata), false, response)); + } else { + handler.invoke(this, getODataRequest().getMethod(), + EntityResponse.getInstance(this, getContextURL(odata), false, response)); + } + } + } + + @Override + public boolean allowedMethod() { + // look for discussion about composable functions in odata-discussion + // group with thread "Clarification on "Function" invocations" + if (getFunction().isComposable()) { + return (isGET() || isPATCH() || isDELETE() || isPOST() || isPUT()); + } + return isGET(); + } + + public UriResourceFunction getUriResourceFunction() { + return uriResourceFunction; + } + + public void setUriResourceFunction(UriResourceFunction uriResourceFunction) { + this.uriResourceFunction = uriResourceFunction; + } + + @Override + public boolean isBound() { + return this.uriResourceFunction.getFunctionImport() != null; + } + + public EdmFunction getFunction() { + return this.uriResourceFunction.getFunction(); + } + + public List getParameters() { + return this.uriResourceFunction.getParameters(); + } + + @Override + public boolean isCollection() { + return getFunction().getReturnType().isCollection(); + } + + @Override + public EdmReturnType getReturnType() { + return getFunction().getReturnType(); + } + + @Override + public boolean hasReturnType() { + // Part3 {12.1} says must have return type + return true; + } +} \ No newline at end of file diff --git a/lib/server-core-ext/src/main/java/org/apache/olingo/server/core/requests/MediaRequest.java b/lib/server-core-ext/src/main/java/org/apache/olingo/server/core/requests/MediaRequest.java new file mode 100644 index 000000000..a4a333a1d --- /dev/null +++ b/lib/server-core-ext/src/main/java/org/apache/olingo/server/core/requests/MediaRequest.java @@ -0,0 +1,99 @@ +/* + * 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.requests; + +import java.io.InputStream; +import java.util.List; + +import org.apache.olingo.commons.api.edm.EdmEntitySet; +import org.apache.olingo.commons.api.edm.EdmEntityType; +import org.apache.olingo.commons.api.format.ContentType; +import org.apache.olingo.server.api.OData; +import org.apache.olingo.server.api.ODataApplicationException; +import org.apache.olingo.server.api.ODataResponse; +import org.apache.olingo.server.api.ODataTranslatedException; +import org.apache.olingo.server.api.ServiceMetadata; +import org.apache.olingo.server.api.uri.UriParameter; +import org.apache.olingo.server.api.uri.UriResourceEntitySet; +import org.apache.olingo.server.core.ContentNegotiatorException; +import org.apache.olingo.server.core.ServiceHandler; +import org.apache.olingo.server.core.ServiceRequest; +import org.apache.olingo.server.core.responses.NoContentResponse; +import org.apache.olingo.server.core.responses.StreamResponse; + +public class MediaRequest extends ServiceRequest { + private UriResourceEntitySet uriResourceEntitySet; + + public MediaRequest(OData odata, ServiceMetadata serviceMetadata) { + super(odata, serviceMetadata); + } + + @Override + public void execute(ServiceHandler handler, ODataResponse response) + throws ODataTranslatedException, ODataApplicationException { + if (!allowedMethod()) { + methodNotAllowed(); + } + // POST will not be here, because the media is created as part of media + // entity creation + if (isGET()) { + handler.readMediaStream(this, new StreamResponse(getServiceMetaData(), response)); + } else if (isPUT()) { + handler.upsertMediaStream(this, getETag(), getMediaStream(), new NoContentResponse( + getServiceMetaData(), response)); + } else if (isDELETE()) { + handler.upsertMediaStream(this, getETag(), null, new NoContentResponse(getServiceMetaData(), + response)); + } + } + + @Override + public ContentType getResponseContentType() throws ContentNegotiatorException { + // the request must specify the content type requested. + return getRequestContentType(); + } + + public EdmEntitySet getEntitySet() { + return this.uriResourceEntitySet.getEntitySet(); + } + + public EdmEntityType getEntityType() { + return this.uriResourceEntitySet.getEntitySet().getEntityType(); + } + + public void setUriResourceEntitySet(UriResourceEntitySet uriResourceEntitySet) { + this.uriResourceEntitySet = uriResourceEntitySet; + } + + public List getKeyPredicates() { + if (this.uriResourceEntitySet != null) { + return this.uriResourceEntitySet.getKeyPredicates(); + } + return null; + } + + private InputStream getMediaStream() { + return this.request.getBody(); + } + + @Override + public boolean allowedMethod() { + return isGET() || isPUT() || isDELETE(); + } +} diff --git a/lib/server-core-ext/src/main/java/org/apache/olingo/server/core/requests/MetadataRequest.java b/lib/server-core-ext/src/main/java/org/apache/olingo/server/core/requests/MetadataRequest.java new file mode 100644 index 000000000..e2c5c5476 --- /dev/null +++ b/lib/server-core-ext/src/main/java/org/apache/olingo/server/core/requests/MetadataRequest.java @@ -0,0 +1,61 @@ +/* + * 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.requests; + +import org.apache.olingo.commons.api.format.ContentType; +import org.apache.olingo.server.api.OData; +import org.apache.olingo.server.api.ODataApplicationException; +import org.apache.olingo.server.api.ODataResponse; +import org.apache.olingo.server.api.ODataTranslatedException; +import org.apache.olingo.server.api.ServiceMetadata; +import org.apache.olingo.server.api.serializer.RepresentationType; +import org.apache.olingo.server.api.uri.UriInfoMetadata; +import org.apache.olingo.server.core.ContentNegotiator; +import org.apache.olingo.server.core.ContentNegotiatorException; +import org.apache.olingo.server.core.ServiceHandler; +import org.apache.olingo.server.core.ServiceRequest; +import org.apache.olingo.server.core.responses.MetadataResponse; + +public class MetadataRequest extends ServiceRequest { + + public MetadataRequest(OData odata, ServiceMetadata serviceMetadata) { + super(odata, serviceMetadata); + } + + @Override + public ContentType getResponseContentType() throws ContentNegotiatorException { + return ContentNegotiator.doContentNegotiation(this.uriInfo.getFormatOption(), this.request, + this.customContentType, RepresentationType.METADATA); + } + + public UriInfoMetadata getUriInfoMetadata() { + return uriInfo.asUriInfoMetadata(); + } + + @Override + public void execute(ServiceHandler handler, ODataResponse response) + throws ODataTranslatedException, ODataApplicationException { + + if (!allowedMethod()) { + methodNotAllowed(); + } + + handler.readMetadata(this, MetadataResponse.getInstance(this, response)); + } +} diff --git a/lib/server-core-ext/src/main/java/org/apache/olingo/server/core/requests/OperationRequest.java b/lib/server-core-ext/src/main/java/org/apache/olingo/server/core/requests/OperationRequest.java new file mode 100644 index 000000000..1f1b1946b --- /dev/null +++ b/lib/server-core-ext/src/main/java/org/apache/olingo/server/core/requests/OperationRequest.java @@ -0,0 +1,118 @@ +/* + * 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.requests; + +import org.apache.olingo.commons.api.data.ContextURL; +import org.apache.olingo.commons.api.edm.EdmReturnType; +import org.apache.olingo.commons.api.edm.constants.EdmTypeKind; +import org.apache.olingo.commons.api.format.ContentType; +import org.apache.olingo.server.api.OData; +import org.apache.olingo.server.api.ServiceMetadata; +import org.apache.olingo.server.api.serializer.RepresentationType; +import org.apache.olingo.server.api.serializer.SerializerException; +import org.apache.olingo.server.api.uri.UriHelper; +import org.apache.olingo.server.core.ContentNegotiator; +import org.apache.olingo.server.core.ContentNegotiatorException; +import org.apache.olingo.server.core.ServiceRequest; + +public abstract class OperationRequest extends ServiceRequest { + + public OperationRequest(OData odata, ServiceMetadata serviceMetadata) { + super(odata, serviceMetadata); + } + + @Override + public ContentType getResponseContentType() throws ContentNegotiatorException { + if (!hasReturnType()) { + // this default content type + return ContentType.APPLICATION_OCTET_STREAM; + } + + if (isReturnTypePrimitive()) { + return ContentNegotiator.doContentNegotiation(uriInfo.getFormatOption(), this.request, + getCustomContentTypeSupport(), isCollection() ? RepresentationType.COLLECTION_PRIMITIVE + : RepresentationType.PRIMITIVE); + } else if (isReturnTypeComplex()) { + return ContentNegotiator.doContentNegotiation(uriInfo.getFormatOption(), this.request, + getCustomContentTypeSupport(), isCollection() ? RepresentationType.COLLECTION_COMPLEX + : RepresentationType.COMPLEX); + } else { + return ContentNegotiator.doContentNegotiation(uriInfo.getFormatOption(), this.request, + getCustomContentTypeSupport(), isCollection() ? RepresentationType.COLLECTION_ENTITY + : RepresentationType.ENTITY); + } + } + + public abstract boolean isBound(); + + public abstract boolean isCollection(); + + public abstract EdmReturnType getReturnType(); + + public abstract boolean hasReturnType(); + + public ContextURL getContextURL(OData odata) throws SerializerException { + if (!hasReturnType()) { + return null; + } + + final UriHelper helper = odata.createUriHelper(); + + if (isReturnTypePrimitive() || isReturnTypeComplex()) { + // Part 1 {10.14, 10.14} since the function return properties does not + // represent a Entity property + ContextURL.Builder builder = ContextURL.with().type(getReturnType().getType()); + if (isCollection()) { + builder.asCollection(); + } + return builder.build(); + } + + /* + // EdmTypeKind.ENTITY; + if (isBound()) { + // Bound means, we know the EnitySet of the return type. Part 1 {10.2, + // 10.3} + EdmEntitySet entitySet = this.uriResourceFunction.getFunctionImport().getReturnedEntitySet(); + ContextURL.Builder builder = DataRequest.buildEntitySetContextURL(helper, entitySet, + this.uriInfo, isCollection(), false); + return builder.build(); + } + */ + + // EdmTypeKind.ENTITY; Not Bound + // Here we do not know the EntitySet, then follow directions from + // Part-1{10.2. 10.3} to use + // {context-url}#{type-name} + ContextURL.Builder builder = ContextURL.with().type(getReturnType().getType()); + if (isCollection()) { + builder.asCollection(); + } + return builder.build(); + } + + public boolean isReturnTypePrimitive() { + return getReturnType().getType().getKind() == EdmTypeKind.PRIMITIVE; + } + + public boolean isReturnTypeComplex() { + return getReturnType().getType().getKind() == EdmTypeKind.COMPLEX; + } +} \ No newline at end of file diff --git a/lib/server-core-ext/src/main/java/org/apache/olingo/server/core/requests/ServiceDocumentRequest.java b/lib/server-core-ext/src/main/java/org/apache/olingo/server/core/requests/ServiceDocumentRequest.java new file mode 100644 index 000000000..f99aaf56e --- /dev/null +++ b/lib/server-core-ext/src/main/java/org/apache/olingo/server/core/requests/ServiceDocumentRequest.java @@ -0,0 +1,57 @@ +/* + * 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.requests; + +import org.apache.olingo.commons.api.format.ContentType; +import org.apache.olingo.server.api.OData; +import org.apache.olingo.server.api.ODataApplicationException; +import org.apache.olingo.server.api.ODataResponse; +import org.apache.olingo.server.api.ODataTranslatedException; +import org.apache.olingo.server.api.ServiceMetadata; +import org.apache.olingo.server.api.serializer.RepresentationType; +import org.apache.olingo.server.core.ContentNegotiator; +import org.apache.olingo.server.core.ContentNegotiatorException; +import org.apache.olingo.server.core.ServiceHandler; +import org.apache.olingo.server.core.ServiceRequest; +import org.apache.olingo.server.core.responses.ServiceDocumentResponse; + +public class ServiceDocumentRequest extends ServiceRequest { + + public ServiceDocumentRequest(OData odata, ServiceMetadata serviceMetadata) { + super(odata, serviceMetadata); + } + + @Override + public ContentType getResponseContentType() throws ContentNegotiatorException { + return ContentNegotiator.doContentNegotiation(getUriInfo().getFormatOption(), + getODataRequest(), getCustomContentTypeSupport(), RepresentationType.SERVICE); + } + + @Override + public void execute(ServiceHandler handler, ODataResponse response) + throws ODataTranslatedException, ODataApplicationException { + + if (!allowedMethod()) { + methodNotAllowed(); + } + handler.readServiceDocument(this, + ServiceDocumentResponse.getInstace(this, response, getResponseContentType())); + } +} diff --git a/lib/server-core-ext/src/main/java/org/apache/olingo/server/core/responses/CountResponse.java b/lib/server-core-ext/src/main/java/org/apache/olingo/server/core/responses/CountResponse.java new file mode 100644 index 000000000..f7cde332b --- /dev/null +++ b/lib/server-core-ext/src/main/java/org/apache/olingo/server/core/responses/CountResponse.java @@ -0,0 +1,60 @@ +/* + * 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.responses; + +import java.util.Map; + +import org.apache.olingo.commons.api.http.HttpContentType; +import org.apache.olingo.server.api.ODataApplicationException; +import org.apache.olingo.server.api.ODataResponse; +import org.apache.olingo.server.api.ODataTranslatedException; +import org.apache.olingo.server.api.ServiceMetadata; +import org.apache.olingo.server.api.serializer.FixedFormatSerializer; +import org.apache.olingo.server.api.serializer.SerializerException; +import org.apache.olingo.server.core.ServiceRequest; + +public class CountResponse extends ServiceResponse { + private final FixedFormatSerializer serializer; + + public static CountResponse getInstance(ServiceRequest request, ODataResponse response) { + FixedFormatSerializer serializer = request.getOdata().createFixedFormatSerializer(); + return new CountResponse(request.getServiceMetaData(), serializer, response, + request.getPreferences()); + } + + private CountResponse(ServiceMetadata metadata, FixedFormatSerializer serializer, + ODataResponse response, Map preferences) { + super(metadata, response, preferences); + this.serializer = serializer; + } + + public void writeCount(int count) throws SerializerException { + assert (!isClosed()); + + this.response.setContent(this.serializer.count(count)); + writeOK(HttpContentType.TEXT_PLAIN); + close(); + } + + @Override + public void accepts(ServiceResponseVisior visitor) throws ODataTranslatedException, + ODataApplicationException { + visitor.visit(this); + } +} diff --git a/lib/server-core-ext/src/main/java/org/apache/olingo/server/core/responses/EntityResponse.java b/lib/server-core-ext/src/main/java/org/apache/olingo/server/core/responses/EntityResponse.java new file mode 100644 index 000000000..fd29bbd03 --- /dev/null +++ b/lib/server-core-ext/src/main/java/org/apache/olingo/server/core/responses/EntityResponse.java @@ -0,0 +1,140 @@ +/* + * 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.responses; + +import java.util.Map; + +import org.apache.olingo.commons.api.data.ContextURL; +import org.apache.olingo.commons.api.data.Entity; +import org.apache.olingo.commons.api.edm.EdmEntityType; +import org.apache.olingo.commons.api.format.ContentType; +import org.apache.olingo.commons.api.http.HttpHeader; +import org.apache.olingo.commons.api.http.HttpStatusCode; +import org.apache.olingo.server.api.ODataApplicationException; +import org.apache.olingo.server.api.ODataResponse; +import org.apache.olingo.server.api.ODataTranslatedException; +import org.apache.olingo.server.api.ServiceMetadata; +import org.apache.olingo.server.api.serializer.EntitySerializerOptions; +import org.apache.olingo.server.api.serializer.ODataSerializer; +import org.apache.olingo.server.api.serializer.SerializerException; +import org.apache.olingo.server.core.ContentNegotiatorException; +import org.apache.olingo.server.core.ReturnRepresentation; +import org.apache.olingo.server.core.ServiceRequest; + +public class EntityResponse extends ServiceResponse { + private final ReturnRepresentation returnRepresentation; + private final ODataSerializer serializer; + private final EntitySerializerOptions options; + private final ContentType responseContentType; + + private EntityResponse(ServiceMetadata metadata, ODataResponse response, + ODataSerializer serializer, EntitySerializerOptions options, ContentType responseContentType, + Map preferences, ReturnRepresentation returnRepresentation) { + super(metadata, response, preferences); + this.serializer = serializer; + this.options = options; + this.responseContentType = responseContentType; + this.returnRepresentation = returnRepresentation; + } + + public static EntityResponse getInstance(ServiceRequest request, ContextURL contextURL, + boolean references, ODataResponse response, ReturnRepresentation returnRepresentation) + throws ContentNegotiatorException, SerializerException { + EntitySerializerOptions options = request.getSerializerOptions(EntitySerializerOptions.class, + contextURL, references); + return new EntityResponse(request.getServiceMetaData(), response, request.getSerializer(), + options, request.getResponseContentType(), request.getPreferences(), returnRepresentation); + } + + public static EntityResponse getInstance(ServiceRequest request, ContextURL contextURL, + boolean references, ODataResponse response) + throws ContentNegotiatorException, SerializerException { + EntitySerializerOptions options = request.getSerializerOptions(EntitySerializerOptions.class, + contextURL, references); + return new EntityResponse(request.getServiceMetaData(), response, request.getSerializer(), + options, request.getResponseContentType(), request.getPreferences(), null); + } + + // write single entity + public void writeReadEntity(EdmEntityType entityType, Entity entity) throws SerializerException { + + assert (!isClosed()); + + if (entity == null) { + writeNotFound(true); + return; + } + + // write the entity to response + this.response.setContent(this.serializer.entity(this.metadata, entityType, entity, this.options)); + writeOK(this.responseContentType.toContentTypeString()); + close(); + } + + public void writeCreatedEntity(EdmEntityType entityType, Entity entity, String locationHeader) + throws SerializerException { + // upsert/insert must created a entity, otherwise should have throw an + // exception + assert (entity != null); + + // Note that if media written just like Stream, but on entity URL + + // 8.2.8.7 + if (this.returnRepresentation == ReturnRepresentation.MINIMAL) { + writeNoContent(false); + writeHeader(HttpHeader.LOCATION, locationHeader); + writeHeader("Preference-Applied", "return=minimal"); //$NON-NLS-1$ //$NON-NLS-2$ + // 8.3.3 + writeHeader("OData-EntityId", entity.getId().toASCIIString()); //$NON-NLS-1$ + close(); + return; + } + + // return the content of the created entity + this.response.setContent(this.serializer.entity(this.metadata, entityType, entity, this.options)); + writeCreated(false); + writeHeader(HttpHeader.LOCATION, locationHeader); + writeHeader("Preference-Applied", "return=representation"); //$NON-NLS-1$ //$NON-NLS-2$ + writeHeader(HttpHeader.CONTENT_TYPE, this.responseContentType.toContentTypeString()); + close(); + } + + public void writeUpdatedEntity() { + // spec says just success response; so either 200 or 204. 200 typically has + // payload + writeNoContent(true); + } + + public void writeDeletedEntityOrReference() { + writeNoContent(true); + } + + @Override + public void accepts(ServiceResponseVisior visitor) throws ODataTranslatedException, + ODataApplicationException { + visitor.visit(this); + } + + public void writeCreated(boolean closeResponse) { + this.response.setStatusCode(HttpStatusCode.CREATED.getStatusCode()); + if (closeResponse) { + close(); + } + } +} diff --git a/lib/server-core-ext/src/main/java/org/apache/olingo/server/core/responses/EntitySetResponse.java b/lib/server-core-ext/src/main/java/org/apache/olingo/server/core/responses/EntitySetResponse.java new file mode 100644 index 000000000..40276d210 --- /dev/null +++ b/lib/server-core-ext/src/main/java/org/apache/olingo/server/core/responses/EntitySetResponse.java @@ -0,0 +1,82 @@ +/* + * 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.responses; + +import java.util.Map; + +import org.apache.olingo.commons.api.data.ContextURL; +import org.apache.olingo.commons.api.data.EntitySet; +import org.apache.olingo.commons.api.edm.EdmEntityType; +import org.apache.olingo.commons.api.format.ContentType; +import org.apache.olingo.server.api.ODataApplicationException; +import org.apache.olingo.server.api.ODataResponse; +import org.apache.olingo.server.api.ODataTranslatedException; +import org.apache.olingo.server.api.ServiceMetadata; +import org.apache.olingo.server.api.serializer.EntityCollectionSerializerOptions; +import org.apache.olingo.server.api.serializer.ODataSerializer; +import org.apache.olingo.server.api.serializer.SerializerException; +import org.apache.olingo.server.core.ContentNegotiatorException; +import org.apache.olingo.server.core.ServiceRequest; + +public class EntitySetResponse extends ServiceResponse { + private final ODataSerializer serializer; + private final EntityCollectionSerializerOptions options; + private final ContentType responseContentType; + + private EntitySetResponse(ServiceMetadata metadata, ODataResponse response, ODataSerializer serializer, + EntityCollectionSerializerOptions options, + ContentType responseContentType, Map preferences) { + super(metadata, response, preferences); + this.serializer = serializer; + this.options = options; + this.responseContentType = responseContentType; + } + + public static EntitySetResponse getInstance(ServiceRequest request, ContextURL contextURL, + boolean referencesOnly, ODataResponse response) throws ContentNegotiatorException, SerializerException { + EntityCollectionSerializerOptions options = request.getSerializerOptions( + EntityCollectionSerializerOptions.class, contextURL, referencesOnly); + return new EntitySetResponse(request.getServiceMetaData(),response, request.getSerializer(), options, + request.getResponseContentType(), request.getPreferences()); + } + + // write collection of entities + // TODO: server paging needs to be implemented. + public void writeReadEntitySet(EdmEntityType entityType, EntitySet entitySet) + throws SerializerException { + + assert (!isClosed()); + + if (entitySet == null) { + writeNotFound(true); + return; + } + + // write the whole collection to response + this.response.setContent(this.serializer.entityCollection(metadata, entityType, entitySet, this.options)); + writeOK(this.responseContentType.toContentTypeString()); + close(); + } + + @Override + public void accepts(ServiceResponseVisior visitor) throws ODataTranslatedException, + ODataApplicationException { + visitor.visit(this); + } +} diff --git a/lib/server-core-ext/src/main/java/org/apache/olingo/server/core/responses/MetadataResponse.java b/lib/server-core-ext/src/main/java/org/apache/olingo/server/core/responses/MetadataResponse.java new file mode 100644 index 000000000..055c0b0be --- /dev/null +++ b/lib/server-core-ext/src/main/java/org/apache/olingo/server/core/responses/MetadataResponse.java @@ -0,0 +1,62 @@ +/* + * 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.responses; + +import java.util.Map; + +import org.apache.olingo.commons.api.format.ContentType; +import org.apache.olingo.server.api.ODataApplicationException; +import org.apache.olingo.server.api.ODataResponse; +import org.apache.olingo.server.api.ODataTranslatedException; +import org.apache.olingo.server.api.ServiceMetadata; +import org.apache.olingo.server.api.serializer.ODataSerializer; +import org.apache.olingo.server.api.serializer.SerializerException; +import org.apache.olingo.server.core.ContentNegotiatorException; +import org.apache.olingo.server.core.ServiceRequest; + +public class MetadataResponse extends ServiceResponse { + private final ODataSerializer serializer; + private final ContentType responseContentType; + + public static MetadataResponse getInstance(ServiceRequest request, + ODataResponse response) throws ContentNegotiatorException, SerializerException { + return new MetadataResponse(request.getServiceMetaData(), response, request.getSerializer(), + request.getResponseContentType(), request.getPreferences()); + } + + private MetadataResponse(ServiceMetadata metadata, ODataResponse response, ODataSerializer serializer, + ContentType responseContentType, Map preferences) { + super(metadata, response, preferences); + this.serializer = serializer; + this.responseContentType = responseContentType; + } + + public void writeMetadata()throws ODataTranslatedException { + assert (!isClosed()); + this.response.setContent(this.serializer.metadataDocument(this.metadata)); + writeOK(this.responseContentType.toContentTypeString()); + close(); + } + + @Override + public void accepts(ServiceResponseVisior visitor) throws ODataTranslatedException, + ODataApplicationException { + visitor.visit(this); + } +} diff --git a/lib/server-core-ext/src/main/java/org/apache/olingo/server/core/responses/NoContentResponse.java b/lib/server-core-ext/src/main/java/org/apache/olingo/server/core/responses/NoContentResponse.java new file mode 100644 index 000000000..eb163658b --- /dev/null +++ b/lib/server-core-ext/src/main/java/org/apache/olingo/server/core/responses/NoContentResponse.java @@ -0,0 +1,100 @@ +/* + * 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.responses; + +import java.util.Collections; + +import org.apache.olingo.commons.api.http.HttpStatusCode; +import org.apache.olingo.server.api.ODataApplicationException; +import org.apache.olingo.server.api.ODataResponse; +import org.apache.olingo.server.api.ODataTranslatedException; +import org.apache.olingo.server.api.ServiceMetadata; + +public class NoContentResponse extends ServiceResponse { + + public NoContentResponse(ServiceMetadata metadata, ODataResponse response) { + super(metadata, response, Collections.EMPTY_MAP); + } + + // 200 + public void writeOK() { + this.response.setStatusCode(HttpStatusCode.OK.getStatusCode()); + close(); + } + + // 201 + public void writeCreated() { + this.response.setStatusCode(HttpStatusCode.CREATED.getStatusCode()); + close(); + } + + // 202 + public void writeAccepted() { + this.response.setStatusCode(HttpStatusCode.ACCEPTED.getStatusCode()); + close(); + } + + // 204 + public void writeNoContent() { + writeNoContent(true); + } + + // 304 + public void writeNotModified() { + this.response.setStatusCode(HttpStatusCode.NOT_MODIFIED.getStatusCode()); + close(); + } + + // error response codes + + // 404 + public void writeNotFound() { + writeNotFound(true); + } + + // 501 + public void writeNotImplemented() { + this.response.setStatusCode(HttpStatusCode.NOT_IMPLEMENTED.getStatusCode()); + close(); + } + + // 405 + public void writeMethodNotAllowed() { + this.response.setStatusCode(HttpStatusCode.METHOD_NOT_ALLOWED.getStatusCode()); + close(); + } + + // 410 + public void writeGone() { + this.response.setStatusCode(HttpStatusCode.GONE.getStatusCode()); + close(); + } + + // 412 + public void writePreConditionFailed() { + this.response.setStatusCode(HttpStatusCode.PRECONDITION_FAILED.getStatusCode()); + close(); + } + + @Override + public void accepts(ServiceResponseVisior visitor) throws ODataTranslatedException, + ODataApplicationException { + visitor.visit(this); + } +} diff --git a/lib/server-core-ext/src/main/java/org/apache/olingo/server/core/responses/PrimitiveValueResponse.java b/lib/server-core-ext/src/main/java/org/apache/olingo/server/core/responses/PrimitiveValueResponse.java new file mode 100644 index 000000000..005bfca9f --- /dev/null +++ b/lib/server-core-ext/src/main/java/org/apache/olingo/server/core/responses/PrimitiveValueResponse.java @@ -0,0 +1,105 @@ +/* + * 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.responses; + +import java.util.Map; + +import org.apache.olingo.commons.api.edm.EdmPrimitiveType; +import org.apache.olingo.commons.api.edm.EdmProperty; +import org.apache.olingo.commons.api.edm.EdmReturnType; +import org.apache.olingo.commons.api.http.HttpContentType; +import org.apache.olingo.server.api.ODataApplicationException; +import org.apache.olingo.server.api.ODataResponse; +import org.apache.olingo.server.api.ODataTranslatedException; +import org.apache.olingo.server.api.ServiceMetadata; +import org.apache.olingo.server.api.serializer.FixedFormatSerializer; +import org.apache.olingo.server.api.serializer.PrimitiveValueSerializerOptions; +import org.apache.olingo.server.api.serializer.SerializerException; +import org.apache.olingo.server.core.ServiceRequest; + +public class PrimitiveValueResponse extends ServiceResponse { + private final boolean returnCollection; + private EdmProperty type; + private EdmReturnType returnType; + private final FixedFormatSerializer serializer; + + public static PrimitiveValueResponse getInstance(ServiceRequest request, ODataResponse response, + boolean collection, EdmProperty type) { + FixedFormatSerializer serializer = request.getOdata().createFixedFormatSerializer(); + return new PrimitiveValueResponse(request.getServiceMetaData(), serializer, response, + collection, type, request.getPreferences()); + } + + public static PrimitiveValueResponse getInstance(ServiceRequest request, ODataResponse response, + boolean collection, EdmReturnType type) { + FixedFormatSerializer serializer = request.getOdata().createFixedFormatSerializer(); + return new PrimitiveValueResponse(request.getServiceMetaData(), serializer, response, + collection, type, request.getPreferences()); + } + + private PrimitiveValueResponse(ServiceMetadata metadata, FixedFormatSerializer serializer, + ODataResponse response, boolean collection, EdmProperty type, Map preferences) { + super(metadata, response, preferences); + this.returnCollection = collection; + this.type = type; + this.serializer = serializer; + } + + private PrimitiveValueResponse(ServiceMetadata metadata, FixedFormatSerializer serializer, + ODataResponse response, boolean collection, EdmReturnType type, + Map preferences) { + super(metadata, response, preferences); + this.returnCollection = collection; + this.returnType = type; + this.serializer = serializer; + } + + public void write(Object value) throws SerializerException { + if (value == null) { + writeNoContent(true); + return; + } + + if (this.type != null) { + PrimitiveValueSerializerOptions options = PrimitiveValueSerializerOptions.with() + .facetsFrom(this.type).build(); + + this.response.setContent(this.serializer.primitiveValue((EdmPrimitiveType) this.type.getType(), + value, options)); + } else { + PrimitiveValueSerializerOptions options = PrimitiveValueSerializerOptions.with() + .nullable(this.returnType.isNullable()).maxLength(this.returnType.getMaxLength()) + .precision(this.returnType.getPrecision()).scale(this.returnType.getScale()).build(); + this.response.setContent(this.serializer.primitiveValue( + (EdmPrimitiveType) this.returnType.getType(), value, options)); + } + + writeOK(HttpContentType.TEXT_PLAIN); + } + + public boolean isReturnCollection() { + return returnCollection; + } + + @Override + public void accepts(ServiceResponseVisior visitor) throws ODataTranslatedException, + ODataApplicationException { + visitor.visit(this); + } +} diff --git a/lib/server-core-ext/src/main/java/org/apache/olingo/server/core/responses/PropertyResponse.java b/lib/server-core-ext/src/main/java/org/apache/olingo/server/core/responses/PropertyResponse.java new file mode 100644 index 000000000..e6b951d39 --- /dev/null +++ b/lib/server-core-ext/src/main/java/org/apache/olingo/server/core/responses/PropertyResponse.java @@ -0,0 +1,144 @@ +/* + * 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.responses; + +import java.util.Map; + +import org.apache.olingo.commons.api.data.ContextURL; +import org.apache.olingo.commons.api.data.Property; +import org.apache.olingo.commons.api.edm.EdmComplexType; +import org.apache.olingo.commons.api.edm.EdmPrimitiveType; +import org.apache.olingo.commons.api.edm.EdmType; +import org.apache.olingo.commons.api.edm.constants.EdmTypeKind; +import org.apache.olingo.commons.api.format.ContentType; +import org.apache.olingo.server.api.ODataApplicationException; +import org.apache.olingo.server.api.ODataResponse; +import org.apache.olingo.server.api.ODataTranslatedException; +import org.apache.olingo.server.api.ServiceMetadata; +import org.apache.olingo.server.api.serializer.ComplexSerializerOptions; +import org.apache.olingo.server.api.serializer.ODataSerializer; +import org.apache.olingo.server.api.serializer.PrimitiveSerializerOptions; +import org.apache.olingo.server.api.serializer.SerializerException; +import org.apache.olingo.server.core.ContentNegotiatorException; +import org.apache.olingo.server.core.ServiceRequest; + +public class PropertyResponse extends ServiceResponse { + private PrimitiveSerializerOptions primitiveOptions; + private ComplexSerializerOptions complexOptions; + private final ContentType responseContentType; + private final ODataSerializer serializer; + private final boolean collection; + + public static PropertyResponse getInstance(ServiceRequest request, ODataResponse response, + EdmType edmType, ContextURL contextURL, boolean collection) throws ContentNegotiatorException, + SerializerException { + if (edmType.getKind() == EdmTypeKind.PRIMITIVE) { + PrimitiveSerializerOptions options = request.getSerializerOptions( + PrimitiveSerializerOptions.class, contextURL, false); + ContentType type = request.getResponseContentType(); + return new PropertyResponse(request.getServiceMetaData(), request.getSerializer(), response, + options, type, collection, request.getPreferences()); + } + ComplexSerializerOptions options = request.getSerializerOptions(ComplexSerializerOptions.class, + contextURL, false); + ContentType type = request.getResponseContentType(); + return new PropertyResponse(request.getServiceMetaData(), request.getSerializer(), response, + options, type, collection, request.getPreferences()); + } + + private PropertyResponse(ServiceMetadata metadata, ODataSerializer serializer, + ODataResponse response, PrimitiveSerializerOptions options, ContentType contentType, + boolean collection, Map preferences) { + super(metadata, response, preferences); + this.serializer = serializer; + this.primitiveOptions = options; + this.responseContentType = contentType; + this.collection = collection; + } + + private PropertyResponse(ServiceMetadata metadata, ODataSerializer serializer, ODataResponse response, + ComplexSerializerOptions options, ContentType contentType, boolean collection, + Map preferences) { + super(metadata, response, preferences); + this.serializer = serializer; + this.complexOptions = options; + this.responseContentType = contentType; + this.collection = collection; + } + + public void writeProperty(EdmType edmType, Property property) throws SerializerException { + assert (!isClosed()); + + if (property == null) { + writeNotFound(true); + return; + } + + if (property.getValue() == null) { + writeNoContent(true); + return; + } + + if (edmType.getKind() == EdmTypeKind.PRIMITIVE) { + writePrimitiveProperty((EdmPrimitiveType) edmType, property); + } else { + writeComplexProperty((EdmComplexType) edmType, property); + } + } + + private void writeComplexProperty(EdmComplexType type, Property property) + throws SerializerException { + if (this.collection) { + this.response.setContent(this.serializer.complexCollection(this.metadata, type, property, + this.complexOptions)); + } else { + this.response.setContent(this.serializer.complex(this.metadata, type, property, + this.complexOptions)); + } + writeOK(this.responseContentType.toContentTypeString()); + close(); + } + + private void writePrimitiveProperty(EdmPrimitiveType type, Property property) + throws SerializerException { + if(this.collection) { + this.response.setContent(this.serializer.primitiveCollection(type, property, this.primitiveOptions)); + } else { + this.response.setContent(this.serializer.primitive(type, property, this.primitiveOptions)); + } + writeOK(this.responseContentType.toContentTypeString()); + close(); + } + + @Override + public void accepts(ServiceResponseVisior visitor) throws ODataTranslatedException, + ODataApplicationException { + visitor.visit(this); + } + + public void writePropertyUpdated() { + // spec says just success response; so either 200 or 204. 200 typically has + // payload + writeNoContent(true); + } + + public void writePropertyDeleted() { + writeNoContent(true); + } +} diff --git a/lib/server-core-ext/src/main/java/org/apache/olingo/server/core/responses/ServiceDocumentResponse.java b/lib/server-core-ext/src/main/java/org/apache/olingo/server/core/responses/ServiceDocumentResponse.java new file mode 100644 index 000000000..86c420b66 --- /dev/null +++ b/lib/server-core-ext/src/main/java/org/apache/olingo/server/core/responses/ServiceDocumentResponse.java @@ -0,0 +1,63 @@ +/* + * 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.responses; + +import java.util.Map; + +import org.apache.olingo.commons.api.format.ContentType; +import org.apache.olingo.server.api.ODataApplicationException; +import org.apache.olingo.server.api.ODataResponse; +import org.apache.olingo.server.api.ODataTranslatedException; +import org.apache.olingo.server.api.ServiceMetadata; +import org.apache.olingo.server.api.serializer.ODataSerializer; +import org.apache.olingo.server.api.serializer.SerializerException; +import org.apache.olingo.server.core.ContentNegotiatorException; +import org.apache.olingo.server.core.ServiceRequest; + +public class ServiceDocumentResponse extends ServiceResponse { + private final ODataSerializer serializer; + private final ContentType responseContentType; + + public static ServiceDocumentResponse getInstace(ServiceRequest request, ODataResponse respose, + ContentType responseContentType) throws ContentNegotiatorException, SerializerException { + return new ServiceDocumentResponse(request.getServiceMetaData(), respose, + request.getSerializer(), responseContentType, request.getPreferences()); + } + + private ServiceDocumentResponse(ServiceMetadata metadata, ODataResponse respose, + ODataSerializer serializer, ContentType responseContentType, Map preferences) { + super(metadata, respose, preferences); + this.serializer = serializer; + this.responseContentType = responseContentType; + } + + public void writeServiceDocument(String serviceRoot) + throws ODataTranslatedException { + assert (!isClosed()); + this.response.setContent(this.serializer.serviceDocument(this.metadata.getEdm(), serviceRoot)); + writeOK(this.responseContentType.toContentTypeString()); + close(); + } + + @Override + public void accepts(ServiceResponseVisior visitor) throws ODataTranslatedException, + ODataApplicationException { + visitor.visit(this); + } +} diff --git a/lib/server-core-ext/src/main/java/org/apache/olingo/server/core/responses/ServiceResponse.java b/lib/server-core-ext/src/main/java/org/apache/olingo/server/core/responses/ServiceResponse.java new file mode 100644 index 000000000..a3065515d --- /dev/null +++ b/lib/server-core-ext/src/main/java/org/apache/olingo/server/core/responses/ServiceResponse.java @@ -0,0 +1,119 @@ +/* + * 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.responses; + +import java.util.Map; + +import org.apache.olingo.commons.api.http.HttpHeader; +import org.apache.olingo.commons.api.http.HttpStatusCode; +import org.apache.olingo.server.api.ODataApplicationException; +import org.apache.olingo.server.api.ODataResponse; +import org.apache.olingo.server.api.ODataTranslatedException; +import org.apache.olingo.server.api.ServiceMetadata; + +public abstract class ServiceResponse { + protected ServiceMetadata metadata; + protected ODataResponse response; + protected Map preferences; + private boolean closed; + private boolean strictApplyPreferences = true; + + public ServiceResponse(ServiceMetadata metadata, ODataResponse response, + Map preferences) { + this.metadata = metadata; + this.response = response; + this.preferences = preferences; + } + + public ODataResponse getODataResponse() { + return this.response; + } + + protected boolean isClosed() { + return this.closed; + } + + protected void close() { + if (!this.closed) { + if (this.strictApplyPreferences) { + if (!preferences.isEmpty()) { + assert(this.response.getHeaders().get("Preference-Applied") != null); + } + } + this.closed = true; + } + } + + public void writeNoContent(boolean closeResponse) { + this.response.setStatusCode(HttpStatusCode.NO_CONTENT.getStatusCode()); + if (closeResponse) { + close(); + } + } + + public void writeNotFound(boolean closeResponse) { + response.setStatusCode(HttpStatusCode.NOT_FOUND.getStatusCode()); + if (closeResponse) { + close(); + } + } + + public void writeServerError(boolean closeResponse) { + response.setStatusCode(HttpStatusCode.INTERNAL_SERVER_ERROR.getStatusCode()); + if (closeResponse) { + close(); + } + } + + public void writeBadRequest(boolean closeResponse) { + response.setStatusCode(HttpStatusCode.BAD_REQUEST.getStatusCode()); + if (closeResponse) { + close(); + } + } + + public void writeOK(String contentType) { + this.response.setStatusCode(HttpStatusCode.OK.getStatusCode()); + this.response.setHeader(HttpHeader.CONTENT_TYPE, contentType); + } + + public void writeHeader(String key, String value) { + if ("Preference-Applied".equals(key)) { + String previous = this.response.getHeaders().get(key); + if (previous != null) { + value = previous+";"+value; + } + this.response.setHeader(key, value); + } else { + this.response.setHeader(key, value); + } + } + + /** + * When true; the "Preference-Applied" header is strictly checked. + * @param flag + */ + public void setStrictlyApplyPreferences(boolean flag) { + this.strictApplyPreferences = flag; + } + + public abstract void accepts(ServiceResponseVisior visitor) throws ODataTranslatedException, + ODataApplicationException; +} diff --git a/lib/server-core-ext/src/main/java/org/apache/olingo/server/core/responses/ServiceResponseVisior.java b/lib/server-core-ext/src/main/java/org/apache/olingo/server/core/responses/ServiceResponseVisior.java new file mode 100644 index 000000000..4f11cb8b6 --- /dev/null +++ b/lib/server-core-ext/src/main/java/org/apache/olingo/server/core/responses/ServiceResponseVisior.java @@ -0,0 +1,71 @@ +/* + * 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.responses; + +import org.apache.olingo.server.api.ODataApplicationException; +import org.apache.olingo.server.api.ODataTranslatedException; + +@SuppressWarnings("unused") +public class ServiceResponseVisior { + + public void visit(CountResponse response) throws ODataTranslatedException, + ODataApplicationException { + response.writeServerError(true); + } + + public void visit(EntityResponse response) throws ODataTranslatedException, + ODataApplicationException { + response.writeServerError(true); + } + + public void visit(MetadataResponse response) throws ODataTranslatedException, + ODataApplicationException { + response.writeServerError(true); + } + + public void visit(NoContentResponse response) throws ODataTranslatedException, + ODataApplicationException { + response.writeServerError(true); + } + + public void visit(PrimitiveValueResponse response) throws ODataTranslatedException, + ODataApplicationException { + response.writeServerError(true); + } + + public void visit(PropertyResponse response) throws ODataTranslatedException, + ODataApplicationException { + response.writeServerError(true); + } + + public void visit(ServiceDocumentResponse response) throws ODataTranslatedException, + ODataApplicationException { + response.writeServerError(true); + } + + public void visit(StreamResponse response) throws ODataTranslatedException, + ODataApplicationException { + response.writeServerError(true); + } + + public void visit(EntitySetResponse response) throws ODataTranslatedException, + ODataApplicationException { + response.writeServerError(true); + } +} diff --git a/lib/server-core-ext/src/main/java/org/apache/olingo/server/core/responses/StreamResponse.java b/lib/server-core-ext/src/main/java/org/apache/olingo/server/core/responses/StreamResponse.java new file mode 100644 index 000000000..ec7db032a --- /dev/null +++ b/lib/server-core-ext/src/main/java/org/apache/olingo/server/core/responses/StreamResponse.java @@ -0,0 +1,54 @@ +/* + * 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.responses; + +import java.io.ByteArrayInputStream; +import java.io.InputStream; +import java.util.Collections; + +import org.apache.olingo.commons.api.format.ContentType; +import org.apache.olingo.server.api.ODataApplicationException; +import org.apache.olingo.server.api.ODataResponse; +import org.apache.olingo.server.api.ODataTranslatedException; +import org.apache.olingo.server.api.ServiceMetadata; + +public class StreamResponse extends ServiceResponse { + + public StreamResponse(ServiceMetadata metadata, ODataResponse response) { + super(metadata, response, Collections.EMPTY_MAP); + } + + public void writeStreamResponse(InputStream streamContent, ContentType contentType) { + this.response.setContent(streamContent); + writeOK(contentType.toContentTypeString()); + close(); + } + + public void writeBinaryResponse(byte[] streamContent, ContentType contentType) { + this.response.setContent(new ByteArrayInputStream(streamContent)); + writeOK(contentType.toContentTypeString()); + close(); + } + + @Override + public void accepts(ServiceResponseVisior visitor) throws ODataTranslatedException, + ODataApplicationException { + visitor.visit(this); + } +} diff --git a/lib/server-core-ext/src/test/java/org/apache/olingo/server/core/MetadataParserTest.java b/lib/server-core-ext/src/test/java/org/apache/olingo/server/core/MetadataParserTest.java new file mode 100644 index 000000000..9749a6cc5 --- /dev/null +++ b/lib/server-core-ext/src/test/java/org/apache/olingo/server/core/MetadataParserTest.java @@ -0,0 +1,185 @@ +/* + * 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; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + +import java.io.FileReader; +import java.util.List; + +import org.apache.olingo.commons.api.ODataException; +import org.apache.olingo.commons.api.edm.FullQualifiedName; +import org.apache.olingo.commons.api.edm.provider.Action; +import org.apache.olingo.commons.api.edm.provider.ActionImport; +import org.apache.olingo.commons.api.edm.provider.ComplexType; +import org.apache.olingo.commons.api.edm.provider.EdmProvider; +import org.apache.olingo.commons.api.edm.provider.EntitySet; +import org.apache.olingo.commons.api.edm.provider.EntityType; +import org.apache.olingo.commons.api.edm.provider.EnumType; +import org.apache.olingo.commons.api.edm.provider.Function; +import org.apache.olingo.commons.api.edm.provider.FunctionImport; +import org.apache.olingo.commons.api.edm.provider.NavigationPropertyBinding; +import org.apache.olingo.commons.api.edm.provider.Parameter; +import org.apache.olingo.commons.api.edm.provider.Property; +import org.apache.olingo.commons.api.edm.provider.Singleton; +import org.junit.Before; +import org.junit.Test; + +public class MetadataParserTest { + final String NS = "Microsoft.OData.SampleService.Models.TripPin"; + final FullQualifiedName NSF = new FullQualifiedName(NS); + + EdmProvider provider = null; + + @Before + public void setUp() throws Exception { + MetadataParser parser = new MetadataParser(); + provider = parser.buildEdmProvider(new FileReader("src/test/resources/trippin.xml")); + } + + @Test + public void testAction() throws ODataException { + // test action + List actions = provider.getActions(new FullQualifiedName(NS, "ResetDataSource")); + assertNotNull(actions); + assertEquals(1, actions.size()); + } + + @Test + public void testFunction() throws ODataException { + // test function + List functions = provider + .getFunctions(new FullQualifiedName(NS, "GetFavoriteAirline")); + assertNotNull(functions); + assertEquals(1, functions.size()); + assertEquals("GetFavoriteAirline", functions.get(0).getName()); + assertTrue(functions.get(0).isBound()); + assertTrue(functions.get(0).isComposable()); + assertEquals( + "person/Trips/PlanItems/Microsoft.OData.SampleService.Models.TripPin.Flight/Airline", + functions.get(0).getEntitySetPath()); + + List parameters = functions.get(0).getParameters(); + assertNotNull(parameters); + assertEquals(1, parameters.size()); + assertEquals("person", parameters.get(0).getName()); + assertEquals("Microsoft.OData.SampleService.Models.TripPin.Person",parameters.get(0).getType()); + assertFalse(parameters.get(0).isNullable()); + + assertNotNull(functions.get(0).getReturnType()); + assertEquals("Microsoft.OData.SampleService.Models.TripPin.Airline", + functions.get(0).getReturnType().getType()); + assertFalse(functions.get(0).getReturnType().isNullable()); + } + + @Test + public void testEnumType() throws ODataException { + // test enum type + EnumType enumType = provider.getEnumType(new FullQualifiedName(NS, "PersonGender")); + assertNotNull(enumType); + assertEquals("Male", enumType.getMembers().get(0).getName()); + assertEquals("Female", enumType.getMembers().get(1).getName()); + assertEquals("Unknown", enumType.getMembers().get(2).getName()); + assertEquals("0", enumType.getMembers().get(0).getValue()); + assertEquals("1", enumType.getMembers().get(1).getValue()); + assertEquals("2", enumType.getMembers().get(2).getValue()); + } + + @Test + public void testEntityType() throws ODataException { + // test Entity Type + EntityType et = provider.getEntityType(new FullQualifiedName(NS, "Photo")); + assertNotNull(et); + assertNotNull(et.getKey()); + assertEquals("Id", et.getKey().get(0).getName()); + assertTrue(et.hasStream()); + assertEquals("Id", et.getProperties().get(0).getName()); + assertEquals("Edm.Int64", et.getProperties().get(0).getType()); + assertEquals("Name", et.getProperties().get(1).getName()); + assertEquals("Edm.String", et.getProperties().get(1).getType()); + } + + @Test + public void testComplexType() throws ODataException { + // Test Complex Type + ComplexType ct = provider.getComplexType(new FullQualifiedName(NS, "City")); + assertNotNull(ct); + assertEquals(3, ct.getProperties().size()); + Property p = ct.getProperties().get(0); + assertEquals("CountryRegion", p.getName()); + assertEquals("Edm.String", p.getType()); + assertEquals(false, p.isNullable()); + + ct = provider.getComplexType(new FullQualifiedName(NS, "Location")); + assertNotNull(ct); + + ct = provider.getComplexType(new FullQualifiedName(NS, "EventLocation")); + assertNotNull(ct); + } + + @Test + public void testEntitySet() throws Exception { + EntitySet es = provider.getEntitySet(NSF, "People"); + assertNotNull(es); + assertEquals("Microsoft.OData.SampleService.Models.TripPin.Person",es.getType()); + + List bindings = es.getNavigationPropertyBindings(); + assertNotNull(bindings); + assertEquals(6, bindings.size()); + assertEquals("Microsoft.OData.SampleService.Models.TripPin.Flight/From", bindings.get(2) + .getPath()); + assertEquals("Airports", bindings.get(2).getTarget()); + } + + @Test + public void testFunctionImport() throws Exception { + FunctionImport fi = provider.getFunctionImport(NSF, "GetNearestAirport"); + assertNotNull(fi); + assertEquals("Microsoft.OData.SampleService.Models.TripPin.GetNearestAirport", fi.getFunction()); + assertEquals("Airports", fi.getEntitySet()); + assertTrue(fi.isIncludeInServiceDocument()); + } + + @Test + public void testActionImport() throws Exception { + ActionImport ai = provider.getActionImport(NSF, "ResetDataSource"); + assertNotNull(ai); + assertEquals("Microsoft.OData.SampleService.Models.TripPin.ResetDataSource", ai.getAction()); + assertNull(ai.getEntitySet()); + } + + @Test + public void testSingleton() throws Exception { + Singleton single = this.provider.getSingleton(NSF, "Me"); + assertNotNull(single); + + assertEquals("Microsoft.OData.SampleService.Models.TripPin.Person",single.getType()); + + List bindings = single.getNavigationPropertyBindings(); + assertNotNull(bindings); + assertEquals(6, bindings.size()); + assertEquals("Microsoft.OData.SampleService.Models.TripPin.Flight/From", bindings.get(2).getPath()); + assertEquals("Airports", bindings.get(2).getTarget()); + + } +} diff --git a/lib/server-core-ext/src/test/java/org/apache/olingo/server/core/ServiceDispatcherTest.java b/lib/server-core-ext/src/test/java/org/apache/olingo/server/core/ServiceDispatcherTest.java new file mode 100644 index 000000000..16caa1ae4 --- /dev/null +++ b/lib/server-core-ext/src/test/java/org/apache/olingo/server/core/ServiceDispatcherTest.java @@ -0,0 +1,417 @@ +/* + * 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; + +import static org.junit.Assert.assertEquals; + +import java.io.FileReader; +import java.io.IOException; +import java.net.URI; +import java.util.Collections; +import java.util.List; + +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.apache.olingo.commons.api.edm.provider.EdmProvider; +import org.apache.olingo.commons.api.http.HttpMethod; +import org.apache.olingo.server.api.OData; +import org.apache.olingo.server.api.ODataHttpHandler; +import org.apache.olingo.server.api.ServiceMetadata; +import org.apache.olingo.server.core.requests.ActionRequest; +import org.apache.olingo.server.core.requests.DataRequest; +import org.apache.olingo.server.core.requests.FunctionRequest; +import org.apache.olingo.server.core.requests.MediaRequest; +import org.apache.olingo.server.core.requests.MetadataRequest; +import org.apache.olingo.server.core.responses.CountResponse; +import org.apache.olingo.server.core.responses.EntityResponse; +import org.apache.olingo.server.core.responses.EntitySetResponse; +import org.apache.olingo.server.core.responses.MetadataResponse; +import org.apache.olingo.server.core.responses.NoContentResponse; +import org.apache.olingo.server.core.responses.PrimitiveValueResponse; +import org.apache.olingo.server.core.responses.PropertyResponse; +import org.apache.olingo.server.core.responses.StreamResponse; +import org.apache.olingo.server.example.TripPinServiceTest; +import org.eclipse.jetty.client.HttpClient; +import org.eclipse.jetty.server.Connector; +import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.server.ServerConnector; +import org.eclipse.jetty.servlet.ServletContextHandler; +import org.eclipse.jetty.servlet.ServletHolder; +import org.junit.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.Mockito; + +public class ServiceDispatcherTest { + private Server server; + + public class SampleODataServlet extends HttpServlet { + private final ServiceHandler handler; // must be stateless + private final EdmProvider provider; // must be stateless + + public SampleODataServlet(ServiceHandler handler, EdmProvider provider) { + this.handler = handler; + this.provider = provider; + } + + @Override + public void service(HttpServletRequest request, HttpServletResponse response) + throws IOException { + OData odata = OData4Impl.newInstance(); + ServiceMetadata metadata = odata.createServiceMetadata(this.provider, Collections.EMPTY_LIST); + + ODataHttpHandler handler = odata.createHandler(metadata); + + handler.register(this.handler); + handler.process(request, response); + } + } + + public int beforeTest(ServiceHandler serviceHandler) throws Exception { + MetadataParser parser = new MetadataParser(); + EdmProvider edmProvider = parser.buildEdmProvider(new FileReader( + "src/test/resources/trippin.xml")); + + this.server = new Server(); + + ServerConnector connector = new ServerConnector(this.server); + this.server.setConnectors(new Connector[] { connector }); + + ServletContextHandler context = new ServletContextHandler(); + context.setContextPath("/trippin"); + context + .addServlet(new ServletHolder(new SampleODataServlet(serviceHandler, edmProvider)), "/*"); + this.server.setHandler(context); + this.server.start(); + + return connector.getLocalPort(); + } + + public void afterTest() throws Exception { + this.server.stop(); + } + + interface TestResult { + void validate() throws Exception; + } + + private void helpGETTest(ServiceHandler handler, String path, TestResult validator) + throws Exception { + int port = beforeTest(handler); + HttpClient http = new HttpClient(); + http.start(); + http.GET("http://localhost:" + port + "/" + path); + validator.validate(); + afterTest(); + } + + private void helpTest(ServiceHandler handler, String path, String method, String payload, + TestResult validator) throws Exception { + int port = beforeTest(handler); + HttpClient http = new HttpClient(); + http.start(); + String editUrl = "http://localhost:" + port + "/" + path; + http.newRequest(editUrl).method(method) + .header("Content-Type", "application/json;odata.metadata=minimal") + .content(TripPinServiceTest.content(payload)).send(); + validator.validate(); + afterTest(); + } + + @Test + public void testMetadata() throws Exception { + final ServiceHandler handler = Mockito.mock(ServiceHandler.class); + helpGETTest(handler, "trippin/$metadata", new TestResult() { + @Override + public void validate() throws Exception { + ArgumentCaptor arg1 = ArgumentCaptor.forClass(MetadataRequest.class); + ArgumentCaptor arg2 = ArgumentCaptor.forClass(MetadataResponse.class); + Mockito.verify(handler).readMetadata(arg1.capture(), arg2.capture()); + } + }); + } + + @Test + public void testEntitySet() throws Exception { + final ServiceHandler handler = Mockito.mock(ServiceHandler.class); + helpGETTest(handler, "trippin/Airports", new TestResult() { + @Override + public void validate() throws Exception { + ArgumentCaptor arg1 = ArgumentCaptor.forClass(DataRequest.class); + ArgumentCaptor arg2 = ArgumentCaptor.forClass(EntityResponse.class); + Mockito.verify(handler).read(arg1.capture(), arg2.capture()); + + DataRequest request = arg1.getValue(); + // Need toString on ContextURL class + // assertEquals("", + // request.getContextURL(request.getOdata()).toString()); + assertEquals("application/json;odata.metadata=minimal", request.getResponseContentType() + .toContentTypeString()); + } + }); + } + + @Test + public void testEntitySetCount() throws Exception { + final ServiceHandler handler = Mockito.mock(ServiceHandler.class); + helpGETTest(handler, "trippin/Airports/$count", new TestResult() { + @Override + public void validate() throws Exception { + ArgumentCaptor arg1 = ArgumentCaptor.forClass(DataRequest.class); + ArgumentCaptor arg2 = ArgumentCaptor.forClass(CountResponse.class); + Mockito.verify(handler).read(arg1.capture(), arg2.capture()); + + DataRequest request = arg1.getValue(); + // Need toString on ContextURL class + // assertEquals("", + // request.getContextURL(request.getOdata()).toString()); + assertEquals("text/plain", request.getResponseContentType().toContentTypeString()); + } + }); + } + + @Test + public void testEntity() throws Exception { + final ServiceHandler handler = Mockito.mock(ServiceHandler.class); + helpGETTest(handler, "trippin/Airports('0')", new TestResult() { + @Override + public void validate() throws Exception { + ArgumentCaptor arg1 = ArgumentCaptor.forClass(DataRequest.class); + ArgumentCaptor arg2 = ArgumentCaptor.forClass(EntityResponse.class); + Mockito.verify(handler).read(arg1.capture(), arg2.capture()); + + DataRequest request = arg1.getValue(); + assertEquals(1, request.getUriResourceEntitySet().getKeyPredicates().size()); + assertEquals("application/json;odata.metadata=minimal", request.getResponseContentType() + .toContentTypeString()); + } + }); + } + + @Test + public void testReadProperty() throws Exception { + final ServiceHandler handler = Mockito.mock(ServiceHandler.class); + helpGETTest(handler, "trippin/Airports('0')/IataCode", new TestResult() { + @Override + public void validate() throws Exception { + ArgumentCaptor arg1 = ArgumentCaptor.forClass(DataRequest.class); + ArgumentCaptor arg2 = ArgumentCaptor.forClass(PropertyResponse.class); + Mockito.verify(handler).read(arg1.capture(), arg2.capture()); + + DataRequest request = arg1.getValue(); + assertEquals(true, request.isPropertyRequest()); + assertEquals(false, request.isPropertyComplex()); + assertEquals(1, request.getUriResourceEntitySet().getKeyPredicates().size()); + assertEquals("application/json;odata.metadata=minimal", request.getResponseContentType() + .toContentTypeString()); + } + }); + } + + @Test + public void testReadComplexProperty() throws Exception { + final ServiceHandler handler = Mockito.mock(ServiceHandler.class); + helpGETTest(handler, "trippin/Airports('0')/Location", new TestResult() { + @Override + public void validate() throws Exception { + ArgumentCaptor arg1 = ArgumentCaptor.forClass(DataRequest.class); + ArgumentCaptor arg2 = ArgumentCaptor.forClass(PropertyResponse.class); + Mockito.verify(handler).read(arg1.capture(), arg2.capture()); + + DataRequest request = arg1.getValue(); + assertEquals(true, request.isPropertyRequest()); + assertEquals(true, request.isPropertyComplex()); + assertEquals(1, request.getUriResourceEntitySet().getKeyPredicates().size()); + assertEquals("application/json;odata.metadata=minimal", request.getResponseContentType() + .toContentTypeString()); + } + }); + } + + @Test + public void testReadProperty$Value() throws Exception { + final ServiceHandler handler = Mockito.mock(ServiceHandler.class); + helpGETTest(handler, "trippin/Airports('0')/IataCode/$value", new TestResult() { + @Override + public void validate() throws Exception { + ArgumentCaptor arg1 = ArgumentCaptor.forClass(DataRequest.class); + ArgumentCaptor arg2 = ArgumentCaptor + .forClass(PrimitiveValueResponse.class); + Mockito.verify(handler).read(arg1.capture(), arg2.capture()); + + DataRequest request = arg1.getValue(); + assertEquals(true, request.isPropertyRequest()); + assertEquals(false, request.isPropertyComplex()); + assertEquals(1, request.getUriResourceEntitySet().getKeyPredicates().size()); + assertEquals("text/plain", request.getResponseContentType().toContentTypeString()); + } + }); + } + + @Test + public void testReadPropertyRef() throws Exception { + final ServiceHandler handler = Mockito.mock(ServiceHandler.class); + helpGETTest(handler, "trippin/Airports('0')/IataCode/$value", new TestResult() { + @Override + public void validate() throws Exception { + ArgumentCaptor arg1 = ArgumentCaptor.forClass(DataRequest.class); + ArgumentCaptor arg2 = ArgumentCaptor + .forClass(PrimitiveValueResponse.class); + Mockito.verify(handler).read(arg1.capture(), arg2.capture()); + + DataRequest request = arg1.getValue(); + assertEquals(true, request.isPropertyRequest()); + assertEquals(false, request.isPropertyComplex()); + assertEquals(1, request.getUriResourceEntitySet().getKeyPredicates().size()); + assertEquals("text/plain", request.getResponseContentType().toContentTypeString()); + } + }); + } + + @Test + public void testFunctionImport() throws Exception { + final ServiceHandler handler = Mockito.mock(ServiceHandler.class); + helpGETTest(handler, "trippin/GetNearestAirport(lat=12.11,lon=34.23)", new TestResult() { + @Override + public void validate() throws Exception { + ArgumentCaptor arg1 = ArgumentCaptor.forClass(FunctionRequest.class); + ArgumentCaptor arg3 = ArgumentCaptor.forClass(PropertyResponse.class); + ArgumentCaptor arg2 = ArgumentCaptor.forClass(HttpMethod.class); + Mockito.verify(handler).invoke(arg1.capture(), arg2.capture(), arg3.capture()); + + FunctionRequest request = arg1.getValue(); + } + }); + } + + @Test + public void testActionImport() throws Exception { + final ServiceHandler handler = Mockito.mock(ServiceHandler.class); + helpTest(handler, "trippin/ResetDataSource", "POST", "", new TestResult() { + @Override + public void validate() throws Exception { + ArgumentCaptor arg1 = ArgumentCaptor.forClass(ActionRequest.class); + ArgumentCaptor arg2 = ArgumentCaptor.forClass(NoContentResponse.class); + Mockito.verify(handler).invoke(arg1.capture(), Mockito.anyString(), arg2.capture()); + + ActionRequest request = arg1.getValue(); + } + }); + } + + @Test + public void testReadMedia() throws Exception { + final ServiceHandler handler = Mockito.mock(ServiceHandler.class); + helpGETTest(handler, "trippin/Photos(1)/$value", new TestResult() { + @Override + public void validate() throws Exception { + ArgumentCaptor arg1 = ArgumentCaptor.forClass(MediaRequest.class); + ArgumentCaptor arg2 = ArgumentCaptor.forClass(StreamResponse.class); + Mockito.verify(handler).readMediaStream(arg1.capture(), arg2.capture()); + + MediaRequest request = arg1.getValue(); + assertEquals("application/octet-stream", request.getResponseContentType() + .toContentTypeString()); + } + }); + } + + @Test + public void testReadNavigation() throws Exception { + final ServiceHandler handler = Mockito.mock(ServiceHandler.class); + helpGETTest(handler, "trippin/People('russelwhyte')/Friends", new TestResult() { + @Override + public void validate() throws Exception { + ArgumentCaptor arg1 = ArgumentCaptor.forClass(DataRequest.class); + ArgumentCaptor arg2 = ArgumentCaptor.forClass(EntitySetResponse.class); + Mockito.verify(handler).read(arg1.capture(), arg2.capture()); + + DataRequest request = arg1.getValue(); + assertEquals("application/json;odata.metadata=minimal", request.getResponseContentType() + .toContentTypeString()); + } + }); + } + + @Test + public void testReadReference() throws Exception { + final ServiceHandler handler = Mockito.mock(ServiceHandler.class); + helpGETTest(handler, "trippin/People('russelwhyte')/Friends/$ref", new TestResult() { + @Override + public void validate() throws Exception { + ArgumentCaptor arg1 = ArgumentCaptor.forClass(DataRequest.class); + ArgumentCaptor arg2 = ArgumentCaptor.forClass(EntitySetResponse.class); + Mockito.verify(handler).read(arg1.capture(), arg2.capture()); + + DataRequest request = arg1.getValue(); + assertEquals("application/json;odata.metadata=minimal", request.getResponseContentType() + .toContentTypeString()); + } + }); + } + + @Test + public void testWriteReferenceCollection() throws Exception { + String payload = "{\n" + "\"@odata.id\": \"/Photos(11)\"\n" + "}"; + + final ServiceHandler handler = Mockito.mock(ServiceHandler.class); + helpTest(handler, "trippin/People('russelwhyte')/Friends/$ref", "POST", payload, + new TestResult() { + @Override + public void validate() throws Exception { + ArgumentCaptor arg1 = ArgumentCaptor.forClass(DataRequest.class); + ArgumentCaptor arg2 = ArgumentCaptor.forClass(String.class); + ArgumentCaptor arg3 = ArgumentCaptor.forClass(List.class); + ArgumentCaptor arg4 = ArgumentCaptor + .forClass(NoContentResponse.class); + Mockito.verify(handler).addReference(arg1.capture(), arg2.capture(), arg3.capture(), + arg4.capture()); + + DataRequest request = arg1.getValue(); + assertEquals("application/json;odata.metadata=minimal", request + .getResponseContentType().toContentTypeString()); + } + }); + } + + @Test + public void testWriteReference() throws Exception { + String payload = "{\n" + "\"@odata.id\": \"/Photos(11)\"\n" + "}"; + + final ServiceHandler handler = Mockito.mock(ServiceHandler.class); + helpTest(handler, "trippin/People('russelwhyte')/Friends('someone')/Photo/$ref", "PUT", payload, + new TestResult() { + @Override + public void validate() throws Exception { + ArgumentCaptor arg1 = ArgumentCaptor.forClass(DataRequest.class); + ArgumentCaptor arg2 = ArgumentCaptor.forClass(String.class); + ArgumentCaptor arg3 = ArgumentCaptor.forClass(URI.class); + ArgumentCaptor arg4 = ArgumentCaptor + .forClass(NoContentResponse.class); + Mockito.verify(handler).updateReference(arg1.capture(), arg2.capture(), arg3.capture(), + arg4.capture()); + + DataRequest request = arg1.getValue(); + assertEquals("application/json;odata.metadata=minimal", request + .getResponseContentType().toContentTypeString()); + } + }); + } +} diff --git a/lib/server-core-ext/src/test/java/org/apache/olingo/server/example/TripPinDataModel.java b/lib/server-core-ext/src/test/java/org/apache/olingo/server/example/TripPinDataModel.java new file mode 100644 index 000000000..904f4d86a --- /dev/null +++ b/lib/server-core-ext/src/test/java/org/apache/olingo/server/example/TripPinDataModel.java @@ -0,0 +1,843 @@ +/* + * 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.example; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.InputStream; +import java.net.URI; +import java.net.URISyntaxException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.UUID; + +import org.apache.olingo.commons.api.data.Entity; +import org.apache.olingo.commons.api.data.EntitySet; +import org.apache.olingo.commons.api.data.Link; +import org.apache.olingo.commons.api.data.Property; +import org.apache.olingo.commons.api.edm.EdmEntityContainer; +import org.apache.olingo.commons.api.edm.EdmEntitySet; +import org.apache.olingo.commons.api.edm.EdmEntityType; +import org.apache.olingo.commons.api.edm.EdmPrimitiveType; +import org.apache.olingo.commons.api.edm.EdmPrimitiveTypeException; +import org.apache.olingo.commons.api.edm.EdmProperty; +import org.apache.olingo.commons.api.edm.EdmType; +import org.apache.olingo.commons.api.edm.FullQualifiedName; +import org.apache.olingo.commons.api.edm.constants.EdmTypeKind; +import org.apache.olingo.commons.core.data.EntityImpl; +import org.apache.olingo.commons.core.data.EntitySetImpl; +import org.apache.olingo.commons.core.data.LinkImpl; +import org.apache.olingo.server.api.ODataApplicationException; +import org.apache.olingo.server.api.ServiceMetadata; +import org.apache.olingo.server.api.deserializer.DeserializerException; +import org.apache.olingo.server.api.uri.UriParameter; +import org.apache.olingo.server.api.uri.UriResourceNavigation; +import org.apache.olingo.server.core.deserializer.json.ODataJsonDeserializer; + +import com.fasterxml.jackson.databind.ObjectMapper; + +public class TripPinDataModel { + private final ServiceMetadata metadata; + private HashMap entitySetMap; + private Map tripLinks; + private Map peopleLinks; + private Map flightLinks; + + public TripPinDataModel(ServiceMetadata metadata) throws Exception { + this.metadata = metadata; + loadData(); + } + + public void loadData() throws Exception { + this.entitySetMap = new HashMap(); + this.tripLinks = new HashMap(); + this.peopleLinks = new HashMap(); + this.flightLinks = new HashMap(); + + EdmEntityContainer ec = metadata.getEdm().getEntityContainer(null); + for (EdmEntitySet edmEntitySet : ec.getEntitySets()) { + String entitySetName = edmEntitySet.getName(); + EntitySet set = loadEnities(entitySetName, edmEntitySet.getEntityType()); + if (set != null) { + this.entitySetMap.put(entitySetName, set); + } + } + + EdmEntityType type = metadata.getEdm().getEntityType( + new FullQualifiedName("Microsoft.OData.SampleService.Models.TripPin", "Trip")); + this.entitySetMap.put("Trip", loadEnities("Trip", type)); + + type = metadata.getEdm().getEntityType( + new FullQualifiedName("Microsoft.OData.SampleService.Models.TripPin", "Flight")); + this.entitySetMap.put("Flight", loadEnities("Flight", type)); + + type = metadata.getEdm().getEntityType( + new FullQualifiedName("Microsoft.OData.SampleService.Models.TripPin", "Event")); + this.entitySetMap.put("Event", loadEnities("Event", type)); + + ObjectMapper mapper = new ObjectMapper(); + Map tripLinks = mapper.readValue(new FileInputStream(new File( + "src/test/resources/trip-links.json")), Map.class); + for (Object link : (ArrayList) tripLinks.get("value")) { + Map map = (Map) link; + this.tripLinks.put((Integer) map.get("TripId"), map); + } + + Map peopleLinks = mapper.readValue(new FileInputStream(new File( + "src/test/resources/people-links.json")), Map.class); + for (Object link : (ArrayList) peopleLinks.get("value")) { + Map map = (Map) link; + this.peopleLinks.put((String) map.get("UserName"), map); + } + + Map flightLinks = mapper.readValue(new FileInputStream(new File( + "src/test/resources/flight-links.json")), Map.class); + for (Object link : (ArrayList) flightLinks.get("value")) { + Map map = (Map) link; + this.flightLinks.put((Integer) map.get("PlanItemId"), map); + } + } + + private EntitySet loadEnities(String entitySetName, EdmEntityType type) { + try { + ODataJsonDeserializer deserializer = new ODataJsonDeserializer(); + + EntitySet set = deserializer.entityCollection(new FileInputStream(new File( + "src/test/resources/" + entitySetName.toLowerCase() + ".json")), type); + // TODO: the count needs to be part of deserializer + set.setCount(set.getEntities().size()); + for (Entity entity : set.getEntities()) { + ((EntityImpl) entity).setETag(UUID.randomUUID().toString()); + ((EntityImpl) entity).setId(new URI(TripPinHandler.buildLocation(entity, entitySetName, + type))); + ((EntityImpl) entity).setType(type.getFullQualifiedName().getFullQualifiedNameAsString()); + } + return set; + } catch (FileNotFoundException e) { + // keep going + e.printStackTrace(); + } catch (DeserializerException e) { + // keep going + e.printStackTrace(); + } catch (URISyntaxException e) { + // keep going + e.printStackTrace(); + } catch (Exception e) { + e.printStackTrace(); + } + return null; + } + + public EntitySet getEntitySet(String name) { + return getEntitySet(name, -1, -1); + } + + public EntitySet getEntitySet(String name, int skip, int pageSize) { + EntitySet set = this.entitySetMap.get(name); + if (set == null) { + return null; + } + + EntitySetImpl modifiedES = new EntitySetImpl(); + int i = 0; + for (Entity e : set.getEntities()) { + if (skip >= 0 && i >= skip && modifiedES.getEntities().size() < pageSize) { + modifiedES.getEntities().add(e); + } + i++; + } + modifiedES.setCount(i); + set.setCount(i); + + if (skip == -1 && pageSize == -1) { + return set; + } + return modifiedES; + } + + private List getMatch(UriParameter param, List es) + throws ODataApplicationException { + ArrayList list = new ArrayList(); + for (Entity entity : es) { + + EdmEntityType entityType = this.metadata.getEdm().getEntityType( + new FullQualifiedName(entity.getType())); + + EdmProperty property = (EdmProperty) entityType.getProperty(param.getName()); + EdmType type = property.getType(); + if (type.getKind() == EdmTypeKind.PRIMITIVE) { + Object match = readPrimitiveValue(property, param.getText()); + Property entityValue = entity.getProperty(param.getName()); + if (match.equals(entityValue.asPrimitive())) { + list.add(entity); + } + } else { + throw new RuntimeException("Can not compare complex objects"); + } + } + return list; + } + + static Object readPrimitiveValue(EdmProperty edmProperty, String value) + throws ODataApplicationException { + if (value == null) { + return null; + } + try { + if (value.startsWith("'") && value.endsWith("'")) { + value = value.substring(1,value.length()-1); + } + EdmPrimitiveType edmPrimitiveType = (EdmPrimitiveType) edmProperty.getType(); + Class javaClass = getJavaClassForPrimitiveType(edmProperty, edmPrimitiveType); + return edmPrimitiveType.valueOfString(value, edmProperty.isNullable(), + edmProperty.getMaxLength(), edmProperty.getPrecision(), edmProperty.getScale(), + edmProperty.isUnicode(), javaClass); + } catch (EdmPrimitiveTypeException e) { + throw new ODataApplicationException("Invalid value: " + value + " for property: " + + edmProperty.getName(), 500, Locale.getDefault()); + } + } + + static Class getJavaClassForPrimitiveType(EdmProperty edmProperty, EdmPrimitiveType edmPrimitiveType) { + Class javaClass = null; + if (edmProperty.getMapping() != null && edmProperty.getMapping().getMappedJavaClass() != null) { + javaClass = edmProperty.getMapping().getMappedJavaClass(); + } else { + javaClass = edmPrimitiveType.getDefaultType(); + } + + edmPrimitiveType.getDefaultType(); + return javaClass; + } + + public Entity getEntity(String name, List keys) throws ODataApplicationException { + EntitySet es = getEntitySet(name); + return getEntity(es, keys); + } + + public Entity getEntity(EntitySet es, List keys) throws ODataApplicationException { + List search = es.getEntities(); + for (UriParameter param : keys) { + search = getMatch(param, search); + } + if (search.isEmpty()) { + return null; + } + return search.get(0); + } + + private EntitySet getFriends(String userName) { + Map map = this.peopleLinks.get(userName); + if (map == null) { + return null; + } + ArrayList friends = (ArrayList) map.get("Friends"); + EntitySet set = getEntitySet("People"); + + EntitySetImpl result = new EntitySetImpl(); + int i = 0; + if (friends != null) { + for (String friend : friends) { + for (Entity e : set.getEntities()) { + if (e.getProperty("UserName").getValue().equals(friend)) { + result.getEntities().add(e); + i++; + break; + } + } + } + } + result.setCount(i); + return result; + } + + private EntitySet getTrips(String userName) { + Map map = this.peopleLinks.get(userName); + if (map == null) { + return null; + } + + ArrayList trips = (ArrayList) map.get("Trips"); + EntitySet set = getEntitySet("Trip"); + + EntitySetImpl result = new EntitySetImpl(); + int i = 0; + if (trips != null) { + for (int trip : trips) { + for (Entity e : set.getEntities()) { + if (e.getProperty("TripId").getValue().equals(trip)) { + result.getEntities().add(e); + i++; + break; + } + } + } + } + result.setCount(i); + return result; + } + + private Entity getPhoto(String userName) { + Map map = this.peopleLinks.get(userName); + if (map == null) { + return null; + } + + Integer photoID = (Integer) map.get("Photo"); + EntitySet set = getEntitySet("Photos"); + if (photoID != null) { + for (Entity e : set.getEntities()) { + if (e.getProperty("Id").getValue().equals(photoID.longValue())) { + return e; + } + } + } + return null; + } + + private EntitySet getPlanItems(int tripId, EntitySetImpl result) { + getFlights(tripId, result); + getEvents(tripId, result); + return result; + } + + private EntitySet getEvents(int tripId, EntitySetImpl result) { + Map map = this.tripLinks.get(tripId); + if (map == null) { + return null; + } + + ArrayList events = (ArrayList) map.get("Events"); + EntitySet set = getEntitySet("Event"); + int i = result.getEntities().size(); + if (events != null) { + for (int event : events) { + for (Entity e : set.getEntities()) { + if (e.getProperty("PlanItemId").getValue().equals(event)) { + result.getEntities().add(e); + i++; + break; + } + } + } + } + result.setCount(i); + return result; + } + + private EntitySet getFlights(int tripId, EntitySetImpl result) { + Map map = this.tripLinks.get(tripId); + if (map == null) { + return null; + } + + ArrayList flights = (ArrayList) map.get("Flights"); + EntitySet set = getEntitySet("Flight"); + int i = result.getEntities().size(); + if (flights != null) { + for (int flight : flights) { + for (Entity e : set.getEntities()) { + if (e.getProperty("PlanItemId").getValue().equals(flight)) { + result.getEntities().add(e); + i++; + break; + } + } + } + } + result.setCount(i); + return result; + } + + private EntitySet getTripPhotos(int tripId) { + Map map = this.tripLinks.get(tripId); + if (map == null) { + return null; + } + + ArrayList photos = (ArrayList) map.get("Photos"); + + EntitySet set = getEntitySet("Photos"); + EntitySetImpl result = new EntitySetImpl(); + int i = 0; + if (photos != null) { + for (int photo : photos) { + for (Entity e : set.getEntities()) { + if (e.getProperty("Id").getValue().equals(photo)) { + result.getEntities().add(e); + i++; + break; + } + } + } + } + result.setCount(i); + return result; + } + + private Entity getFlightFrom(int flighID) { + Map map = this.flightLinks.get(flighID); + if (map == null) { + return null; + } + + String from = (String) map.get("From"); + EntitySet set = getEntitySet("Airports"); + + if (from != null) { + for (Entity e : set.getEntities()) { + if (e.getProperty("IataCode").getValue().equals(from)) { + return e; + } + } + } + return null; + } + + private Entity getFlightTo(int flighID) { + Map map = this.flightLinks.get(flighID); + if (map == null) { + return null; + } + + String to = (String) map.get("To"); + EntitySet set = getEntitySet("Airports"); + + if (to != null) { + for (Entity e : set.getEntities()) { + if (e.getProperty("IataCode").getValue().equals(to)) { + return e; + } + } + } + return null; + } + + private Entity getFlightAirline(int flighID) { + Map map = this.flightLinks.get(flighID); + if (map == null) { + return null; + } + + String airline = (String) map.get("Airline"); + EntitySet set = getEntitySet("Airlines"); + + if (airline != null) { + for (Entity e : set.getEntities()) { + if (e.getProperty("AirlineCode").getValue().equals(airline)) { + return e; + } + } + } + return null; + } + + public void addNavigationLink(String navigation, Entity parentEntity, Entity childEntity) { + + EdmEntityType type = this.metadata.getEdm().getEntityType( + new FullQualifiedName(parentEntity.getType())); + String key = type.getKeyPredicateNames().get(0); + if (type.getName().equals("Person") && navigation.equals("Friends")) { + Map map = this.peopleLinks.get(parentEntity.getProperty(key).getValue()); + if (map == null) { + map = new HashMap(); + this.peopleLinks.put((String) parentEntity.getProperty(key).getValue(), map); + } + + ArrayList friends = (ArrayList) map.get("Friends"); + if (friends == null) { + friends = new ArrayList(); + map.put("Friends", friends); + } + friends.add((String) childEntity.getProperty(key).getValue()); + setLink(parentEntity, navigation, childEntity); + } else if (type.getName().equals("Person") && navigation.equals("Trips")) { + Map map = this.peopleLinks.get(parentEntity.getProperty(key).getValue()); + if (map == null) { + map = new HashMap(); + this.peopleLinks.put((String) parentEntity.getProperty(key).getValue(), map); + } + + ArrayList trips = (ArrayList) map.get("Trips"); + if (trips == null) { + trips = new ArrayList(); + map.put("Trips", trips); + } + trips.add((Integer) childEntity.getProperty(key).getValue()); + setLink(parentEntity, navigation, childEntity); + } else if (type.getName().equals("Person") && navigation.equals("Photo")) { + Map map = this.peopleLinks.get(parentEntity.getProperty(key).getValue()); + if (map == null) { + map = new HashMap(); + this.peopleLinks.put((String) parentEntity.getProperty(key).getValue(), map); + } + map.put("Photo", childEntity.getProperty(key).getValue()); + setLink(parentEntity, navigation, childEntity); + } else if (type.getName().equals("Trip") && navigation.equals("PlanItems")) { + Map map = this.tripLinks.get(parentEntity.getProperty(key).getValue()); + if (map == null) { + map = new HashMap(); + this.tripLinks.put((Integer) parentEntity.getProperty(key).getValue(), map); + } + if (childEntity.getType().equals("Flight")) { + ArrayList flights = (ArrayList) map.get("Flights"); + if (flights == null) { + flights = new ArrayList(); + map.put("Flights", flights); + } + flights.add((Integer) childEntity.getProperty(key).getValue()); + } else { + ArrayList events = (ArrayList) map.get("Events"); + if (events == null) { + events = new ArrayList(); + map.put("Events", events); + } + events.add((Integer) childEntity.getProperty(key).getValue()); + } + setLink(parentEntity, navigation, childEntity); + } else if (type.getName().equals("Trip") && navigation.equals("Photo")) { + Map map = this.tripLinks.get(parentEntity.getProperty(key).getValue()); + if (map == null) { + map = new HashMap(); + this.tripLinks.put((Integer) parentEntity.getProperty(key).getValue(), map); + } + ArrayList photos = (ArrayList) map.get("Photos"); + if (photos == null) { + photos = new ArrayList(); + map.put("Photos", photos); + } + photos.add((Integer) childEntity.getProperty(key).getValue()); + setLink(parentEntity, navigation, childEntity); + } else if (type.getName().equals("Flight") && navigation.equals("From")) { + Map map = this.flightLinks.get(parentEntity.getProperty(key).getValue()); + if (map == null) { + map = new HashMap(); + this.flightLinks.put((Integer) parentEntity.getProperty(key).getValue(), map); + } + map.put("From", childEntity.getProperty(key).getValue()); + setLink(parentEntity, navigation, childEntity); + } else if (type.getName().equals("Flight") && navigation.equals("To")) { + Map map = this.flightLinks.get(parentEntity.getProperty(key).getValue()); + if (map == null) { + map = new HashMap(); + this.flightLinks.put((Integer) parentEntity.getProperty(key).getValue(), map); + } + map.put("To", childEntity.getProperty(key).getValue()); + setLink(parentEntity, navigation, childEntity); + } else if (type.getName().equals("Flight") && navigation.equals("Airline")) { + Map map = this.flightLinks.get(parentEntity.getProperty(key).getValue()); + if (map == null) { + map = new HashMap(); + this.flightLinks.put((Integer) parentEntity.getProperty(key).getValue(), map); + } + map.put("Airline", childEntity.getProperty(key).getValue()); + setLink(parentEntity, navigation, childEntity); + } else { + throw new RuntimeException("unknown relation"); + } + } + + protected static void setLink(Entity entity, final String navigationPropertyName, + final Entity target) { + Link link = new LinkImpl(); + link.setTitle(navigationPropertyName); + link.setInlineEntity(target); + entity.getNavigationLinks().add(link); + } + + public boolean updateNavigationLink(String navigationProperty, Entity parentEntity, + Entity updateEntity) { + boolean updated = false; + EdmEntityType type = this.metadata.getEdm().getEntityType( + new FullQualifiedName(parentEntity.getType())); + String key = type.getKeyPredicateNames().get(0); + + EdmEntityType updateType = this.metadata.getEdm().getEntityType( + new FullQualifiedName(updateEntity.getType())); + String updateKey = updateType.getKeyPredicateNames().get(0); + + if (type.getName().equals("Person") && navigationProperty.equals("Photo")) { + Map map = this.peopleLinks.get(parentEntity.getProperty(key).getValue()); + if (map != null) { + map.put("Photo", ((Long) updateEntity.getProperty(updateKey).getValue()).intValue()); + updated = true; + } + } else if (type.getName().equals("Flight") && navigationProperty.equals("From")) { + Map map = this.flightLinks.get(parentEntity.getProperty(key).getValue()); + if (map != null) { + map.put("From", updateEntity.getProperty(updateKey).getValue()); + updated = true; + } + } else if (type.getName().equals("Flight") && navigationProperty.equals("To")) { + Map map = this.flightLinks.get(parentEntity.getProperty(key).getValue()); + if (map != null) { + map.put("To", updateEntity.getProperty(updateKey).getValue()); + updated = true; + } + } else if (type.getName().equals("Flight") && navigationProperty.equals("Airline")) { + Map map = this.flightLinks.get(parentEntity.getProperty(key).getValue()); + if (map != null) { + map.put("Airline", updateEntity.getProperty(updateKey).getValue()); + updated = true; + } + } else { + throw new RuntimeException("unknown relation"); + } + return updated; + } + + public Entity createEntity(String entitySetName, Entity entity, String location) + throws ODataApplicationException { + + EntitySet set = this.entitySetMap.get(entitySetName); + EntityImpl copy = new EntityImpl(); + copy.setType(entity.getType()); + for (Property p : entity.getProperties()) { + copy.addProperty(p); + } + + try { + copy.setId(new URI(location)); + copy.setETag(UUID.randomUUID().toString()); + } catch (URISyntaxException e) { + throw new ODataApplicationException("Failed to create ID for entity", 500, + Locale.getDefault()); + } + + set.getEntities().add(copy); + return copy; + } + + public boolean deleteEntity(String entitySetName, String eTag, String key, Object keyValue) { + EntitySet set = getEntitySet(entitySetName); + Iterator it = set.getEntities().iterator(); + boolean removed = false; + while (it.hasNext()) { + Entity entity = it.next(); + if (entity.getProperty(key).getValue().equals(keyValue) && eTag.equals("*") + || eTag.equals(entity.getETag())) { + it.remove(); + removed = true; + break; + } + } + return removed; + } + + public boolean updateProperty(String entitySetName, String eTag, String key, Object keyValue, + Property property) { + EntitySet set = getEntitySet(entitySetName); + Iterator it = set.getEntities().iterator(); + boolean replaced = false; + while (it.hasNext()) { + Entity entity = it.next(); + if (entity.getProperty(key).getValue().equals(keyValue) && eTag.equals("*") + || eTag.equals(entity.getETag())) { + entity.getProperty(property.getName()).setValue(property.getValueType(), + property.getValue()); + replaced = true; + break; + } + } + return replaced; + } + + public EntitySet getNavigableEntitySet(Entity parentEntity, UriResourceNavigation navigation) { + EdmEntityType type = this.metadata.getEdm().getEntityType( + new FullQualifiedName(parentEntity.getType())); + + String key = type.getKeyPredicateNames().get(0); + String linkName = navigation.getProperty().getName(); + + EntitySet results = null; + if (type.getName().equals("Person") && linkName.equals("Friends")) { + results = getFriends((String) parentEntity.getProperty(key).getValue()); + } else if (type.getName().equals("Person") && linkName.equals("Trips")) { + results = getTrips((String) parentEntity.getProperty(key).getValue()); + } else if (type.getName().equals("Trip") && linkName.equals("PlanItems")) { + EntitySetImpl planitems = new EntitySetImpl(); + if (navigation.getTypeFilterOnCollection() == null) { + results = getPlanItems((Integer) parentEntity.getProperty(key).getValue(), planitems); + } else if (navigation.getTypeFilterOnCollection().getName().equals("Flight")) { + results = getFlights((Integer) parentEntity.getProperty(key).getValue(), planitems); + } else if (navigation.getTypeFilterOnCollection().getName().equals("Event")) { + results = getEvents((Integer) parentEntity.getProperty(key).getValue(), planitems); + } else { + throw new RuntimeException("unknown relation"); + } + } else if (type.getName().equals("Trip") && linkName.equals("Photos")) { + results = getTripPhotos((Integer) parentEntity.getProperty(key).getValue()); + } + return results; + } + + public Entity getNavigableEntity(Entity parentEntity, UriResourceNavigation navigation) + throws ODataApplicationException { + EdmEntityType type = this.metadata.getEdm().getEntityType( + new FullQualifiedName(parentEntity.getType())); + + String key = type.getKeyPredicateNames().get(0); + String linkName = navigation.getProperty().getName(); + + EntitySet results = null; + if (navigation.getProperty().isCollection()) { + results = getNavigableEntitySet(parentEntity, navigation); + return this.getEntity(results, navigation.getKeyPredicates()); + } + if (type.getName().equals("Person") && linkName.equals("Photo")) { + return getPhoto((String) parentEntity.getProperty(key).getValue()); + } else if (type.getName().equals("Flight") && linkName.equals("From")) { + return getFlightFrom((Integer) parentEntity.getProperty(key).getValue()); + } else if (type.getName().equals("Flight") && linkName.equals("To")) { + return getFlightTo((Integer) parentEntity.getProperty(key).getValue()); + } else if (type.getName().equals("Flight") && linkName.equals("Airline")) { + return getFlightAirline((Integer) parentEntity.getProperty(key).getValue()); + } else { + throw new RuntimeException("unknown relation"); + } + } + + public boolean removeNavigationLink(String navigationProperty, Entity parentEntity, + Entity deleteEntity) { + boolean removed = false; + EdmEntityType type = this.metadata.getEdm().getEntityType( + new FullQualifiedName(parentEntity.getType())); + String key = type.getKeyPredicateNames().get(0); + + if (type.getName().equals("Person") && navigationProperty.equals("Friends")) { + Map map = this.peopleLinks.get(parentEntity.getProperty(key).getValue()); + if (map != null) { + ArrayList friends = (ArrayList) map.get("Friends"); + if (friends != null) { + friends.remove(deleteEntity.getProperty(key).getValue()); + removed = true; + } + } + } else if (type.getName().equals("Person") && navigationProperty.equals("Trips")) { + Map map = this.peopleLinks.get(parentEntity.getProperty(key).getValue()); + if (map != null) { + ArrayList trips = (ArrayList) map.get("Trips"); + if (trips != null) { + trips.remove(deleteEntity.getProperty(key).getValue()); + removed = true; + } + } + } else if (type.getName().equals("Person") && navigationProperty.equals("Photo")) { + Map map = this.peopleLinks.get(parentEntity.getProperty(key).getValue()); + if (map != null) { + map.remove("Photo"); + removed = true; + } + } else if (type.getName().equals("Trip") && navigationProperty.equals("PlanItems")) { + Map map = this.tripLinks.get(parentEntity.getProperty(key).getValue()); + if (map != null) { + if (deleteEntity.getType().equals("Flight")) { + ArrayList flights = (ArrayList) map.get("Flights"); + if (flights != null) { + flights.remove(deleteEntity.getProperty(key).getValue()); + removed = true; + } + } else { + ArrayList events = (ArrayList) map.get("Events"); + if (events != null) { + events.remove(deleteEntity.getProperty(key).getValue()); + removed = true; + } + } + } + } else if (type.getName().equals("Trip") && navigationProperty.equals("Photo")) { + Map map = this.tripLinks.get(parentEntity.getProperty(key).getValue()); + if (map != null) { + ArrayList photos = (ArrayList) map.get("Photos"); + if (photos != null) { + photos.remove(deleteEntity.getProperty(key).getValue()); + removed = true; + } + } + } else if (type.getName().equals("Flight") && navigationProperty.equals("From")) { + Map map = this.flightLinks.get(parentEntity.getProperty(key).getValue()); + if (map != null) { + map.remove("From"); + removed = true; + } + } else if (type.getName().equals("Flight") && navigationProperty.equals("To")) { + Map map = this.flightLinks.get(parentEntity.getProperty(key).getValue()); + if (map != null) { + map.remove("To"); + removed = true; + } + } else if (type.getName().equals("Flight") && navigationProperty.equals("Airline")) { + Map map = this.flightLinks.get(parentEntity.getProperty(key).getValue()); + if (map != null) { + map.remove("Airline"); + removed = true; + } + } else { + throw new RuntimeException("unknown relation"); + } + return removed; + } + + // note these are not tied to entities for simplicity sake + public boolean updateMedia(Entity entity, InputStream mediaContent) + throws ODataApplicationException { + checkForMedia(entity); + return true; + } + + // note these are not tied to entities for simplicity sake + public InputStream readMedia(Entity entity) throws ODataApplicationException { + checkForMedia(entity); + try { + return new FileInputStream(new File("src/test/resources/OlingoOrangeTM.png")); + } catch (FileNotFoundException e) { + throw new ODataApplicationException("image not found", 500, Locale.getDefault()); + } + } + + // note these are not tied to entities for simplicity sake + public boolean deleteMedia(Entity entity) throws ODataApplicationException { + checkForMedia(entity); + return true; + } + + private void checkForMedia(Entity entity) throws ODataApplicationException { + EdmEntityType type = this.metadata.getEdm().getEntityType( + new FullQualifiedName(entity.getType())); + if (!type.hasStream()) { + throw new ODataApplicationException("No Media proeprty on the entity", 500, + Locale.getDefault()); + } + } + + public boolean deleteStream(Entity entity, EdmProperty property) { + // should remove stream links + return true; + } + + public boolean updateStream(Entity entity, EdmProperty property, InputStream streamContent) { + // should add stream links + return true; + } +} \ No newline at end of file diff --git a/lib/server-core-ext/src/test/java/org/apache/olingo/server/example/TripPinHandler.java b/lib/server-core-ext/src/test/java/org/apache/olingo/server/example/TripPinHandler.java new file mode 100644 index 000000000..19a038761 --- /dev/null +++ b/lib/server-core-ext/src/test/java/org/apache/olingo/server/example/TripPinHandler.java @@ -0,0 +1,546 @@ +/* + * 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.example; + +import java.io.ByteArrayInputStream; +import java.io.InputStream; +import java.net.URI; +import java.net.URISyntaxException; +import java.util.List; +import java.util.Locale; +import java.util.Random; + +import org.apache.olingo.commons.api.data.Entity; +import org.apache.olingo.commons.api.data.EntitySet; +import org.apache.olingo.commons.api.data.Link; +import org.apache.olingo.commons.api.data.Property; +import org.apache.olingo.commons.api.edm.EdmAction; +import org.apache.olingo.commons.api.edm.EdmEntitySet; +import org.apache.olingo.commons.api.edm.EdmEntityType; +import org.apache.olingo.commons.api.edm.EdmFunction; +import org.apache.olingo.commons.api.edm.EdmProperty; +import org.apache.olingo.commons.api.edm.EdmSingleton; +import org.apache.olingo.commons.api.format.ContentType; +import org.apache.olingo.commons.api.http.HttpMethod; +import org.apache.olingo.commons.core.data.EntitySetImpl; +import org.apache.olingo.server.api.OData; +import org.apache.olingo.server.api.ODataApplicationException; +import org.apache.olingo.server.api.ODataRequest; +import org.apache.olingo.server.api.ODataResponse; +import org.apache.olingo.server.api.ODataTranslatedException; +import org.apache.olingo.server.api.ServiceMetadata; +import org.apache.olingo.server.api.uri.UriParameter; +import org.apache.olingo.server.api.uri.UriResourceNavigation; +import org.apache.olingo.server.core.ServiceHandler; +import org.apache.olingo.server.core.requests.ActionRequest; +import org.apache.olingo.server.core.requests.DataRequest; +import org.apache.olingo.server.core.requests.FunctionRequest; +import org.apache.olingo.server.core.requests.MediaRequest; +import org.apache.olingo.server.core.requests.MetadataRequest; +import org.apache.olingo.server.core.requests.ServiceDocumentRequest; +import org.apache.olingo.server.core.responses.CountResponse; +import org.apache.olingo.server.core.responses.EntityResponse; +import org.apache.olingo.server.core.responses.EntitySetResponse; +import org.apache.olingo.server.core.responses.MetadataResponse; +import org.apache.olingo.server.core.responses.NoContentResponse; +import org.apache.olingo.server.core.responses.PrimitiveValueResponse; +import org.apache.olingo.server.core.responses.PropertyResponse; +import org.apache.olingo.server.core.responses.ServiceDocumentResponse; +import org.apache.olingo.server.core.responses.ServiceResponse; +import org.apache.olingo.server.core.responses.ServiceResponseVisior; +import org.apache.olingo.server.core.responses.StreamResponse; + +public class TripPinHandler implements ServiceHandler { + private OData odata; + private ServiceMetadata serviceMetadata; + private final TripPinDataModel dataModel; + + public TripPinHandler(TripPinDataModel datamodel) { + this.dataModel = datamodel; + } + + @Override + public void init(OData odata, ServiceMetadata serviceMetadata) { + this.odata = odata; + this.serviceMetadata = serviceMetadata; + } + + @Override + public void readMetadata(MetadataRequest request, MetadataResponse response) + throws ODataTranslatedException, ODataApplicationException { + response.writeMetadata(); + } + + @Override + public void readServiceDocument(ServiceDocumentRequest request, ServiceDocumentResponse response) + throws ODataTranslatedException, ODataApplicationException { + response.writeServiceDocument(request.getODataRequest().getRawBaseUri()); + } + + static class EntityDetails { + EntitySet entitySet = null; + Entity entity = null; + EdmEntityType entityType; + String navigationProperty; + Entity parentEntity = null; + } + + private EntityDetails process(final DataRequest request) throws ODataApplicationException { + EntitySet entitySet = null; + Entity entity = null; + EdmEntityType entityType; + Entity parentEntity = null; + + if (request.isSingleton()) { + EdmSingleton singleton = request.getUriResourceSingleton().getSingleton(); + entityType = singleton.getEntityType(); + if (singleton.getName().equals("Me")) { + entitySet = this.dataModel.getEntitySet("People"); + entity = entitySet.getEntities().get(0); + } + } else { + final EdmEntitySet edmEntitySet = request.getEntitySet(); + entityType = edmEntitySet.getEntityType(); + List keys = request.getKeyPredicates(); + + // TODO: This example so far ignores all system options; but a real + // service should not + if (keys != null && !keys.isEmpty()) { + entity = this.dataModel.getEntity(edmEntitySet.getName(), keys); + } else { + int skip = 0; + if (request.getUriInfo().getSkipTokenOption() != null) { + skip = Integer.parseInt(request.getUriInfo().getSkipTokenOption().getValue()); + } + int pageSize = getPageSize(request); + entitySet = this.dataModel.getEntitySet(edmEntitySet.getName(), skip, pageSize); + if (entitySet.getEntities().size() == pageSize) { + try { + entitySet.setNext(new URI(request.getODataRequest().getRawRequestUri() + "?$skiptoken=" + + (skip + pageSize))); + } catch (URISyntaxException e) { + throw new ODataApplicationException(e.getMessage(), 500, Locale.getDefault()); + } + } + } + } + EntityDetails details = new EntityDetails(); + + if (!request.getNavigations().isEmpty() && entity != null) { + UriResourceNavigation lastNavigation = request.getNavigations().getLast(); + for (UriResourceNavigation nav : request.getNavigations()) { + entityType = nav.getProperty().getType(); + if (nav.isCollection()) { + entitySet = this.dataModel.getNavigableEntitySet(entity, nav); + } else { + parentEntity = entity; + entity = this.dataModel.getNavigableEntity(parentEntity, nav); + } + } + details.navigationProperty = lastNavigation.getProperty().getName(); + } + + details.entity = entity; + details.entitySet = entitySet; + details.entityType = entityType; + details.parentEntity = parentEntity; + return details; + } + + @Override + public void read(final DataRequest request, final T response) + throws ODataTranslatedException, ODataApplicationException { + + final EntityDetails details = process(request); + + response.accepts(new ServiceResponseVisior() { + @Override + public void visit(CountResponse response) throws ODataTranslatedException, ODataApplicationException { + response.writeCount(details.entitySet.getCount()); + } + + @Override + public void visit(PrimitiveValueResponse response) throws ODataTranslatedException, + ODataApplicationException { + EdmProperty edmProperty = request.getUriResourceProperty().getProperty(); + Property property = details.entity.getProperty(edmProperty.getName()); + response.write(property.getValue()); + } + + @Override + public void visit(PropertyResponse response) throws ODataTranslatedException, + ODataApplicationException { + EdmProperty edmProperty = request.getUriResourceProperty().getProperty(); + Property property = details.entity.getProperty(edmProperty.getName()); + response.writeProperty(edmProperty.getType(), property); + } + + @Override + public void visit(StreamResponse response) throws ODataTranslatedException, + ODataApplicationException { + // stream property response + response.writeStreamResponse(new ByteArrayInputStream("dummy".getBytes()), + ContentType.APPLICATION_OCTET_STREAM); + } + + @Override + public void visit(EntitySetResponse response) throws ODataTranslatedException, + ODataApplicationException { + if (request.getPreference("odata.maxpagesize") != null) { + response.writeHeader("Preference-Applied", request.getPreference("odata.maxpagesize")); + } + if (details.entity == null && !request.getNavigations().isEmpty()) { + response.writeReadEntitySet(details.entityType, new EntitySetImpl()); + } else { + response.writeReadEntitySet(details.entityType, details.entitySet); + } + } + + @Override + public void visit(EntityResponse response) throws ODataTranslatedException, + ODataApplicationException { + if (details.entity == null && !request.getNavigations().isEmpty()) { + response.writeNoContent(true); + } else { + response.writeReadEntity(details.entityType, details.entity); + } + } + }); + } + + private int getPageSize(DataRequest request) { + String size = request.getPreference("odata.maxpagesize"); + if (size == null) { + return 8; + } + return Integer.parseInt(size); + } + + @Override + public void createEntity(DataRequest request, Entity entity, EntityResponse response) + throws ODataTranslatedException, ODataApplicationException { + EdmEntitySet edmEntitySet = request.getEntitySet(); + + String location = buildLocation(entity, edmEntitySet.getName(), edmEntitySet.getEntityType()); + Entity created = this.dataModel.createEntity(edmEntitySet.getName(), entity, location); + + try { + // create references, they come in "@odata.bind" value + List bindings = entity.getNavigationBindings(); + if (bindings != null & !bindings.isEmpty()) { + for (Link link : bindings) { + String navigationProperty = link.getTitle(); + String uri = link.getBindingLink(); + if (uri != null) { + DataRequest bindingRequest = request.parseLink(new URI(uri)); + + Entity reference = this.dataModel.getEntity(bindingRequest.getEntitySet().getName(), + bindingRequest.getKeyPredicates()); + + this.dataModel.addNavigationLink(navigationProperty, created, reference); + + } else { + for (String binding : link.getBindingLinks()) { + DataRequest bindingRequest = request.parseLink(new URI(binding)); + + Entity reference = this.dataModel.getEntity(bindingRequest.getEntitySet().getName(), + bindingRequest.getKeyPredicates()); + + this.dataModel.addNavigationLink(navigationProperty, created, reference); + } + } + } + } + } catch (URISyntaxException e) { + throw new ODataApplicationException(e.getMessage(), 500, Locale.getDefault()); + } + + response.writeCreatedEntity(edmEntitySet.getEntityType(), created, location); + } + + static String buildLocation(Entity entity, String name, EdmEntityType type) { + String location = "/" + name + "("; + int i = 0; + boolean usename = type.getKeyPredicateNames().size() > 1; + + for (String key : type.getKeyPredicateNames()) { + if (i > 0) { + location += ","; + } + i++; + if (usename) { + location += (key + "="); + } + if (entity.getProperty(key).getType().equals("Edm.String")) { + location = location + "'" + entity.getProperty(key).getValue().toString() + "'"; + } else { + location = location + entity.getProperty(key).getValue().toString(); + } + } + location += ")"; + return location; + } + + @Override + public void updateEntity(DataRequest request, Entity entity, boolean merge, String entityETag, + EntityResponse response) throws ODataTranslatedException, ODataApplicationException { + response.writeServerError(true); + } + + @Override + public void deleteEntity(DataRequest request, String eTag, EntityResponse response) + throws ODataTranslatedException, ODataApplicationException { + + EdmEntitySet edmEntitySet = request.getEntitySet(); + Entity entity = this.dataModel.getEntity(edmEntitySet.getName(), request.getKeyPredicates()); + if (entity == null) { + response.writeNotFound(true); + return; + } + String key = edmEntitySet.getEntityType().getKeyPredicateNames().get(0); + boolean removed = this.dataModel.deleteEntity(edmEntitySet.getName(), eTag, key, entity + .getProperty(key).getValue()); + + if (removed) { + response.writeDeletedEntityOrReference(); + } else { + response.writeNotFound(true); + } + } + + @Override + public void updateProperty(DataRequest request, final Property property, boolean merge, + String entityETag, PropertyResponse response) throws ODataTranslatedException, + ODataApplicationException { + + EdmEntitySet edmEntitySet = request.getEntitySet(); + Entity entity = this.dataModel.getEntity(edmEntitySet.getName(), request.getKeyPredicates()); + if (entity == null) { + response.writeNotFound(true); + return; + } + + String key = edmEntitySet.getEntityType().getKeyPredicateNames().get(0); + boolean replaced = this.dataModel.updateProperty(edmEntitySet.getName(), entityETag, key, + entity.getProperty(key).getValue(), property); + + if (replaced) { + if (property.getValue() == null) { + response.writePropertyDeleted(); + } else { + response.writePropertyUpdated(); + } + } else { + response.writeServerError(true); + } + } + + @Override + public void invoke(FunctionRequest request, HttpMethod method, + T response) throws ODataTranslatedException, ODataApplicationException { + EdmFunction function = request.getFunction(); + if (function.getName().equals("GetNearestAirport")) { + + final EdmEntityType type = serviceMetadata.getEdm().getEntityContainer(null) + .getEntitySet("Airports").getEntityType(); + + EntitySet es = this.dataModel.getEntitySet("Airports"); + int i = new Random().nextInt(es.getEntities().size()); + final Entity entity = es.getEntities().get(i); + + response.accepts(new ServiceResponseVisior() { + @Override + public void visit(EntityResponse response) throws ODataTranslatedException, + ODataApplicationException { + response.writeReadEntity(type, entity); + } + }); + } + } + + @Override + public void invoke(ActionRequest request, String eTag, T response) + throws ODataTranslatedException, ODataApplicationException { + EdmAction action = request.getAction(); + if (action.getName().equals("ResetDataSource")) { + try { + this.dataModel.loadData(); + response.accepts(new ServiceResponseVisior() { + @Override + public void visit(NoContentResponse response) throws ODataTranslatedException, + ODataApplicationException { + response.writeNoContent(); + } + }); + } catch (Exception e) { + response.writeServerError(true); + } + } else { + response.writeServerError(true); + } + } + + @Override + public void readMediaStream(MediaRequest request, StreamResponse response) + throws ODataTranslatedException, ODataApplicationException { + + final EdmEntitySet edmEntitySet = request.getEntitySet(); + List keys = request.getKeyPredicates(); + Entity entity = this.dataModel.getEntity(edmEntitySet.getName(), keys); + + InputStream contents = this.dataModel.readMedia(entity); + response.writeStreamResponse(contents, request.getResponseContentType()); + } + + @Override + public void upsertMediaStream(MediaRequest request, String entityETag, InputStream mediaContent, + NoContentResponse response) throws ODataTranslatedException, ODataApplicationException { + final EdmEntitySet edmEntitySet = request.getEntitySet(); + List keys = request.getKeyPredicates(); + Entity entity = this.dataModel.getEntity(edmEntitySet.getName(), keys); + + if (mediaContent == null) { + boolean deleted = this.dataModel.deleteMedia(entity); + if (deleted) { + response.writeNoContent(); + } else { + response.writeNotFound(); + } + } else { + boolean updated = this.dataModel.updateMedia(entity, mediaContent); + if (updated) { + response.writeNoContent(); + } else { + response.writeServerError(true); + } + } + } + + @Override + public void upsertStreamProperty(DataRequest request, String entityETag, InputStream streamContent, + NoContentResponse response) throws ODataTranslatedException, ODataApplicationException { + final EdmEntitySet edmEntitySet = request.getEntitySet(); + List keys = request.getKeyPredicates(); + Entity entity = this.dataModel.getEntity(edmEntitySet.getName(), keys); + + EdmProperty property = request.getUriResourceProperty().getProperty(); + + if (streamContent == null) { + boolean deleted = this.dataModel.deleteStream(entity, property); + if (deleted) { + response.writeNoContent(); + } else { + response.writeNotFound(); + } + } else { + boolean updated = this.dataModel.updateStream(entity, property, streamContent); + if (updated) { + response.writeNoContent(); + } else { + response.writeServerError(true); + } + } + } + + @Override + public void addReference(DataRequest request, String entityETag, List references, + NoContentResponse response) throws ODataTranslatedException, ODataApplicationException { + + final EntityDetails details = process(request); + + for (URI reference : references) { + DataRequest bindingRequest = request.parseLink(reference); + Entity linkEntity = this.dataModel.getEntity(bindingRequest.getEntitySet().getName(), + bindingRequest.getKeyPredicates()); + this.dataModel.addNavigationLink(details.navigationProperty, details.entity, linkEntity); + } + response.writeNoContent(); + } + + @Override + public void updateReference(DataRequest request, String entityETag, URI updateId, + NoContentResponse response) throws ODataTranslatedException, ODataApplicationException { + // this single valued navigation. + final EntityDetails details = process(request); + DataRequest updateRequest = request.parseLink(updateId); + Entity updateEntity = this.dataModel.getEntity(updateRequest.getEntitySet().getName(), + updateRequest.getKeyPredicates()); + boolean updated = false; + if (updateEntity != null) { + updated = this.dataModel.updateNavigationLink(details.navigationProperty, + details.parentEntity, updateEntity); + } + + if (updated) { + response.writeNoContent(); + } else { + response.writeServerError(true); + } + } + + @Override + public void deleteReference(DataRequest request, URI deleteId, String entityETag, + NoContentResponse response) throws ODataTranslatedException, ODataApplicationException { + boolean removed = false; + if (deleteId != null) { + final EntityDetails details = process(request); + DataRequest deleteRequest = request.parseLink(deleteId); + Entity deleteEntity = this.dataModel.getEntity(deleteRequest.getEntitySet().getName(), + deleteRequest.getKeyPredicates()); + if (deleteEntity != null) { + removed = this.dataModel.removeNavigationLink(details.navigationProperty, details.entity, + deleteEntity); + } + } else { + // this single valued navigation. + final EntityDetails details = process(request); + removed = this.dataModel.removeNavigationLink(details.navigationProperty, + details.parentEntity, details.entity); + } + if (removed) { + response.writeNoContent(); + } else { + response.writeServerError(true); + } + } + + @Override + public void anyUnsupported(ODataRequest request, ODataResponse response) + throws ODataTranslatedException, ODataApplicationException { + response.setStatusCode(500); + } + + @Override + public String startTransaction() { + return null; + } + + @Override + public void commit(String txnId) { + } + + @Override + public void rollback(String txnId) { + } + + @Override + public void crossJoin(DataRequest dataRequest, List entitySetNames, ODataResponse response) { + response.setStatusCode(200); + } +} diff --git a/lib/server-core-ext/src/test/java/org/apache/olingo/server/example/TripPinServiceTest.java b/lib/server-core-ext/src/test/java/org/apache/olingo/server/example/TripPinServiceTest.java new file mode 100644 index 000000000..4b26b8eb5 --- /dev/null +++ b/lib/server-core-ext/src/test/java/org/apache/olingo/server/example/TripPinServiceTest.java @@ -0,0 +1,756 @@ +/* + * 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.example; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.Iterator; + +import org.apache.olingo.commons.core.Encoder; +import org.eclipse.jetty.client.HttpClient; +import org.eclipse.jetty.client.api.ContentProvider; +import org.eclipse.jetty.client.api.ContentResponse; +import org.eclipse.jetty.http.HttpMethod; +import org.eclipse.jetty.server.Connector; +import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.server.ServerConnector; +import org.eclipse.jetty.servlet.ServletContextHandler; +import org.eclipse.jetty.servlet.ServletHolder; +import org.junit.AfterClass; +import org.junit.BeforeClass; +import org.junit.Ignore; +import org.junit.Test; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ArrayNode; + +/** + * Please note that NONE of the system query options are developed in the sample + * service like $filter, $orderby etc. So using those options will be ignored + * right now. These tests designed to test the framework, all options are responsibilities + * of service developer. + */ +public class TripPinServiceTest { + private static Server server = new Server(); + private static String baseURL; + private static HttpClient http = new HttpClient(); + + @BeforeClass + public static void beforeTest() throws Exception { + ServerConnector connector = new ServerConnector(server); + server.setConnectors(new Connector[] { connector }); + + ServletContextHandler context = new ServletContextHandler(); + context.setContextPath("/trippin"); + context.addServlet(new ServletHolder(new TripPinServlet()), "/*"); + server.setHandler(context); + server.start(); + int port = connector.getLocalPort(); + http.start(); + baseURL = "http://localhost:"+port+"/trippin"; + } + + @AfterClass + public static void afterTest() throws Exception { + server.stop(); + } + + @Test + public void testEntitySet() throws Exception { + ContentResponse response = http.newRequest(baseURL + "/People") + .header("Content-Type", "application/json;odata.metadata=minimal") + .method(HttpMethod.GET) + .send(); + assertEquals(200, response.getStatus()); + + JsonNode node = getJSONNode(response); + + assertEquals("$metadata#People", node.get("@odata.context").asText()); + assertEquals(baseURL+"/People?$skiptoken=8", node.get("@odata.nextLink").asText()); + + JsonNode person = ((ArrayNode)node.get("value")).get(0); + assertEquals("russellwhyte", person.get("UserName").asText()); + } + + private JsonNode getJSONNode(ContentResponse response) throws IOException, + JsonProcessingException { + ObjectMapper objectMapper = new ObjectMapper(); + JsonNode node = objectMapper.readTree(response.getContent()); + return node; + } + + @Test + public void testReadEntitySetWithPaging() throws Exception { + ContentResponse response = http.newRequest(baseURL + "/People") + .header("Prefer", "odata.maxpagesize=10").send(); + + assertEquals(200, response.getStatus()); + + JsonNode node = getJSONNode(response); + assertEquals("$metadata#People", node.get("@odata.context").asText()); + assertEquals(baseURL+"/People?$skiptoken=10", node.get("@odata.nextLink").asText()); + + JsonNode person = ((ArrayNode)node.get("value")).get(0); + assertEquals("russellwhyte", person.get("UserName").asText()); + + assertNotNull(response.getHeaders().get("Preference-Applied")); + } + + @Test + public void testReadEntityWithKey() throws Exception { + ContentResponse response = http.GET(baseURL + "/Airlines('AA')"); + assertEquals(200, response.getStatus()); + JsonNode node = getJSONNode(response); + assertEquals("$metadata#Airlines/$entity", node.get("@odata.context").asText()); + assertEquals("American Airlines", node.get("Name").asText()); + } + + @Test + public void testReadEntityWithNonExistingKey() throws Exception { + ContentResponse response = http.GET(baseURL + "/Airlines('OO')"); + assertEquals(404, response.getStatus()); + } + + @Test + public void testRead$Count() throws Exception { + ContentResponse response = http.GET(baseURL + "/Airlines/$count"); + assertEquals(200, response.getStatus()); + assertEquals("15", response.getContentAsString()); + } + + @Test + public void testReadPrimitiveProperty() throws Exception { + ContentResponse response = http.GET(baseURL + "/Airlines('AA')/Name"); + assertEquals(200, response.getStatus()); + JsonNode node = getJSONNode(response); + assertEquals("$metadata#Airlines('AA')/Name", node.get("@odata.context").asText()); + assertEquals("American Airlines", node.get("value").asText()); + } + + @Test + public void testReadNonExistentProperty() throws Exception { + ContentResponse response = http.GET(baseURL + "/Airlines('AA')/Unknown"); + assertEquals(404, response.getStatus()); + } + + @Test + public void testReadPrimitiveArrayProperty() throws Exception { + ContentResponse response = http.GET(baseURL + "/People('russellwhyte')/Emails"); + assertEquals(200, response.getStatus()); + JsonNode node = getJSONNode(response); + assertEquals("$metadata#People('russellwhyte')/Emails", node.get("@odata.context").asText()); + assertTrue(node.get("value").isArray()); + assertEquals("Russell@example.com", ((ArrayNode)node.get("value")).get(0).asText()); + assertEquals("Russell@contoso.com", ((ArrayNode)node.get("value")).get(1).asText()); + } + + @Test + public void testReadPrimitivePropertyValue() throws Exception { + ContentResponse response = http.GET(baseURL + "/Airlines('AA')/Name/$value"); + assertEquals(200, response.getStatus()); + assertEquals("American Airlines", response.getContentAsString()); + } + + @Test @Ignore + // TODO: Support geometry types to make this run + public void testReadComplexProperty() throws Exception { + ContentResponse response = http.GET(baseURL + "/Airports('KSFO')/Location"); + fail("support geometry type"); + } + + @Test + public void testReadComplexArrayProperty() throws Exception { + ContentResponse response = http.GET(baseURL + "/People('russellwhyte')/AddressInfo"); + assertEquals(200, response.getStatus()); + JsonNode node = getJSONNode(response); + assertEquals("$metadata#People('russellwhyte')/AddressInfo", node.get("@odata.context").asText()); + assertTrue(node.get("value").isArray()); + assertEquals("187 Suffolk Ln.", ((ArrayNode)node.get("value")).get(0).get("Address").asText()); + } + + @Test + public void testReadMedia() throws Exception { + ContentResponse response = http.GET(baseURL + "/Photos(1)/$value"); + assertEquals(200, response.getStatus()); + } + + @Test + public void testCreateMedia() throws Exception { + // treating update and create as same for now, as there is details about + // how entity payload and media payload can be sent at same time in request's body + String editUrl = baseURL + "/Photos(1)/$value"; + ContentResponse response = http.newRequest(editUrl) + .content(content("bytecontents"), "image/jpeg") + .method(HttpMethod.PUT) + .send(); + assertEquals(204, response.getStatus()); + } + + @Test + public void testDeleteMedia() throws Exception { + // treating update and create as same for now, as there is details about + // how entity payload and media payload can be sent at same time in request's body + String editUrl = baseURL + "/Photos(1)/$value"; + ContentResponse response = http.newRequest(editUrl) + .content(content("bytecontents"), "image/jpeg") + .method(HttpMethod.DELETE) + .send(); + assertEquals(204, response.getStatus()); + } + + @Test + public void testCreateStream() throws Exception { + // treating update and create as same for now, as there is details about + // how entity payload and media payload can be sent at same time in request's body + String editUrl = baseURL + "/Airlines('AA')/Picture"; + ContentResponse response = http.newRequest(editUrl) + .content(content("bytecontents"), "image/jpeg") + .method(HttpMethod.POST) + .send(); + // method not allowed + assertEquals(405, response.getStatus()); + } + + @Test + public void testCreateStream2() throws Exception { + // treating update and create as same for now, as there is details about + // how entity payload and media payload can be sent at same time in request's body + String editUrl = baseURL + "/Airlines('AA')/Picture"; + ContentResponse response = http.newRequest(editUrl) + .content(content("bytecontents"), "image/jpeg") + .method(HttpMethod.PUT) + .send(); + assertEquals(204, response.getStatus()); + } + + @Test + public void testDeleteStream() throws Exception { + // treating update and create as same for now, as there is details about + // how entity payload and media payload can be sent at same time in request's body + String editUrl = baseURL + "/Airlines('AA')/Picture"; + ContentResponse response = http.newRequest(editUrl) + .method(HttpMethod.DELETE) + .send(); + assertEquals(204, response.getStatus()); + } + + @Test + public void testReadStream() throws Exception { + // treating update and create as same for now, as there is details about + // how entity payload and media payload can be sent at same time in request's body + String editUrl = baseURL + "/Airlines('AA')/Picture"; + ContentResponse response = http.newRequest(editUrl) + .method(HttpMethod.GET) + .send(); + assertEquals(200, response.getStatus()); + } + + @Test + public void testLambdaAny() throws Exception { + // this is just testing to see the labba expresions are going through the + // framework, none of the system options are not implemented in example service + String query = "Friends/any(d:d/UserName eq 'foo')"; + ContentResponse response = http.newRequest(baseURL + "/People?$filter="+Encoder.encode(query)) + .method(HttpMethod.GET) + .send(); + assertEquals(200, response.getStatus()); + } + + @Test + public void testSingleton() throws Exception { + ContentResponse response = http.GET(baseURL + "/Me"); + assertEquals(200, response.getStatus()); + JsonNode node = getJSONNode(response); + assertEquals("$metadata#Me", node.get("@odata.context").asText()); + assertEquals("russellwhyte", node.get("UserName").asText()); + } + + @Test + public void testSelectOption() throws Exception { + ContentResponse response = http.GET(baseURL + "/People('russellwhyte')?$select=FirstName,LastName"); + assertEquals(200, response.getStatus()); + JsonNode node = getJSONNode(response); + assertEquals("$metadata#People(FirstName,LastName)/$entity", node.get("@odata.context").asText()); + assertEquals("Russell", node.get("FirstName").asText()); + } + + @Test + public void testActionImportWithNoResponse() throws Exception { + ContentResponse response = http.POST(baseURL + "/ResetDataSource").send(); + assertEquals(204, response.getStatus()); + } + + @Test + public void testFunctionImport() throws Exception { + //TODO: fails because of lack of geometery support + ContentResponse response = http.GET(baseURL + "/GetNearestAirport(lat=23.0,lon=34.0)"); + } + + @Test + public void testBadReferences() throws Exception { + ContentResponse response = http.GET(baseURL + "/People('russelwhyte')/$ref"); + assertEquals(405, response.getStatus()); + } + + @Test + public void testReadReferences() throws Exception { + ContentResponse response = http.GET(baseURL + "/People('russellwhyte')/Friends/$ref"); + assertEquals(200, response.getStatus()); + JsonNode node = getJSONNode(response); + assertEquals("$metadata#Collection($ref)", node.get("@odata.context").asText()); + assertTrue(node.get("value").isArray()); + assertEquals("/People('scottketchum')", ((ArrayNode)node.get("value")).get(0).get("@odata.id").asText()); + } + + @Test + public void testAddCollectionReferences() throws Exception { + //GET + ContentResponse response = http.GET(baseURL + "/People('kristakemp')/Friends/$ref"); + assertEquals(200, response.getStatus()); + JsonNode node = getJSONNode(response); + + assertTrue(node.get("value").isArray()); + assertEquals("/People('genevievereeves')", ((ArrayNode)node.get("value")).get(0).get("@odata.id").asText()); + assertNull(((ArrayNode)node.get("value")).get(1)); + + //ADD + String payload = "{\n" + + " \"@odata.context\": \""+baseURL+"/$metadata#Collection($ref)\",\n" + + " \"value\": [\n" + + " { \"@odata.id\": \"People('russellwhyte')\" },\n" + + " { \"@odata.id\": \"People('scottketchum')\" } \n" + + " ]\n" + + "}"; + response = http.POST(baseURL + "/People('kristakemp')/Friends/$ref") + .content(content(payload), "application/json") + .send(); + assertEquals(204, response.getStatus()); + + //GET + response = http.GET(baseURL + "/People('kristakemp')/Friends/$ref"); + assertEquals(200, response.getStatus()); + node = getJSONNode(response); + + assertTrue(node.get("value").isArray()); + assertEquals("/People('genevievereeves')", ((ArrayNode)node.get("value")).get(0).get("@odata.id").asText()); + assertEquals("/People('russellwhyte')", ((ArrayNode)node.get("value")).get(1).get("@odata.id").asText()); + assertEquals("/People('scottketchum')", ((ArrayNode)node.get("value")).get(2).get("@odata.id").asText()); + } + + + @Test + public void testEntityId() throws Exception { + ContentResponse response = http.GET(baseURL+"/$entity?$id="+baseURL + "/People('kristakemp')"); + assertEquals(200, response.getStatus()); + JsonNode node = getJSONNode(response); + assertEquals("$metadata#People/$entity", node.get("@odata.context").asText()); + assertEquals("kristakemp", node.get("UserName").asText()); + + // using relative URL + response = http.GET(baseURL+"/$entity?$id="+"People('kristakemp')"); + assertEquals(200, response.getStatus()); + node = getJSONNode(response); + assertEquals("$metadata#People/$entity", node.get("@odata.context").asText()); + assertEquals("kristakemp", node.get("UserName").asText()); + } + + @Test + public void testCreateReadDeleteEntity() throws Exception { + String payload = "{\n" + + " \"UserName\":\"olingodude\",\n" + + " \"FirstName\":\"Olingo\",\n" + + " \"LastName\":\"Apache\",\n" + + " \"Emails\":[\n" + + " \"olingo@apache.org\"\n" + + " ],\n" + + " \"AddressInfo\":[\n" + + " {\n" + + " \"Address\":\"100 apache Ln.\",\n" + + " \"City\":{\n" + + " \"CountryRegion\":\"United States\",\n" + + " \"Name\":\"Boise\",\n" + + " \"Region\":\"ID\"\n" + + " }\n" + + " }\n" + + " ],\n" + + " \"Gender\":\"0\",\n" + + " \"Concurrency\":635585295719432047\n" + + "}"; + ContentResponse response = http.POST(baseURL + "/People") + .content(content(payload), "application/json") + .header("Prefer", "return=minimal") + .send(); + // the below woud be 204, if minimal was not supplied + assertEquals(204, response.getStatus()); + assertEquals("/People('olingodude')", response.getHeaders().get("Location")); + assertEquals("return=minimal", response.getHeaders().get("Preference-Applied")); + + String location = baseURL+response.getHeaders().get("Location"); + response = http.GET(location); + assertEquals(200, response.getStatus()); + + response = http.newRequest(location).method(HttpMethod.DELETE).send(); + assertEquals(204, response.getStatus()); + + response = http.GET(location); + assertEquals(404, response.getStatus()); + } + + + @Test + public void testCreateEntityWithLinkToRelatedEntities() throws Exception { + String payload = "{\n" + + " \"UserName\":\"olingo\",\n" + + " \"FirstName\":\"Olingo\",\n" + + " \"LastName\":\"Apache\",\n" + + " \"Emails\":[\n" + + " \"olingo@apache.org\"\n" + + " ],\n" + + " \"AddressInfo\":[\n" + + " {\n" + + " \"Address\":\"100 apache Ln.\",\n" + + " \"City\":{\n" + + " \"CountryRegion\":\"United States\",\n" + + " \"Name\":\"Boise\",\n" + + " \"Region\":\"ID\"\n" + + " }\n" + + " }\n" + + " ],\n" + + " \"Gender\":\"0\",\n" + + " \"Concurrency\":635585295719432047,\n" + + "\"Friends@odata.bind\":[\"" + + baseURL+"/People('russellwhyte')\",\""+ + baseURL+"/People('scottketchum')\""+ + "]"+ + "}"; + ContentResponse response = http.POST(baseURL + "/People") + .content(content(payload), "application/json") + .header("Prefer", "return=minimal") + .send(); + // the below woud be 204, if minimal was not supplied + assertEquals(204, response.getStatus()); + + response = http.GET(baseURL+"/People('olingo')/Friends"); + assertEquals(200, response.getStatus()); + + JsonNode node = getJSONNode(response); + assertEquals("$metadata#People", node.get("@odata.context").asText()); + assertTrue(node.get("value").isArray()); + assertEquals("scottketchum", ((ArrayNode)node.get("value")).get(1).get("UserName").asText()); + } + + @Test + public void testUpdatePrimitiveProperty() throws Exception { + String payload = "{" + + " \"value\":\"Pilar Ackerman\"" + + "}"; + + String editUrl = baseURL + "/People('russellwhyte')/FirstName"; + ContentResponse response = http.newRequest(editUrl) + .content(content(payload), "application/json") + .method(HttpMethod.PUT) + .send(); + assertEquals(204, response.getStatus()); + + response = http.GET(editUrl); + assertEquals(200, response.getStatus()); + JsonNode node = getJSONNode(response); + assertEquals("$metadata#People('russellwhyte')/FirstName", node.get("@odata.context").asText()); + assertEquals("Pilar Ackerman", node.get("value").asText()); + } + + @Test + public void testUpdatePrimitiveArrayProperty() throws Exception { + String payload = "{" + + " \"value\": [\n" + + " \"olingo@apache.com\"\n" + + " ]" + + "}"; + + String editUrl = baseURL + "/People('russellwhyte')/Emails"; + ContentResponse response = http.newRequest(editUrl) + .content(content(payload), "application/json") + .method(HttpMethod.PUT) + .send(); + assertEquals(204, response.getStatus()); + + response = http.GET(editUrl); + assertEquals(200, response.getStatus()); + JsonNode node = getJSONNode(response); + assertEquals("$metadata#People('russellwhyte')/Emails", node.get("@odata.context").asText()); + assertTrue(node.get("value").isArray()); + assertEquals("olingo@apache.com", ((ArrayNode)node.get("value")).get(0).asText()); + } + + @Test + public void testDeleteProperty() throws Exception { + String editUrl = baseURL + "/People('russellwhyte')/FirstName"; + ContentResponse response = http.GET(editUrl); + assertEquals(200, response.getStatus()); + JsonNode node = getJSONNode(response); + assertEquals("Russell", node.get("value").asText()); + + response = http.newRequest(editUrl) + .method(HttpMethod.DELETE) + .send(); + assertEquals(204, response.getStatus()); + + response = http.GET(editUrl); + assertEquals(204, response.getStatus()); + } + + @Test + public void testReadNavigationPropertyEntityCollection() throws Exception { + String editUrl = baseURL + "/People('russellwhyte')/Friends"; + ContentResponse response = http.GET(editUrl); + assertEquals(200, response.getStatus()); + + JsonNode node = getJSONNode(response); + assertEquals("$metadata#People", node.get("@odata.context").asText()); + + JsonNode person = ((ArrayNode)node.get("value")).get(0); + assertEquals("scottketchum", person.get("UserName").asText()); + } + + @Test + public void testReadNavigationPropertyEntityCollection2() throws Exception { + String editUrl = baseURL + "/People('russellwhyte')/Friends('scottketchum')/Trips"; + ContentResponse response = http.GET(editUrl); + assertEquals(200, response.getStatus()); + + JsonNode node = getJSONNode(response); + assertEquals("$metadata#People('russellwhyte')/Friends('scottketchum')/Trips", + node.get("@odata.context").asText()); + assertTrue(node.get("value").isArray()); + assertEquals("1001", ((ArrayNode)node.get("value")).get(0).get("TripId").asText()); + } + + @Test + public void testReadNavigationPropertyEntity() throws Exception { + String editUrl = baseURL + "/People('russellwhyte')/Trips(1003)"; + ContentResponse response = http.GET(editUrl); + assertEquals(200, response.getStatus()); + + JsonNode node = getJSONNode(response); + assertEquals("$metadata#People('russellwhyte')/Trips/$entity", + node.get("@odata.context").asText()); + assertEquals("f94e9116-8bdd-4dac-ab61-08438d0d9a71", node.get("ShareId").asText()); + } + + @Test + public void testReadNavigationPropertyEntityNotExisting() throws Exception { + String editUrl = baseURL + "/People('russellwhyte')/Trips(9999)"; + ContentResponse response = http.GET(editUrl); + assertEquals(204, response.getStatus()); + } + + @Test + public void testReadNavigationPropertyEntitySetNotExisting() throws Exception { + String editUrl = baseURL + "/People('jhondoe')/Trips"; + ContentResponse response = http.GET(editUrl); + assertEquals(200, response.getStatus()); + JsonNode node = getJSONNode(response); + assertEquals("$metadata#People('jhondoe')/Trips", + node.get("@odata.context").asText()); + assertEquals(0, ((ArrayNode)node.get("value")).size()); + } + + @Test + public void testBadNavigationProperty() throws Exception { + String editUrl = baseURL + "/People('russellwhyte')/Unknown"; + ContentResponse response = http.GET(editUrl); + assertEquals(404, response.getStatus()); + } + + @Test + public void testReadNavigationPropertyEntityProperty() throws Exception { + String editUrl = baseURL + "/People('russellwhyte')/Trips(1003)/PlanItems(5)/ConfirmationCode"; + ContentResponse response = http.GET(editUrl); + assertEquals(200, response.getStatus()); + + JsonNode node = getJSONNode(response); + assertEquals("$metadata#People('russellwhyte')/Trips(1003)/PlanItems(5)/ConfirmationCode", + node.get("@odata.context").asText()); + + assertEquals("JH58494", node.get("value").asText()); + } + + @Test + public void testReadNavigationPropertyEntityMultipleDerivedTypes() throws Exception { + String editUrl = baseURL + "/People('russellwhyte')/Trips(1003)/PlanItems"; + ContentResponse response = http.GET(editUrl); + assertEquals(200, response.getStatus()); + + JsonNode node = getJSONNode(response); + assertEquals("$metadata#People('russellwhyte')/Trips(1003)/PlanItems", + node.get("@odata.context").asText()); + + assertEquals("#Microsoft.OData.SampleService.Models.TripPin.Flight", + ((ArrayNode) node.get("value")).get(0).get("@odata.type").asText()); + } + + @Test + public void testReadNavigationPropertyEntityCoolectionDerivedFilter() throws Exception { + String editUrl = baseURL + + "/People('russellwhyte')/Trips(1003)/PlanItems/Microsoft.OData.SampleService.Models.TripPin.Event"; + ContentResponse response = http.GET(editUrl); + assertEquals(200, response.getStatus()); + + JsonNode node = getJSONNode(response); + assertEquals("$metadata#People('russellwhyte')/Trips(1003)/PlanItems/" + + "Microsoft.OData.SampleService.Models.TripPin.Event", + node.get("@odata.context").asText()); + + assertEquals("#Microsoft.OData.SampleService.Models.TripPin.Event", + ((ArrayNode) node.get("value")).get(0).get("@odata.type").asText()); + } + + @Test + public void testReadNavigationPropertyEntityDerivedFilter() throws Exception { + String editUrl = baseURL+ "/People('russellwhyte')/Trips(1003)/PlanItems(56)/" + + "Microsoft.OData.SampleService.Models.TripPin.Event"; + ContentResponse response = http.GET(editUrl); + assertEquals(200, response.getStatus()); + JsonNode node = getJSONNode(response); + assertEquals("$metadata#People('russellwhyte')/Trips(1003)/PlanItems/" + + "Microsoft.OData.SampleService.Models.TripPin.Event/$entity", + node.get("@odata.context").asText()); + + assertEquals("#Microsoft.OData.SampleService.Models.TripPin.Event", node.get("@odata.type").asText()); + assertEquals("56", node.get("PlanItemId").asText()); + } + + @Test + public void testUpdateReference() throws Exception { + ContentResponse response = http.GET(baseURL+"/People('ronaldmundy')/Photo/$ref"); + assertEquals(200, response.getStatus()); + JsonNode node = getJSONNode(response); + assertEquals("/Photos(12)", node.get("@odata.id").asText()); + + String msg = "{\n" + + "\"@odata.id\": \"/Photos(11)\"\n" + + "}"; + String editUrl = baseURL + "/People('ronaldmundy')/Photo/$ref"; + response = http.newRequest(editUrl) + .method(HttpMethod.PUT) + .content(content(msg)) + .header("Content-Type", "application/json;odata.metadata=minimal") + .send(); + assertEquals(204, response.getStatus()); + + response = http.GET(baseURL+"/People('ronaldmundy')/Photo/$ref"); + assertEquals(200, response.getStatus()); + node = getJSONNode(response); + assertEquals("/Photos(11)", node.get("@odata.id").asText()); + } + + @Test + public void testAddDelete2ReferenceCollection() throws Exception { + // add + String msg = "{\n" + + "\"@odata.id\": \"/People('russellwhyte')\"\n" + + "}"; + String editUrl = baseURL + "/People('vincentcalabrese')/Friends/$ref"; + ContentResponse response = http.newRequest(editUrl) + .method(HttpMethod.POST) + .content(content(msg)) + .header("Content-Type", "application/json;odata.metadata=minimal") + .send(); + assertEquals(204, response.getStatus()); + + // get + response = http.GET(editUrl); + assertEquals(200, response.getStatus()); + JsonNode node = getJSONNode(response); + assertEquals("/People('russellwhyte')", + ((ArrayNode) node.get("value")).get(2).get("@odata.id").asText()); + + //delete + response = http.newRequest(editUrl+"?$id="+baseURL+"/People('russellwhyte')") + .method(HttpMethod.DELETE) + .content(content(msg)) + .header("Content-Type", "application/json;odata.metadata=minimal") + .send(); + assertEquals(204, response.getStatus()); + + // get + response = http.GET(editUrl); + assertEquals(200, response.getStatus()); + node = getJSONNode(response); + assertNull("/People('russellwhyte')", ((ArrayNode) node.get("value")).get(2)); + } + + @Test + public void testDeleteReference() throws Exception { + String editUrl = baseURL + "/People('russellwhyte')/Photo/$ref"; + ContentResponse response = http.GET(editUrl); + assertEquals(200, response.getStatus()); + + response = http.newRequest(editUrl) + .method(HttpMethod.DELETE) + .send(); + assertEquals(204, response.getStatus()); + + response = http.GET(editUrl); + assertEquals(204, response.getStatus()); + } + + @Test + public void testCrossJoin() throws Exception { + String editUrl = baseURL + "/$crossjoin(People,Airlines)"; + ContentResponse response = http.GET(editUrl); + assertEquals(200, response.getStatus()); + } + + public static ContentProvider content(final String msg) { + return new ContentProvider() { + boolean hasNext = true; + + @Override + public Iterator iterator() { + return new Iterator() { + @Override + public boolean hasNext() { + return hasNext; + } + @Override + public ByteBuffer next() { + hasNext = false; + return ByteBuffer.wrap(msg.getBytes()); + } + @Override + public void remove() { + } + }; + } + @Override + public long getLength() { + return msg.length(); + } + }; + } +} diff --git a/lib/server-core-ext/src/test/java/org/apache/olingo/server/example/TripPinServlet.java b/lib/server-core-ext/src/test/java/org/apache/olingo/server/example/TripPinServlet.java new file mode 100644 index 000000000..2c05d6522 --- /dev/null +++ b/lib/server-core-ext/src/test/java/org/apache/olingo/server/example/TripPinServlet.java @@ -0,0 +1,75 @@ +/* + * 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.example; + +import java.io.FileReader; +import java.io.IOException; +import java.util.Collections; + +import javax.servlet.ServletConfig; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.xml.stream.XMLStreamException; + +import org.apache.olingo.commons.api.edm.provider.EdmProvider; +import org.apache.olingo.server.api.OData; +import org.apache.olingo.server.api.ODataHttpHandler; +import org.apache.olingo.server.api.ServiceMetadata; +import org.apache.olingo.server.core.MetadataParser; +import org.apache.olingo.server.core.OData4Impl; + +public class TripPinServlet extends HttpServlet { + private static final long serialVersionUID = 2663595419366214401L; + private TripPinDataModel dataModel; + + @Override + public void init(ServletConfig config) throws ServletException { + super.init(config); + } + + @Override + public void service(HttpServletRequest request, HttpServletResponse response) throws IOException { + OData odata = OData4Impl.newInstance(); + MetadataParser parser = new MetadataParser(); + EdmProvider edmProvider = null; + + try { + edmProvider = parser.buildEdmProvider(new FileReader("src/test/resources/trippin.xml")); + } catch (XMLStreamException e) { + throw new IOException(e); + } + + ServiceMetadata metadata = odata.createServiceMetadata(edmProvider, Collections.EMPTY_LIST); + + ODataHttpHandler handler = odata.createHandler(metadata); + + if (this.dataModel == null) { + try { + this.dataModel = new TripPinDataModel(metadata); + } catch (Exception e) { + throw new IOException("Failed to load data for TripPin Service"); + } + } + + handler.register(new TripPinHandler(this.dataModel)); + handler.process(request, response); + } +} diff --git a/lib/server-core-ext/src/test/resources/OlingoOrangeTM.png b/lib/server-core-ext/src/test/resources/OlingoOrangeTM.png new file mode 100644 index 0000000000000000000000000000000000000000..d614f1a46ed63b0203cc63d66f188452a63bf204 GIT binary patch literal 93316 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≫oQlm9mStI(9Ac&;0n%B%XC%Nbn>spHW> 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

&FE5Tzr^>0vK-pd3twhBvc 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 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Org.OData.Core.V1.Permission/Read + + + + + + image/jpeg + + + + + + + + + + Org.OData.Core.V1.Permission/Read + + + + + + + + + + + + + + + + + + + + + Org.OData.Core.V1.Permission/Read + + + + + + + + + + + + Org.OData.Core.V1.Permission/Read + + + + + + + + + + + + + + + Org.OData.Core.V1.Permission/Read + + + + + + + + + + + + + + + + + + + + + + + + + + + Org.OData.Core.V1.Permission/Read + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Org.OData.Capabilities.V1.SearchExpressions/none + + + + + + + + + + + + + + + + + + + + + + Concurrency + + + + + + + Org.OData.Capabilities.V1.NavigationType/None + + + + + + + Org.OData.Capabilities.V1.NavigationType/Recursive + + + + + + + + + + + Org.OData.Capabilities.V1.SearchExpressions/none + + + + + + + + + Trips + Friends + + + + + + + + + + + + Org.OData.Capabilities.V1.SearchExpressions/none + + + + + + + + + + + + + + + + + + + Org.OData.Capabilities.V1.SearchExpressions/none + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Org.OData.Capabilities.V1.ConformanceLevelType/Advanced + + + + application/json;odata.metadata=full;IEEE754Compatible=false;odata.streaming=true + application/json;odata.metadata=minimal;IEEE754Compatible=false;odata.streaming=true + application/json;odata.metadata=none;IEEE754Compatible=false;odata.streaming=true + + + + + + + contains + endswith + startswith + length + indexof + substring + tolower + toupper + trim + concat + year + month + day + hour + minute + second + round + floor + ceiling + cast + isof + + + + + + \ No newline at end of file diff --git a/lib/server-core/src/main/java/org/apache/olingo/server/core/deserializer/json/ODataJsonDeserializer.java b/lib/server-core/src/main/java/org/apache/olingo/server/core/deserializer/json/ODataJsonDeserializer.java index 74c73c8f1..a66570f0f 100644 --- a/lib/server-core/src/main/java/org/apache/olingo/server/core/deserializer/json/ODataJsonDeserializer.java +++ b/lib/server-core/src/main/java/org/apache/olingo/server/core/deserializer/json/ODataJsonDeserializer.java @@ -20,6 +20,8 @@ package org.apache.olingo.server.core.deserializer.json; import java.io.IOException; import java.io.InputStream; +import java.net.URI; +import java.net.URISyntaxException; import java.util.ArrayList; import java.util.Iterator; import java.util.List; @@ -65,6 +67,7 @@ import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.JsonMappingException; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ArrayNode; import com.fasterxml.jackson.databind.node.ObjectNode; public class ODataJsonDeserializer implements ODataDeserializer { @@ -267,8 +270,8 @@ public class ODataJsonDeserializer implements ODataDeserializer { node.remove(toRemove); } - private void consumeEntityProperties(final EdmEntityType edmEntityType, final ObjectNode node, final EntityImpl - entity) throws DeserializerException { + private void consumeEntityProperties(final EdmEntityType edmEntityType, final ObjectNode node, + final EntityImpl entity) throws DeserializerException { List propertyNames = edmEntityType.getPropertyNames(); for (String propertyName : propertyNames) { JsonNode jsonNode = node.get(propertyName); @@ -409,7 +412,7 @@ public class ODataJsonDeserializer implements ODataDeserializer { case ENUM: value = readEnumValue(name, type, isNullable, maxLength, precision, scale, isUnicode, mapping, jsonNode); - property.setValue(ValueType.PRIMITIVE, value); + property.setValue(ValueType.ENUM, value); break; case COMPLEX: value = readComplexNode(name, type, isNullable, jsonNode); @@ -706,4 +709,81 @@ public class ODataJsonDeserializer implements ODataDeserializer { DeserializerException.MessageKeys.NOT_IMPLEMENTED); } } + + @Override + public Property property(InputStream stream, EdmProperty edmProperty) + throws DeserializerException { + try { + ObjectMapper objectMapper = new ObjectMapper(); + objectMapper.configure(DeserializationFeature.FAIL_ON_READING_DUP_TREE_KEY, true); + JsonParser parser = new JsonFactory(objectMapper).createParser(stream); + final ObjectNode tree = parser.getCodec().readTree(parser); + + Property property = null; + JsonNode jsonNode = tree.get(Constants.VALUE); + if (jsonNode != null) { + property = consumePropertyNode(edmProperty.getName(), edmProperty.getType(), + edmProperty.isCollection(), + edmProperty.isNullable(), edmProperty.getMaxLength(), edmProperty.getPrecision(), edmProperty.getScale(), + edmProperty.isUnicode(), edmProperty.getMapping(), + jsonNode); + tree.remove(Constants.VALUE); + } else { + property = consumePropertyNode(edmProperty.getName(), edmProperty.getType(), + edmProperty.isCollection(), + edmProperty.isNullable(), edmProperty.getMaxLength(), edmProperty.getPrecision(), edmProperty.getScale(), + edmProperty.isUnicode(), edmProperty.getMapping(), + tree); + } + return property; + } catch (JsonParseException e) { + throw new DeserializerException("An JsonParseException occurred", e, + DeserializerException.MessageKeys.JSON_SYNTAX_EXCEPTION); + } catch (JsonMappingException e) { + throw new DeserializerException("Duplicate property detected", e, + DeserializerException.MessageKeys.DUPLICATE_PROPERTY); + } catch (IOException e) { + throw new DeserializerException("An IOException occurred", e, DeserializerException.MessageKeys.IO_EXCEPTION); + } + } + + public List entityReferences(InputStream stream) throws DeserializerException { + try { + ArrayList parsedValues = new ArrayList(); + ObjectMapper objectMapper = new ObjectMapper(); + objectMapper.configure(DeserializationFeature.FAIL_ON_READING_DUP_TREE_KEY, true); + JsonParser parser = new JsonFactory(objectMapper).createParser(stream); + final ObjectNode tree = parser.getCodec().readTree(parser); + final String key = "@odata.id"; + JsonNode jsonNode = tree.get(Constants.VALUE); + if (jsonNode != null) { + if (jsonNode.isArray()) { + ArrayNode arrayNode = (ArrayNode)jsonNode; + Iterator it = arrayNode.iterator(); + while(it.hasNext()) { + parsedValues.add(new URI(it.next().get(key).asText())); + } + } else { + parsedValues.add(new URI(jsonNode.asText())); + } + tree.remove(Constants.VALUE); + // if this is value there can be only one property + return parsedValues; + } + parsedValues.add(new URI(tree.get(key).asText())); + return parsedValues; + } catch (JsonParseException e) { + throw new DeserializerException("An JsonParseException occurred", e, + DeserializerException.MessageKeys.JSON_SYNTAX_EXCEPTION); + } catch (JsonMappingException e) { + throw new DeserializerException("Duplicate property detected", e, + DeserializerException.MessageKeys.DUPLICATE_PROPERTY); + } catch (IOException e) { + throw new DeserializerException("An IOException occurred", e, + DeserializerException.MessageKeys.IO_EXCEPTION); + } catch (URISyntaxException e) { + throw new DeserializerException("failed to read @odata.id", e, + DeserializerException.MessageKeys.UNKOWN_CONTENT); + } + } } 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 aab662454..5956e8286 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 @@ -6,9 +6,9 @@ * 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 @@ -41,6 +41,7 @@ import org.apache.olingo.commons.api.edm.EdmPrimitiveTypeException; import org.apache.olingo.commons.api.edm.EdmPrimitiveTypeKind; import org.apache.olingo.commons.api.edm.EdmProperty; import org.apache.olingo.commons.api.edm.EdmStructuredType; +import org.apache.olingo.commons.api.edm.FullQualifiedName; import org.apache.olingo.commons.api.format.ODataFormat; import org.apache.olingo.commons.core.edm.primitivetype.EdmPrimitiveTypeFactory; import org.apache.olingo.server.api.ODataServerError; @@ -127,7 +128,8 @@ public class ODataJsonSerializer implements ODataSerializer { } @Override - public InputStream entityCollection(final EdmEntityType entityType, final EntitySet entitySet, + public InputStream entityCollection(final ServiceMetadata metadata, + final EdmEntityType entityType, final EntitySet entitySet, final EntityCollectionSerializerOptions options) throws SerializerException { CircleStreamBuffer buffer = new CircleStreamBuffer(); try { @@ -145,8 +147,9 @@ public class ODataJsonSerializer implements ODataSerializer { json.writeNumberField(Constants.JSON_COUNT, entitySet.getCount()); } json.writeFieldName(Constants.VALUE); - writeEntitySet(entityType, entitySet, - options == null ? null : options.getExpand(), options == null ? null : options.getSelect(), json); + writeEntitySet(metadata, entityType, entitySet, options == null ? null : options.getExpand(), + options == null ? null : options.getSelect(), + options == null ? false : options.onlyReferences(), json); if (entitySet.getNext() != null) { json.writeStringField(Constants.JSON_NEXT_LINK, entitySet.getNext().toASCIIString()); } @@ -159,14 +162,16 @@ public class ODataJsonSerializer implements ODataSerializer { } @Override - public InputStream entity(final EdmEntityType entityType, final Entity entity, - final EntitySerializerOptions options) throws SerializerException { + public InputStream entity(final ServiceMetadata metadata, final EdmEntityType entityType, + final Entity entity, final EntitySerializerOptions options) throws SerializerException { final ContextURL contextURL = checkContextURL(options == null ? null : options.getContextURL()); CircleStreamBuffer buffer = new CircleStreamBuffer(); try { JsonGenerator json = new JsonFactory().createGenerator(buffer.getOutputStream()); - writeEntity(entityType, entity, contextURL, - options == null ? null : options.getExpand(), options == null ? null : options.getSelect(), json); + writeEntity(metadata, entityType, entity, contextURL, + options == null ? null : options.getExpand(), + options == null ? null : options.getSelect(), + options == null ? false: options.onlyReferences(), json); json.close(); } catch (final IOException e) { throw new SerializerException("An I/O exception occurred.", e, @@ -184,18 +189,26 @@ public class ODataJsonSerializer implements ODataSerializer { return contextURL; } - protected void writeEntitySet(final EdmEntityType entityType, final EntitySet entitySet, - final ExpandOption expand, final SelectOption select, final JsonGenerator json) - throws IOException, SerializerException { + protected void writeEntitySet(final ServiceMetadata metadata, final EdmEntityType entityType, + final EntitySet entitySet, final ExpandOption expand, final SelectOption select, + final boolean onlyReference, final JsonGenerator json) throws IOException, + SerializerException { json.writeStartArray(); for (final Entity entity : entitySet.getEntities()) { - writeEntity(entityType, entity, null, expand, select, json); + if (onlyReference) { + json.writeStartObject(); + json.writeStringField(Constants.JSON_ID, entity.getId().toASCIIString()); + json.writeEndObject(); + } else { + writeEntity(metadata, entityType, entity, null, expand, select, false, json); + } } json.writeEndArray(); } - protected void writeEntity(final EdmEntityType entityType, final Entity entity, final ContextURL contextURL, - final ExpandOption expand, final SelectOption select, final JsonGenerator json) + protected void writeEntity(final ServiceMetadata metadata, final EdmEntityType entityType, + final Entity entity, final ContextURL contextURL, final ExpandOption expand, + final SelectOption select, boolean onlyReference, final JsonGenerator json) throws IOException, SerializerException { json.writeStartObject(); if (format != ODataFormat.JSON_NO_METADATA) { @@ -214,9 +227,63 @@ public class ODataJsonSerializer implements ODataSerializer { } } } - writeProperties(entityType, entity.getProperties(), select, json); - writeNavigationProperties(entityType, entity, expand, json); - json.writeEndObject(); + if (onlyReference) { + json.writeStringField(Constants.JSON_ID, entity.getId().toASCIIString()); + } else { + EdmEntityType resolvedType = resolveEntityType(metadata, entityType, entity.getType()); + if (!resolvedType.equals(entityType)) { + json.writeStringField(Constants.JSON_TYPE, "#"+entity.getType()); + } + writeProperties(resolvedType, entity.getProperties(), select, json); + writeNavigationProperties(metadata, resolvedType, entity, expand, json); + json.writeEndObject(); + } + } + + protected EdmEntityType resolveEntityType(ServiceMetadata metadata, EdmEntityType baseType, + String derivedTypeName) throws SerializerException { + if (baseType.getFullQualifiedName().getFullQualifiedNameAsString().equals(derivedTypeName)) { + return baseType; + } + EdmEntityType derivedType = metadata.getEdm().getEntityType(new FullQualifiedName(derivedTypeName)); + if (derivedType == null) { + throw new SerializerException("EntityType not found", + SerializerException.MessageKeys.UNKNOWN_TYPE, derivedTypeName); + } + EdmEntityType type = derivedType.getBaseType(); + while (type != null) { + if (type.getFullQualifiedName().getFullQualifiedNameAsString() + .equals(baseType.getFullQualifiedName().getFullQualifiedNameAsString())) { + return derivedType; + } + type = type.getBaseType(); + } + throw new SerializerException("Wrong base type", + SerializerException.MessageKeys.WRONG_BASE_TYPE, derivedTypeName, baseType + .getFullQualifiedName().getFullQualifiedNameAsString()); + } + + protected EdmComplexType resolveComplexType(ServiceMetadata metadata, EdmComplexType baseType, + String derivedTypeName) throws SerializerException { + if (baseType.getFullQualifiedName().getFullQualifiedNameAsString().equals(derivedTypeName)) { + return baseType; + } + EdmComplexType derivedType = metadata.getEdm().getComplexType(new FullQualifiedName(derivedTypeName)); + if (derivedType == null) { + throw new SerializerException("Complex Type not found", + SerializerException.MessageKeys.UNKNOWN_TYPE, derivedTypeName); + } + EdmComplexType type = derivedType.getBaseType(); + while (type != null) { + if (type.getFullQualifiedName().getFullQualifiedNameAsString() + .equals(baseType.getFullQualifiedName().getFullQualifiedNameAsString())) { + return derivedType; + } + type = type.getBaseType(); + } + throw new SerializerException("Wrong base type", + SerializerException.MessageKeys.WRONG_BASE_TYPE, derivedTypeName, baseType + .getFullQualifiedName().getFullQualifiedNameAsString()); } protected void writeProperties(final EdmStructuredType type, final List properties, @@ -235,8 +302,9 @@ public class ODataJsonSerializer implements ODataSerializer { } } - protected void writeNavigationProperties(final EdmStructuredType type, final Linked linked, - final ExpandOption expand, final JsonGenerator json) throws SerializerException, IOException { + protected void writeNavigationProperties(final ServiceMetadata metadata, + final EdmStructuredType type, final Linked linked, final ExpandOption expand, + final JsonGenerator json) throws SerializerException, IOException { if (ExpandSelectHelper.hasExpand(expand)) { final boolean expandAll = ExpandSelectHelper.isExpandAll(expand); final Set expanded = expandAll ? null : @@ -251,7 +319,7 @@ public class ODataJsonSerializer implements ODataSerializer { throw new SerializerException("Expand options $ref and $levels are not supported.", SerializerException.MessageKeys.NOT_IMPLEMENTED); } - writeExpandedNavigationProperty(property, navigationLink, + writeExpandedNavigationProperty(metadata, property, navigationLink, innerOptions == null ? null : innerOptions.getExpandOption(), innerOptions == null ? null : innerOptions.getSelectOption(), json); @@ -260,7 +328,8 @@ public class ODataJsonSerializer implements ODataSerializer { } } - protected void writeExpandedNavigationProperty(final EdmNavigationProperty property, final Link navigationLink, + protected void writeExpandedNavigationProperty(final ServiceMetadata metadata, + final EdmNavigationProperty property, final Link navigationLink, final ExpandOption innerExpand, final SelectOption innerSelect, JsonGenerator json) throws IOException, SerializerException { json.writeFieldName(property.getName()); @@ -269,13 +338,15 @@ public class ODataJsonSerializer implements ODataSerializer { json.writeStartArray(); json.writeEndArray(); } else { - writeEntitySet(property.getType(), navigationLink.getInlineEntitySet(), innerExpand, innerSelect, json); + writeEntitySet(metadata, property.getType(), navigationLink.getInlineEntitySet(), innerExpand, + innerSelect, false, json); } } else { if (navigationLink == null || navigationLink.getInlineEntity() == null) { json.writeNull(); } else { - writeEntity(property.getType(), navigationLink.getInlineEntity(), null, innerExpand, innerSelect, json); + writeEntity(metadata, property.getType(), navigationLink.getInlineEntity(), null, + innerExpand, innerSelect, false, json); } } } @@ -316,6 +387,11 @@ public class ODataJsonSerializer implements ODataSerializer { } else if (property.isComplex()) { writeComplexValue((EdmComplexType) edmProperty.getType(), property.asComplex().getValue(), selectedPaths, json); + } else if (property.isEnum()) { + writePrimitive((EdmPrimitiveType) edmProperty.getType(), property, + edmProperty.isNullable(), edmProperty.getMaxLength(), + edmProperty.getPrecision(), edmProperty.getScale(), edmProperty.isUnicode(), + json); } else { throw new SerializerException("Property type not yet supported!", SerializerException.MessageKeys.UNSUPPORTED_PROPERTY_TYPE, edmProperty.getName()); @@ -467,8 +543,8 @@ public class ODataJsonSerializer implements ODataSerializer { } @Override - public InputStream complex(final EdmComplexType type, final Property property, - final ComplexSerializerOptions options) throws SerializerException { + public InputStream complex(final ServiceMetadata metadata, final EdmComplexType type, + final Property property, final ComplexSerializerOptions options) throws SerializerException { final ContextURL contextURL = checkContextURL(options == null ? null : options.getContextURL()); CircleStreamBuffer buffer = new CircleStreamBuffer(); try { @@ -477,11 +553,15 @@ public class ODataJsonSerializer implements ODataSerializer { if (contextURL != null) { json.writeStringField(Constants.JSON_CONTEXT, ContextURLBuilder.create(contextURL).toASCIIString()); } + EdmComplexType resolvedType = resolveComplexType(metadata, type, property.getType()); + if (!resolvedType.equals(type)) { + json.writeStringField(Constants.JSON_TYPE, "#"+property.getType()); + } final List values = property.isNull() ? Collections. emptyList() : property.asComplex().getValue(); writeProperties(type, values, options == null ? null : options.getSelect(), json); if (!property.isNull() && property.isComplex()) { - writeNavigationProperties(type, property.asComplex(), + writeNavigationProperties(metadata, type, property.asComplex(), options == null ? null : options.getExpand(), json); } json.writeEndObject(); @@ -523,8 +603,8 @@ public class ODataJsonSerializer implements ODataSerializer { } @Override - public InputStream complexCollection(final EdmComplexType type, final Property property, - final ComplexSerializerOptions options) throws SerializerException { + public InputStream complexCollection(final ServiceMetadata metadata, final EdmComplexType type, + final Property property, final ComplexSerializerOptions options) throws SerializerException { final ContextURL contextURL = checkContextURL(options == null ? null : options.getContextURL()); CircleStreamBuffer buffer = new CircleStreamBuffer(); try { diff --git a/lib/server-core/src/main/java/org/apache/olingo/server/core/serializer/utils/ContextURLBuilder.java b/lib/server-core/src/main/java/org/apache/olingo/server/core/serializer/utils/ContextURLBuilder.java index 4a3f82aae..ac3375976 100644 --- a/lib/server-core/src/main/java/org/apache/olingo/server/core/serializer/utils/ContextURLBuilder.java +++ b/lib/server-core/src/main/java/org/apache/olingo/server/core/serializer/utils/ContextURLBuilder.java @@ -66,7 +66,14 @@ public final class ContextURLBuilder { if (contextURL.getEntitySetOrSingletonOrType() != null) { throw new IllegalArgumentException("ContextURL: $ref with Entity Set"); } - result.append('#').append(ContextURL.Suffix.REFERENCE.getRepresentation()); + if(contextURL.isCollection()) { + result.append('#'); + result.append("Collection(") + .append(ContextURL.Suffix.REFERENCE.getRepresentation()) + .append(")"); + } else { + result.append('#').append(ContextURL.Suffix.REFERENCE.getRepresentation()); + } } else if (contextURL.getSuffix() != null) { if (contextURL.getEntitySetOrSingletonOrType() == null) { throw new IllegalArgumentException("ContextURL: Suffix without preceding Entity Set!"); diff --git a/lib/server-core/src/main/java/org/apache/olingo/server/core/serializer/xml/ODataXmlSerializerImpl.java b/lib/server-core/src/main/java/org/apache/olingo/server/core/serializer/xml/ODataXmlSerializerImpl.java index acd1dedd0..34756c106 100644 --- a/lib/server-core/src/main/java/org/apache/olingo/server/core/serializer/xml/ODataXmlSerializerImpl.java +++ b/lib/server-core/src/main/java/org/apache/olingo/server/core/serializer/xml/ODataXmlSerializerImpl.java @@ -6,9 +6,9 @@ * 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 @@ -87,14 +87,15 @@ public class ODataXmlSerializerImpl implements ODataSerializer { } @Override - public InputStream entity(final EdmEntityType entityType, final Entity entity, - final EntitySerializerOptions options) throws SerializerException { + public InputStream entity(final ServiceMetadata metadata, final EdmEntityType entityType, + final Entity entity, final EntitySerializerOptions options) throws SerializerException { throw new SerializerException("Entity serialization not implemented for XML format", SerializerException.MessageKeys.NOT_IMPLEMENTED); } @Override - public InputStream entityCollection(final EdmEntityType entityType, final EntitySet entitySet, + public InputStream entityCollection(final ServiceMetadata metadata, + final EdmEntityType entityType, final EntitySet entitySet, final EntityCollectionSerializerOptions options) throws SerializerException { throw new SerializerException("Entityset serialization not implemented for XML format", SerializerException.MessageKeys.NOT_IMPLEMENTED); @@ -114,8 +115,8 @@ public class ODataXmlSerializerImpl implements ODataSerializer { } @Override - public InputStream complex(final EdmComplexType type, final Property property, - final ComplexSerializerOptions options) throws SerializerException { + public InputStream complex(final ServiceMetadata metadata, final EdmComplexType type, + final Property property, final ComplexSerializerOptions options) throws SerializerException { throw new SerializerException("Serialization not implemented for XML format.", SerializerException.MessageKeys.NOT_IMPLEMENTED); } @@ -128,8 +129,8 @@ public class ODataXmlSerializerImpl implements ODataSerializer { } @Override - public InputStream complexCollection(final EdmComplexType type, final Property property, - final ComplexSerializerOptions options) throws SerializerException { + public InputStream complexCollection(final ServiceMetadata metadata, final EdmComplexType type, + final Property property, final ComplexSerializerOptions options) throws SerializerException { throw new SerializerException("Serialization not implemented for XML format.", SerializerException.MessageKeys.NOT_IMPLEMENTED); } diff --git a/lib/server-core/src/main/java/org/apache/olingo/server/core/uri/UriResourceActionImpl.java b/lib/server-core/src/main/java/org/apache/olingo/server/core/uri/UriResourceActionImpl.java index 82fe74392..a78d79f1a 100644 --- a/lib/server-core/src/main/java/org/apache/olingo/server/core/uri/UriResourceActionImpl.java +++ b/lib/server-core/src/main/java/org/apache/olingo/server/core/uri/UriResourceActionImpl.java @@ -6,9 +6,9 @@ * 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 @@ -56,12 +56,18 @@ public class UriResourceActionImpl extends UriResourceTypedImpl implements UriRe @Override public boolean isCollection() { - return action.getReturnType() !=null && action.getReturnType().isCollection(); + if (action.getReturnType() != null) { + return action.getReturnType().isCollection(); + } + return false; } @Override public EdmType getType() { - return action.getReturnType() == null ? null : action.getReturnType().getType(); + if (action.getReturnType() != null) { + return action.getReturnType().getType(); + } + return null; } @Override diff --git a/lib/server-core/src/main/java/org/apache/olingo/server/core/uri/validator/UriValidator.java b/lib/server-core/src/main/java/org/apache/olingo/server/core/uri/validator/UriValidator.java index 0b3a5f924..cf5493873 100644 --- a/lib/server-core/src/main/java/org/apache/olingo/server/core/uri/validator/UriValidator.java +++ b/lib/server-core/src/main/java/org/apache/olingo/server/core/uri/validator/UriValidator.java @@ -6,9 +6,9 @@ * 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 @@ -60,7 +60,7 @@ public class UriValidator { /* entitySetCount 7 */ { true , false, false, false, false, false, true, false, false, false, false }, /* entity 8 */ { false, true , true , false, false, false, false, true , false, false, false }, /* mediaStream 9 */ { false, false, false, false, false, false, false, false, false, false, false }, - /* references 10 */ { true , true , false, false, false, true , true , false, true , true , true }, + /* references 10 */ { true , true , false, true, false, true , true , false, true , true , true }, /* reference 11 */ { false, true , false, false, false, false, false, false, false, false, false }, /* propertyComplex 12 */ { false, true , true , false, false, false, false, true , false, false, false }, /* propertyComplexCollection 13 */ { true , true , true , false, true , true , false, true , true , true , true }, @@ -78,7 +78,7 @@ public class UriValidator { /* GET 0 */ { true , true , true , true, true , true , true , true , true , true , true }, /* POST 0 */ { true , false , true , false, false , true , false , true , false , false , false }, /* PUT 0 */ { false , false , false , false, false , false , false , false , false , false , false }, - /* DELETE 0 */ { false , false , false , false, false , false, false , false, false , false , false }, + /* DELETE 0 */ { false , false , false , true, false , false, false , false, false , false , false }, /* PATCH 0 */ { false , false , false , false, false , false , false , false , false , false , false } }; //CHECKSTYLE:ON diff --git a/lib/server-core/src/main/resources/server-core-exceptions-i18n.properties b/lib/server-core/src/main/resources/server-core-exceptions-i18n.properties index 76266ea9c..b8254c3d2 100644 --- a/lib/server-core/src/main/resources/server-core-exceptions-i18n.properties +++ b/lib/server-core/src/main/resources/server-core-exceptions-i18n.properties @@ -96,6 +96,8 @@ SerializerException.INCONSISTENT_PROPERTY_TYPE=An inconsistency has been detecte SerializerException.MISSING_PROPERTY=The non-nullable property '%1$s' is missing. SerializerException.WRONG_PROPERTY_VALUE=The value '%2$s' is not valid for property '%1$s'. SerializerException.WRONG_PRIMITIVE_VALUE=The value '%2$s' is not valid for the primitive type '%1$s' and the given facets. +SerializerException.UNKNOWN_TYPE=Type '%1s' not found in metadata. +SerializerException.WRONG_BASE_TYPE=Type '%1s' is not derived from '%2s'. DeserializerException.NOT_IMPLEMENTED=The requested deserialization method has not been implemented yet. DeserializerException.IO_EXCEPTION=An I/O exception occurred. diff --git a/lib/server-core/src/test/java/org/apache/olingo/server/core/deserializer/json/ODataJsonDeserializerBasicTest.java b/lib/server-core/src/test/java/org/apache/olingo/server/core/deserializer/json/ODataJsonDeserializerBasicTest.java index f3e22efca..a301c3d78 100644 --- a/lib/server-core/src/test/java/org/apache/olingo/server/core/deserializer/json/ODataJsonDeserializerBasicTest.java +++ b/lib/server-core/src/test/java/org/apache/olingo/server/core/deserializer/json/ODataJsonDeserializerBasicTest.java @@ -6,9 +6,9 @@ * 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 @@ -18,8 +18,13 @@ */ package org.apache.olingo.server.core.deserializer.json; +import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; +import java.io.ByteArrayInputStream; +import java.net.URI; +import java.util.List; + import org.apache.olingo.commons.api.format.ODataFormat; import org.apache.olingo.server.api.OData; import org.apache.olingo.server.api.deserializer.ODataDeserializer; @@ -41,4 +46,33 @@ public class ODataJsonDeserializerBasicTest { assertNotNull(deserializer); deserializer = null; } + + @Test + public void testReadingCollectionProperties() throws Exception { + String payload = "{\n" + + " \"@odata.context\": \"http://host/service/$metadata#Collection($ref)\",\n" + + " \"value\": [\n" + + " { \"@odata.id\": \"Orders(10643)\" },\n" + + " { \"@odata.id\": \"Orders(10759)\" }\n" + + " ]\n" + + "}"; + ODataDeserializer deserializer = OData.newInstance().createDeserializer(ODataFormat.JSON); + List values = deserializer.entityReferences(new ByteArrayInputStream(payload.getBytes())); + assertEquals(2, values.size()); + assertEquals("Orders(10643)", values.get(0).toASCIIString()); + assertEquals("Orders(10759)", values.get(1).toASCIIString()); + } + + @Test + public void testReadingProperties() throws Exception { + String payload = "{\n" + + " \"@odata.context\": \"http://host/service/$metadata#$ref\",\n" + + " \"@odata.id\": \"Orders(10643)\"\n" + + "}"; + ODataDeserializer deserializer = OData.newInstance().createDeserializer(ODataFormat.JSON); + List values = deserializer.entityReferences(new ByteArrayInputStream(payload + .getBytes())); + assertEquals(1, values.size()); + assertEquals("Orders(10643)", values.get(0).toASCIIString()); + } } diff --git a/lib/server-core/src/test/java/org/apache/olingo/server/core/serializer/json/ODataJsonSerializerTest.java b/lib/server-core/src/test/java/org/apache/olingo/server/core/serializer/json/ODataJsonSerializerTest.java index 742c5d54e..1e4537f8d 100644 --- a/lib/server-core/src/test/java/org/apache/olingo/server/core/serializer/json/ODataJsonSerializerTest.java +++ b/lib/server-core/src/test/java/org/apache/olingo/server/core/serializer/json/ODataJsonSerializerTest.java @@ -6,9 +6,9 @@ * 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 @@ -49,14 +49,17 @@ public class ODataJsonSerializerTest { final ODataJsonSerializer serializer = new ODataJsonSerializer(ODataFormat.APPLICATION_JSON); final ComplexSerializerOptions options = ComplexSerializerOptions.with() .contextURL(ContextURL.with().selectList("ComplexCollection").build()).build(); - final InputStream in = serializer.complexCollection(ComplexTypeHelper.createType(), complexCollection, options); + final InputStream in = serializer.complexCollection(null, ComplexTypeHelper.createType(), + complexCollection, options); final BufferedReader reader = new BufferedReader(new InputStreamReader(in)); String line; while ((line = reader.readLine()) != null) { if (line.contains("value")) { - assertEquals("{\"@odata.context\":\"$metadata(ComplexCollection)\",\"value\":" - + "[{\"prop1\":\"test1\",\"prop2\":\"test11\"},{\"prop1\":\"test2\",\"prop2\":\"test22\"}]}", line); + assertEquals( + "{\"@odata.context\":\"$metadata(ComplexCollection)\",\"value\":" + + "[{\"prop1\":\"test1\",\"prop2\":\"test11\"},{\"prop1\":\"test2\",\"prop2\":\"test22\"}]}", + line); } } diff --git a/lib/server-tecsvc/src/main/java/org/apache/olingo/server/tecsvc/TechnicalServlet.java b/lib/server-tecsvc/src/main/java/org/apache/olingo/server/tecsvc/TechnicalServlet.java index 7fa981b44..4137db09c 100644 --- a/lib/server-tecsvc/src/main/java/org/apache/olingo/server/tecsvc/TechnicalServlet.java +++ b/lib/server-tecsvc/src/main/java/org/apache/olingo/server/tecsvc/TechnicalServlet.java @@ -6,9 +6,9 @@ * 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 @@ -66,8 +66,8 @@ public class TechnicalServlet extends HttpServlet { } ODataHttpHandler handler = odata.createHandler(serviceMetadata); - handler.register(new TechnicalEntityProcessor(dataProvider)); - handler.register(new TechnicalPrimitiveComplexProcessor(dataProvider)); + handler.register(new TechnicalEntityProcessor(dataProvider, serviceMetadata)); + handler.register(new TechnicalPrimitiveComplexProcessor(dataProvider, serviceMetadata)); handler.register(new TechnicalBatchProcessor(dataProvider)); handler.process(req, resp); } catch (RuntimeException e) { diff --git a/lib/server-tecsvc/src/main/java/org/apache/olingo/server/tecsvc/data/DataCreator.java b/lib/server-tecsvc/src/main/java/org/apache/olingo/server/tecsvc/data/DataCreator.java index e6193dff5..1f28900f4 100644 --- a/lib/server-tecsvc/src/main/java/org/apache/olingo/server/tecsvc/data/DataCreator.java +++ b/lib/server-tecsvc/src/main/java/org/apache/olingo/server/tecsvc/data/DataCreator.java @@ -6,9 +6,9 @@ * 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 @@ -41,11 +41,14 @@ import org.apache.olingo.commons.core.data.EntityImpl; import org.apache.olingo.commons.core.data.EntitySetImpl; import org.apache.olingo.commons.core.data.LinkImpl; import org.apache.olingo.commons.core.data.PropertyImpl; +import org.apache.olingo.server.tecsvc.provider.ComplexTypeProvider; +import org.apache.olingo.server.tecsvc.provider.EntityTypeProvider; + public class DataCreator { private static final UUID GUID = UUID.fromString("01234567-89ab-cdef-0123-456789abcdef"); - + private static final String ctPropComp = ComplexTypeProvider.nameCTTwoPrim.getFullQualifiedNameAsString(); private final Map data; public DataCreator() { @@ -96,7 +99,9 @@ public class DataCreator { .addProperty(createPrimitive("PropertyInt16", i)) .addProperty(createPrimitive("PropertyString", "Number:" + i))); } - + for (Entity en:entitySet.getEntities()) { + en.setType(EntityTypeProvider.nameETServerSidePaging.getFullQualifiedNameAsString()); + } return entitySet; } @@ -106,7 +111,9 @@ public class DataCreator { entitySet.getEntities().add(createETKeyNavEntity(1, "I am String Property 1")); entitySet.getEntities().add(createETKeyNavEntity(2, "I am String Property 2")); entitySet.getEntities().add(createETKeyNavEntity(3, "I am String Property 3")); - + for (Entity en:entitySet.getEntities()) { + en.setType(EntityTypeProvider.nameETKeyNav.getFullQualifiedNameAsString()); + } return entitySet; } @@ -115,10 +122,12 @@ public class DataCreator { return new EntityImpl() .addProperty(createPrimitive("PropertyInt16", propertyInt16)) .addProperty(createPrimitive("PropertyString", propertyString)) - .addProperty(createComplex("PropertyCompNav", + .addProperty(createComplex("PropertyCompNav", ctPropComp, createPrimitive("PropertyInt16", 1))) .addProperty(createKeyNavAllPrimComplexValue("PropertyCompAllPrim")) - .addProperty(createComplex("PropertyCompTwoPrim", + .addProperty( + createComplex("PropertyCompTwoPrim", + ComplexTypeProvider.nameCTTwoPrim.getFullQualifiedNameAsString(), createPrimitive("PropertyInt16", 16), createPrimitive("PropertyString", "Test123"))) .addProperty(createPrimitiveCollection("CollPropertyString", @@ -126,7 +135,9 @@ public class DataCreator { "Employee2@company.example", "Employee3@company.example")) .addProperty(createPrimitiveCollection("CollPropertyInt16", 1000, 2000, 30112)) - .addProperty(createComplexCollection("CollPropertyComp", + .addProperty( + createComplexCollection("CollPropertyComp", ComplexTypeProvider.nameCTPrimComp + .getFullQualifiedNameAsString(), Arrays.asList( createPrimitive("PropertyInt16", 1), createKeyNavAllPrimComplexValue("PropertyComp")), @@ -136,9 +147,11 @@ public class DataCreator { Arrays.asList( createPrimitive("PropertyInt16", 3), createKeyNavAllPrimComplexValue("PropertyComp")))) - .addProperty(createComplex("PropertyCompCompNav", + .addProperty( + createComplex("PropertyCompCompNav", + ComplexTypeProvider.nameCTCompComp.getFullQualifiedNameAsString(), createPrimitive("PropertyString", "1"), - createComplex("PropertyComp", createPrimitive("PropertyInt16", 1)))); + createComplex("PropertyComp", ctPropComp, createPrimitive("PropertyInt16", 1)))); } private EntitySet createESTwoKeyNav() { @@ -148,7 +161,9 @@ public class DataCreator { entitySet.getEntities().add(createESTwoKeyNavEntity(1, "2")); entitySet.getEntities().add(createESTwoKeyNavEntity(2, "1")); entitySet.getEntities().add(createESTwoKeyNavEntity(3, "1")); - + for (Entity en:entitySet.getEntities()) { + en.setType(EntityTypeProvider.nameETTwoKeyNav.getFullQualifiedNameAsString()); + } return entitySet; } @@ -157,9 +172,9 @@ public class DataCreator { return new EntityImpl() .addProperty(createPrimitive("PropertyInt16", propertyInt16)) .addProperty(createPrimitive("PropertyString", propertyString)) - .addProperty(createComplex("PropertyComp", + .addProperty(createComplex("PropertyComp",ctPropComp, createPrimitive("PropertyInt16", 11), - createComplex("PropertyComp", + createComplex("PropertyComp", ctPropComp, createPrimitive("PropertyString", "StringValue"), createPrimitive("PropertyBinary", new byte[] { 1, 35, 69, 103, -119, -85, -51, -17 }), createPrimitive("PropertyBoolean", true), @@ -175,20 +190,26 @@ public class DataCreator { createPrimitive("PropertyInt64", Long.MAX_VALUE), createPrimitive("PropertySByte", Byte.MAX_VALUE), createPrimitive("PropertyTimeOfDay", getTime(21, 5, 59))))) - .addProperty(createComplex("PropertyCompNav", + .addProperty( + createComplex("PropertyCompNav", + ComplexTypeProvider.nameCTCompNav.getFullQualifiedNameAsString(), createPrimitive("PropertyInt16", 1), createKeyNavAllPrimComplexValue("PropertyComp"))) - .addProperty(createComplexCollection("CollPropertyComp")) - .addProperty(createComplexCollection("CollPropertyCompNav", + .addProperty(createComplexCollection("CollPropertyComp", null)) + .addProperty( + createComplexCollection("CollPropertyCompNav", + ComplexTypeProvider.nameCTCompNav.getFullQualifiedNameAsString(), Arrays.asList(createPrimitive("PropertyInt16", 1)))) .addProperty(createPrimitiveCollection("CollPropertyString", 1, 2)) - .addProperty(createComplex("PropertyCompTwoPrim", + .addProperty( + createComplex("PropertyCompTwoPrim", + ComplexTypeProvider.nameCTTwoPrim.getFullQualifiedNameAsString(), createPrimitive("PropertyInt16", 11), createPrimitive("PropertyString", "11"))); } private Property createKeyNavAllPrimComplexValue(final String name) { - return createComplex(name, + return createComplex(name, ComplexTypeProvider.nameCTAllPrim.getFullQualifiedNameAsString(), createPrimitive("PropertyString", "First Resource - positive values"), createPrimitive("PropertyBinary", new byte[] { 1, 35, 69, 103, -119, -85, -51, -17 }), createPrimitive("PropertyBoolean", true), @@ -213,8 +234,9 @@ public class DataCreator { entitySet.getEntities().add(new EntityImpl() .addProperty(createPrimitive("PropertyInt16", Short.MAX_VALUE)) - .addProperty(createComplex("PropertyComp", - createComplexCollection("CollPropertyComp", + .addProperty(createComplex("PropertyComp", null, + createComplexCollection("CollPropertyComp", ComplexTypeProvider.nameCTTwoPrim + .getFullQualifiedNameAsString(), Arrays.asList( createPrimitive("PropertyInt16", 555), createPrimitive("PropertyString", "1 Test Complex in Complex Property")), @@ -227,8 +249,9 @@ public class DataCreator { entitySet.getEntities().add(new EntityImpl() .addProperty(createPrimitive("PropertyInt16", 12345)) - .addProperty(createComplex("PropertyComp", - createComplexCollection("CollPropertyComp", + .addProperty(createComplex("PropertyComp",null, + createComplexCollection("CollPropertyComp", ComplexTypeProvider.nameCTTwoPrim + .getFullQualifiedNameAsString(), Arrays.asList( createPrimitive("PropertyInt16", 888), createPrimitive("PropertyString", "11 Test Complex in Complex Property")), @@ -238,7 +261,9 @@ public class DataCreator { Arrays.asList( createPrimitive("PropertyInt16", 0), createPrimitive("PropertyString", "13 Test Complex in Complex Property")))))); - + for (Entity en:entitySet.getEntities()) { + en.setType(EntityTypeProvider.nameETCompCollComp.getFullQualifiedNameAsString()); + } return entitySet; } @@ -260,7 +285,9 @@ public class DataCreator { entitySet.getEntities().add(new EntityImpl() .addProperty(createPrimitive("PropertyInt16", Short.MAX_VALUE)) .addProperty(createPrimitive("PropertyString", "Test String4"))); - + for (Entity en:entitySet.getEntities()) { + en.setType(EntityTypeProvider.nameETTwoPrim.getFullQualifiedNameAsString()); + } return entitySet; } @@ -322,7 +349,9 @@ public class DataCreator { .addProperty(createPrimitive("PropertyDuration", 0)) .addProperty(createPrimitive("PropertyGuid", UUID.fromString("76543201-23ab-cdef-0123-456789cccddd"))) .addProperty(createPrimitive("PropertyTimeOfDay", getTime(0, 1, 1)))); - + for (Entity en:entitySet.getEntities()) { + en.setType(EntityTypeProvider.nameETAllPrim.getFullQualifiedNameAsString()); + } return entitySet; } @@ -331,7 +360,7 @@ public class DataCreator { Entity entity = new EntityImpl(); entity.addProperty(createPrimitive("PropertyInt16", Short.MAX_VALUE)); - entity.addProperty(createComplex("PropertyComp", + entity.addProperty(createComplex("PropertyComp",ctPropComp, createPrimitive("PropertyString", "First Resource - first"), createPrimitive("PropertyBinary", new byte[] { 0x01, 0x23, 0x45, 0x67, (byte) 0x89, (byte) 0xAB, (byte) 0xCD, (byte) 0xEF }), @@ -353,7 +382,7 @@ public class DataCreator { entity = new EntityImpl(); entity.addProperty(createPrimitive("PropertyInt16", 7)); - entity.addProperty(createComplex("PropertyComp", + entity.addProperty(createComplex("PropertyComp",ctPropComp, createPrimitive("PropertyString", "Second Resource - second"), createPrimitive("PropertyBinary", new byte[] { 0x01, 0x23, 0x45, 0x67, (byte) 0x89, (byte) 0xAB, (byte) 0xCD, (byte) 0xEF }), @@ -375,7 +404,7 @@ public class DataCreator { entity = new EntityImpl(); entity.addProperty(createPrimitive("PropertyInt16", 0)); - entity.addProperty(createComplex("PropertyComp", + entity.addProperty(createComplex("PropertyComp",ctPropComp, createPrimitive("PropertyString", "Third Resource - third"), createPrimitive("PropertyBinary", new byte[] { 0x01, 0x23, 0x45, 0x67, (byte) 0x89, (byte) 0xAB, (byte) 0xCD, (byte) 0xEF }), @@ -394,7 +423,9 @@ public class DataCreator { createPrimitive("PropertySByte", Byte.MAX_VALUE), createPrimitive("PropertyTimeOfDay", getTime(13, 27, 45)))); entitySet.getEntities().add(entity); - + for (Entity en:entitySet.getEntities()) { + en.setType(EntityTypeProvider.nameETCompAllPrim.getFullQualifiedNameAsString()); + } return entitySet; } @@ -444,13 +475,15 @@ public class DataCreator { entity.getProperties().addAll(entitySet.getEntities().get(0).getProperties()); entity.getProperties().set(0, createPrimitive("PropertyInt16", 3)); entitySet.getEntities().add(entity); - + for (Entity en:entitySet.getEntities()) { + en.setType(EntityTypeProvider.nameETCollAllPrim.getFullQualifiedNameAsString()); + } return entitySet; } private EntitySet createESMixPrimCollComp() { @SuppressWarnings("unchecked") - final Property complexCollection = createComplexCollection("CollPropertyComp", + final Property complexCollection = createComplexCollection("CollPropertyComp", ctPropComp, Arrays.asList(createPrimitive("PropertyInt16", 123), createPrimitive("PropertyString", "TEST 1")), Arrays.asList(createPrimitive("PropertyInt16", 456), createPrimitive("PropertyString", "TEST 2")), Arrays.asList(createPrimitive("PropertyInt16", 789), createPrimitive("PropertyString", "TEST 3"))); @@ -461,7 +494,7 @@ public class DataCreator { .addProperty(createPrimitive("PropertyInt16", Short.MAX_VALUE)) .addProperty(createPrimitiveCollection("CollPropertyString", "Employee1@company.example", "Employee2@company.example", "Employee3@company.example")) - .addProperty(createComplex("PropertyComp", + .addProperty(createComplex("PropertyComp",ctPropComp, createPrimitive("PropertyInt16", 111), createPrimitive("PropertyString", "TEST A"))) .addProperty(complexCollection)); @@ -470,7 +503,7 @@ public class DataCreator { .addProperty(createPrimitive("PropertyInt16", 7)) .addProperty(createPrimitiveCollection("CollPropertyString", "Employee1@company.example", "Employee2@company.example", "Employee3@company.example")) - .addProperty(createComplex("PropertyComp", + .addProperty(createComplex("PropertyComp",ctPropComp, createPrimitive("PropertyInt16", 222), createPrimitive("PropertyString", "TEST B"))) .addProperty(complexCollection)); @@ -479,11 +512,13 @@ public class DataCreator { .addProperty(createPrimitive("PropertyInt16", 0)) .addProperty(createPrimitiveCollection("CollPropertyString", "Employee1@company.example", "Employee2@company.example", "Employee3@company.example")) - .addProperty(createComplex("PropertyComp", + .addProperty(createComplex("PropertyComp",ctPropComp, createPrimitive("PropertyInt16", 333), createPrimitive("PropertyString", "TEST C"))) .addProperty(complexCollection)); - + for (Entity en:entitySet.getEntities()) { + en.setType(EntityTypeProvider.nameETMixPrimCollComp.getFullQualifiedNameAsString()); + } return entitySet; } @@ -519,7 +554,9 @@ public class DataCreator { .addProperty(createPrimitive("PropertyDuration", 6)) .addProperty(createPrimitive("PropertyGuid", GUID)) .addProperty(createPrimitive("PropertyTimeOfDay", getTime(2, 48, 21)))); - + for (Entity en:entitySet.getEntities()) { + en.setType(EntityTypeProvider.nameETAllKey.getFullQualifiedNameAsString()); + } return entitySet; } @@ -528,20 +565,22 @@ public class DataCreator { Entity entity = new EntityImpl(); entity.addProperty(createPrimitive("PropertyInt16", 1)); - entity.addProperty(createComplex("PropertyComp", - createComplex("PropertyComp", + entity.addProperty(createComplex("PropertyComp", null, + createComplex("PropertyComp",ctPropComp, createPrimitive("PropertyInt16", 123), createPrimitive("PropertyString", "String 1")))); entitySet.getEntities().add(entity); entity = new EntityImpl(); entity.addProperty(createPrimitive("PropertyInt16", 2)); - entity.addProperty(createComplex("PropertyComp", - createComplex("PropertyComp", + entity.addProperty(createComplex("PropertyComp", null, + createComplex("PropertyComp",ctPropComp, createPrimitive("PropertyInt16", 987), createPrimitive("PropertyString", "String 2")))); entitySet.getEntities().add(entity); - + for (Entity en:entitySet.getEntities()) { + en.setType(EntityTypeProvider.nameETCompComp.getFullQualifiedNameAsString()); + } return entitySet; } @@ -571,7 +610,9 @@ public class DataCreator { .addProperty(createPrimitive(DataProvider.MEDIA_PROPERTY_NAME, createImage("black"))); entity.setMediaContentType("image/svg+xml"); entitySet.getEntities().add(entity); - + for (Entity en:entitySet.getEntities()) { + en.setType(EntityTypeProvider.nameETMedia.getFullQualifiedNameAsString()); + } return entitySet; } @@ -677,22 +718,23 @@ public class DataCreator { return new PropertyImpl(null, name, ValueType.COLLECTION_PRIMITIVE, Arrays.asList(values)); } - protected static Property createComplex(final String name, final Property... properties) { + protected static Property createComplex(final String name, String type, final Property... properties) { ComplexValue complexValue = new ComplexValueImpl(); for (final Property property : properties) { complexValue.getValue().add(property); } - return new PropertyImpl(null, name, ValueType.COMPLEX, complexValue); + return new PropertyImpl(type, name, ValueType.COMPLEX, complexValue); } - protected static Property createComplexCollection(final String name, final List... propertiesList) { + protected static Property createComplexCollection(final String name, String type, + final List... propertiesList) { List complexCollection = new ArrayList(); for (final List properties : propertiesList) { ComplexValue complexValue = new ComplexValueImpl(); complexValue.getValue().addAll(properties); complexCollection.add(complexValue); } - return new PropertyImpl(null, name, ValueType.COLLECTION_COMPLEX, complexCollection); + return new PropertyImpl(type, name, ValueType.COLLECTION_COMPLEX, complexCollection); } private Calendar getDateTime(final int year, final int month, final int day, diff --git a/lib/server-tecsvc/src/main/java/org/apache/olingo/server/tecsvc/data/DataProvider.java b/lib/server-tecsvc/src/main/java/org/apache/olingo/server/tecsvc/data/DataProvider.java index fed499fef..4fc9300e6 100644 --- a/lib/server-tecsvc/src/main/java/org/apache/olingo/server/tecsvc/data/DataProvider.java +++ b/lib/server-tecsvc/src/main/java/org/apache/olingo/server/tecsvc/data/DataProvider.java @@ -6,9 +6,9 @@ * 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 @@ -138,7 +138,7 @@ public class DataProvider { final List entities = entitySet.getEntities(); final Map newKey = findFreeComposedKey(entities, edmEntitySet.getEntityType()); final Entity newEntity = new EntityImpl(); - + newEntity.setType(edmEntityType.getFullQualifiedName().getFullQualifiedNameAsString()); for (final String keyName : edmEntityType.getKeyPredicateNames()) { newEntity.addProperty(DataCreator.createPrimitive(keyName, newKey.get(keyName))); } @@ -194,7 +194,8 @@ public class DataProvider { return true; } - private void createProperties(final EdmStructuredType type, List properties) throws DataProviderException { + private void createProperties(final EdmStructuredType type, List properties) + throws DataProviderException { final List keyNames = type instanceof EdmEntityType ? ((EdmEntityType) type).getKeyPredicateNames() : Collections. emptyList(); for (final String propertyName : type.getPropertyNames()) { @@ -204,11 +205,11 @@ public class DataProvider { } } } - - private Property createProperty(final EdmProperty edmProperty, final String propertyName) + + private Property createProperty(final EdmProperty edmProperty, final String propertyName) throws DataProviderException { Property newProperty; - + if (edmProperty.isPrimitive()) { newProperty = edmProperty.isCollection() ? DataCreator.createPrimitiveCollection(propertyName) : @@ -216,17 +217,19 @@ public class DataProvider { } else { if (edmProperty.isCollection()) { @SuppressWarnings("unchecked") - Property newProperty2 = DataCreator.createComplexCollection(propertyName); + Property newProperty2 = DataCreator.createComplexCollection(propertyName, edmProperty + .getType().getFullQualifiedName().getFullQualifiedNameAsString()); newProperty = newProperty2; } else { - newProperty = DataCreator.createComplex(propertyName); + newProperty = DataCreator.createComplex(propertyName, edmProperty.getType() + .getFullQualifiedName().getFullQualifiedNameAsString()); createProperties((EdmComplexType) edmProperty.getType(), newProperty.asComplex().getValue()); } } - + return newProperty; } - + public void update(final String rawBaseUri, final EdmEntitySet edmEntitySet, Entity entity, final Entity changedEntity, final boolean patch, final boolean isInsert) throws DataProviderException { @@ -433,7 +436,7 @@ public class DataProvider { } } - private ComplexValue createComplexValue(final EdmProperty edmProperty, final ComplexValue complexValue, + private ComplexValue createComplexValue(final EdmProperty edmProperty, final ComplexValue complexValue, final boolean patch) throws DataProviderException { final ComplexValueImpl result = new ComplexValueImpl(); final EdmComplexType edmType = (EdmComplexType) edmProperty.getType(); @@ -445,7 +448,7 @@ public class DataProvider { final Property currentProperty = findProperty(propertyName, givenProperties); final Property newProperty = createProperty(innerEdmProperty, propertyName); result.getValue().add(newProperty); - + if (currentProperty != null) { updateProperty(innerEdmProperty, newProperty, currentProperty, patch); } else { @@ -459,7 +462,7 @@ public class DataProvider { } } } - + return result; } diff --git a/lib/server-tecsvc/src/main/java/org/apache/olingo/server/tecsvc/data/FunctionData.java b/lib/server-tecsvc/src/main/java/org/apache/olingo/server/tecsvc/data/FunctionData.java index 5451d5dd4..316a7b0d0 100644 --- a/lib/server-tecsvc/src/main/java/org/apache/olingo/server/tecsvc/data/FunctionData.java +++ b/lib/server-tecsvc/src/main/java/org/apache/olingo/server/tecsvc/data/FunctionData.java @@ -31,6 +31,7 @@ import org.apache.olingo.commons.core.data.EntitySetImpl; import org.apache.olingo.commons.core.edm.primitivetype.EdmPrimitiveTypeFactory; import org.apache.olingo.server.api.uri.UriParameter; import org.apache.olingo.server.tecsvc.data.DataProvider.DataProviderException; +import org.apache.olingo.server.tecsvc.provider.ComplexTypeProvider; public class FunctionData { @@ -80,12 +81,12 @@ public class FunctionData { } else if (name.equals("UFCRTCollString")) { return data.get("ESCollAllPrim").getEntities().get(0).getProperty("CollPropertyString"); } else if (name.equals("UFCRTCTTwoPrim")) { - return DataCreator.createComplex(name, + return DataCreator.createComplex(name, ComplexTypeProvider.nameCTTwoPrim.getFullQualifiedNameAsString(), DataCreator.createPrimitive("PropertyInt16", 16), DataCreator.createPrimitive("PropertyString", "UFCRTCTTwoPrim string value")); } else if (name.equals("UFCRTCTTwoPrimParam")) { try { - return DataCreator.createComplex(name, + return DataCreator.createComplex(name,ComplexTypeProvider.nameCTTwoPrim.getFullQualifiedNameAsString(), DataCreator.createPrimitive("PropertyInt16", EdmPrimitiveTypeFactory.getInstance(EdmPrimitiveTypeKind.Int16).valueOfString( getParameterText("ParameterInt16", parameters), @@ -99,7 +100,7 @@ public class FunctionData { throw new DataProviderException("Error in function " + name + ".", e); } } else if (name.equals("UFCRTCollCTTwoPrim")) { - return DataCreator.createComplexCollection(name, + return DataCreator.createComplexCollection(name,ComplexTypeProvider.nameCTTwoPrim.getFullQualifiedNameAsString(), Arrays.asList(DataCreator.createPrimitive("PropertyInt16", 16), DataCreator.createPrimitive("PropertyString", "Test123")), Arrays.asList(DataCreator.createPrimitive("PropertyInt16", 17), diff --git a/lib/server-tecsvc/src/main/java/org/apache/olingo/server/tecsvc/processor/TechnicalEntityProcessor.java b/lib/server-tecsvc/src/main/java/org/apache/olingo/server/tecsvc/processor/TechnicalEntityProcessor.java index f610fc2bb..841eabe11 100644 --- a/lib/server-tecsvc/src/main/java/org/apache/olingo/server/tecsvc/processor/TechnicalEntityProcessor.java +++ b/lib/server-tecsvc/src/main/java/org/apache/olingo/server/tecsvc/processor/TechnicalEntityProcessor.java @@ -6,9 +6,9 @@ * 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 @@ -38,6 +38,7 @@ import org.apache.olingo.commons.core.data.EntitySetImpl; import org.apache.olingo.server.api.ODataApplicationException; import org.apache.olingo.server.api.ODataRequest; import org.apache.olingo.server.api.ODataResponse; +import org.apache.olingo.server.api.ServiceMetadata; import org.apache.olingo.server.api.deserializer.DeserializerException; import org.apache.olingo.server.api.deserializer.ODataDeserializer; import org.apache.olingo.server.api.processor.ActionEntityCollectionProcessor; @@ -74,8 +75,11 @@ public class TechnicalEntityProcessor extends TechnicalProcessor EntityProcessor, ActionEntityProcessor, MediaEntityProcessor, ActionVoidProcessor { - public TechnicalEntityProcessor(final DataProvider dataProvider) { + private final ServiceMetadata serviceMetadata; + + public TechnicalEntityProcessor(final DataProvider dataProvider, ServiceMetadata serviceMetadata) { super(dataProvider); + this.serviceMetadata = serviceMetadata; } @Override @@ -109,21 +113,24 @@ public class TechnicalEntityProcessor extends TechnicalProcessor entitySet, edmEntitySet, request.getRawRequestUri()); - + // Apply expand system query option final ODataFormat format = ODataFormat.fromContentType(requestedContentType); ODataSerializer serializer = odata.createSerializer(format); final ExpandOption expand = uriInfo.getExpandOption(); final SelectOption select = uriInfo.getSelectOption(); - + // Create a shallow copy of each entity. So the expanded navigation properties can be modified for serialization, // without affecting the data stored in the database. final ExpandSystemQueryOptionHandler expandHandler = new ExpandSystemQueryOptionHandler(); final EntitySet entitySetSerialization = expandHandler.copyEntitySetShallowRekursive(entitySet); expandHandler.applyExpandQueryOptions(entitySetSerialization, edmEntitySet, expand); - + // Serialize - response.setContent(serializer.entityCollection(edmEntityType, entitySetSerialization, + response.setContent(serializer.entityCollection( + this.serviceMetadata, + edmEntityType, + entitySetSerialization, EntityCollectionSerializerOptions.with() .contextURL(format == ODataFormat.JSON_NO_METADATA ? null : getContextUrl(edmEntitySet, edmEntityType, false, expand, select)) @@ -170,17 +177,20 @@ public class TechnicalEntityProcessor extends TechnicalProcessor edmEntitySet.getEntityType(); final Entity entity = readEntity(uriInfo); - + final ODataFormat format = ODataFormat.fromContentType(requestedContentType); ODataSerializer serializer = odata.createSerializer(format); final ExpandOption expand = uriInfo.getExpandOption(); final SelectOption select = uriInfo.getSelectOption(); - + final ExpandSystemQueryOptionHandler expandHandler = new ExpandSystemQueryOptionHandler(); final Entity entitySerialization = expandHandler.copyEntityShallowRekursive(entity); expandHandler.applyExpandQueryOptions(entitySerialization, edmEntitySet, expand); - - response.setContent(serializer.entity(edmEntitySet.getEntityType(), entitySerialization, + + response.setContent(serializer.entity( + this.serviceMetadata, + edmEntitySet.getEntityType(), + entitySerialization, EntitySerializerOptions.with() .contextURL(format == ODataFormat.JSON_NO_METADATA ? null : getContextUrl(edmEntitySet, edmEntityType, true, expand, select)) @@ -233,7 +243,7 @@ public class TechnicalEntityProcessor extends TechnicalProcessor final ODataFormat format = ODataFormat.fromContentType(responseFormat); ODataSerializer serializer = odata.createSerializer(format); - response.setContent(serializer.entity(edmEntityType, entity, + response.setContent(serializer.entity(this.serviceMetadata, edmEntityType, entity, EntitySerializerOptions.with() .contextURL(format == ODataFormat.JSON_NO_METADATA ? null : getContextUrl(edmEntitySet, edmEntityType, true, null, null)) diff --git a/lib/server-tecsvc/src/main/java/org/apache/olingo/server/tecsvc/processor/TechnicalPrimitiveComplexProcessor.java b/lib/server-tecsvc/src/main/java/org/apache/olingo/server/tecsvc/processor/TechnicalPrimitiveComplexProcessor.java index e36dc6b7c..b853e488f 100644 --- a/lib/server-tecsvc/src/main/java/org/apache/olingo/server/tecsvc/processor/TechnicalPrimitiveComplexProcessor.java +++ b/lib/server-tecsvc/src/main/java/org/apache/olingo/server/tecsvc/processor/TechnicalPrimitiveComplexProcessor.java @@ -6,9 +6,9 @@ * 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 @@ -44,6 +44,7 @@ import org.apache.olingo.commons.core.edm.primitivetype.EdmPrimitiveTypeFactory; import org.apache.olingo.server.api.ODataApplicationException; import org.apache.olingo.server.api.ODataRequest; import org.apache.olingo.server.api.ODataResponse; +import org.apache.olingo.server.api.ServiceMetadata; import org.apache.olingo.server.api.deserializer.DeserializerException; import org.apache.olingo.server.api.processor.ActionComplexCollectionProcessor; import org.apache.olingo.server.api.processor.ActionComplexProcessor; @@ -81,8 +82,12 @@ public class TechnicalPrimitiveComplexProcessor extends TechnicalProcessor ComplexProcessor, ActionComplexProcessor, ComplexCollectionProcessor, ActionComplexCollectionProcessor { - public TechnicalPrimitiveComplexProcessor(final DataProvider dataProvider) { + private final ServiceMetadata serviceMetadata; + + public TechnicalPrimitiveComplexProcessor(final DataProvider dataProvider, + ServiceMetadata serviceMetadata) { super(dataProvider); + this.serviceMetadata = serviceMetadata; } @Override @@ -246,7 +251,7 @@ public class TechnicalPrimitiveComplexProcessor extends TechnicalProcessor .build())); break; case COMPLEX: - response.setContent(serializer.complex((EdmComplexType) type, property, + response.setContent(serializer.complex(this.serviceMetadata,(EdmComplexType) type, property, ComplexSerializerOptions.with().contextURL(contextURL) .expand(expand).select(select) .build())); @@ -262,7 +267,7 @@ public class TechnicalPrimitiveComplexProcessor extends TechnicalProcessor .build())); break; case COLLECTION_COMPLEX: - response.setContent(serializer.complexCollection((EdmComplexType) type, property, + response.setContent(serializer.complexCollection(this.serviceMetadata, (EdmComplexType) type, property, ComplexSerializerOptions.with().contextURL(contextURL) .expand(expand).select(select) .build())); diff --git a/lib/server-tecsvc/src/test/java/org/apache/olingo/server/tecsvc/data/DataProviderTest.java b/lib/server-tecsvc/src/test/java/org/apache/olingo/server/tecsvc/data/DataProviderTest.java index 76edb3333..4549a0349 100644 --- a/lib/server-tecsvc/src/test/java/org/apache/olingo/server/tecsvc/data/DataProviderTest.java +++ b/lib/server-tecsvc/src/test/java/org/apache/olingo/server/tecsvc/data/DataProviderTest.java @@ -22,9 +22,9 @@ import java.util.Arrays; import java.util.Collections; import java.util.List; +import org.apache.olingo.commons.api.data.ComplexValue; import org.apache.olingo.commons.api.data.Entity; import org.apache.olingo.commons.api.data.EntitySet; -import org.apache.olingo.commons.api.data.ComplexValue; import org.apache.olingo.commons.api.data.Property; import org.apache.olingo.commons.api.edm.Edm; import org.apache.olingo.commons.api.edm.EdmEntityContainer; diff --git a/lib/server-test/src/test/java/org/apache/olingo/server/core/deserializer/json/ODataJsonDeserializerEntityTest.java b/lib/server-test/src/test/java/org/apache/olingo/server/core/deserializer/json/ODataJsonDeserializerEntityTest.java index cd421f43e..870f7c3bb 100644 --- a/lib/server-test/src/test/java/org/apache/olingo/server/core/deserializer/json/ODataJsonDeserializerEntityTest.java +++ b/lib/server-test/src/test/java/org/apache/olingo/server/core/deserializer/json/ODataJsonDeserializerEntityTest.java @@ -6,9 +6,9 @@ * 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 diff --git a/lib/server-test/src/test/java/org/apache/olingo/server/core/serializer/json/ODataJsonSerializerTest.java b/lib/server-test/src/test/java/org/apache/olingo/server/core/serializer/json/ODataJsonSerializerTest.java index 038c6686b..ee3868473 100644 --- a/lib/server-test/src/test/java/org/apache/olingo/server/core/serializer/json/ODataJsonSerializerTest.java +++ b/lib/server-test/src/test/java/org/apache/olingo/server/core/serializer/json/ODataJsonSerializerTest.java @@ -41,11 +41,12 @@ import org.apache.olingo.commons.api.edm.FullQualifiedName; import org.apache.olingo.commons.api.format.ODataFormat; import org.apache.olingo.commons.core.data.PropertyImpl; import org.apache.olingo.server.api.OData; +import org.apache.olingo.server.api.ServiceMetadata; import org.apache.olingo.server.api.edmx.EdmxReference; import org.apache.olingo.server.api.serializer.ComplexSerializerOptions; import org.apache.olingo.server.api.serializer.EntityCollectionSerializerOptions; -import org.apache.olingo.server.api.serializer.ODataSerializer; import org.apache.olingo.server.api.serializer.EntitySerializerOptions; +import org.apache.olingo.server.api.serializer.ODataSerializer; import org.apache.olingo.server.api.serializer.PrimitiveSerializerOptions; import org.apache.olingo.server.api.serializer.SerializerException; import org.apache.olingo.server.api.uri.UriHelper; @@ -64,9 +65,9 @@ import org.junit.Test; import org.mockito.Mockito; public class ODataJsonSerializerTest { - - private static final Edm edm = OData.newInstance().createServiceMetadata( - new EdmTechProvider(), Collections. emptyList()).getEdm(); + private static final ServiceMetadata metadata = OData.newInstance().createServiceMetadata( + new EdmTechProvider(), Collections. emptyList()); + private static final Edm edm = metadata.getEdm(); private static final EdmEntityContainer entityContainer = edm.getEntityContainer( new FullQualifiedName("olingo.odata.test1", "Container")); private final DataProvider data = new DataProvider(); @@ -77,7 +78,7 @@ public class ODataJsonSerializerTest { public void entitySimple() throws Exception { final EdmEntitySet edmEntitySet = entityContainer.getEntitySet("ESAllPrim"); final Entity entity = data.readAll(edmEntitySet).getEntities().get(0); - InputStream result = serializer.entity(edmEntitySet.getEntityType(), entity, + InputStream result = serializer.entity(metadata, edmEntitySet.getEntityType(), entity, EntitySerializerOptions.with() .contextURL(ContextURL.with().entitySet(edmEntitySet).suffix(Suffix.ENTITY).build()) .build()); @@ -109,7 +110,8 @@ public class ODataJsonSerializerTest { final EdmEntitySet edmEntitySet = entityContainer.getEntitySet("ESAllPrim"); Entity entity = data.readAll(edmEntitySet).getEntities().get(0); entity.getProperties().retainAll(Arrays.asList(entity.getProperties().get(0))); - final String resultString = IOUtils.toString(serializer.entity(edmEntitySet.getEntityType(), entity, + final String resultString = IOUtils.toString(serializer.entity(metadata, edmEntitySet.getEntityType(), + entity, EntitySerializerOptions.with() .contextURL(ContextURL.with().entitySet(edmEntitySet).suffix(Suffix.ENTITY).build()) .build())); @@ -130,7 +132,7 @@ public class ODataJsonSerializerTest { final EdmEntitySet edmEntitySet = entityContainer.getEntitySet("ESAllPrim"); Entity entity = data.readAll(edmEntitySet).getEntities().get(0); entity.getProperties().clear(); - serializer.entity(edmEntitySet.getEntityType(), entity, + serializer.entity(metadata, edmEntitySet.getEntityType(), entity, EntitySerializerOptions.with() .contextURL(ContextURL.with().entitySet(edmEntitySet).suffix(Suffix.ENTITY).build()) .build()); @@ -142,7 +144,7 @@ public class ODataJsonSerializerTest { Entity entity = data.readAll(edmEntitySet).getEntities().get(0); entity.getProperties().get(0).setValue(ValueType.PRIMITIVE, false); try { - serializer.entity(edmEntitySet.getEntityType(), entity, + serializer.entity(metadata, edmEntitySet.getEntityType(), entity, EntitySerializerOptions.with() .contextURL(ContextURL.with().entitySet(edmEntitySet).suffix(Suffix.ENTITY).build()) .build()); @@ -163,7 +165,7 @@ public class ODataJsonSerializerTest { entitySet.setNext(URI.create("/next")); CountOption countOption = Mockito.mock(CountOption.class); Mockito.when(countOption.getValue()).thenReturn(true); - InputStream result = serializer.entityCollection(edmEntitySet.getEntityType(), entitySet, + InputStream result = serializer.entityCollection(metadata, edmEntitySet.getEntityType(), entitySet, EntityCollectionSerializerOptions.with() .contextURL(ContextURL.with().entitySet(edmEntitySet).build()) .count(countOption) @@ -188,7 +190,7 @@ public class ODataJsonSerializerTest { public void entityCollAllPrim() throws Exception { final EdmEntitySet edmEntitySet = entityContainer.getEntitySet("ESCollAllPrim"); final Entity entity = data.readAll(edmEntitySet).getEntities().get(0); - InputStream result = serializer.entity(edmEntitySet.getEntityType(), entity, + InputStream result = serializer.entity(metadata, edmEntitySet.getEntityType(), entity, EntitySerializerOptions.with() .contextURL(ContextURL.with().serviceRoot(URI.create("http://host/service/")) .entitySet(edmEntitySet).suffix(Suffix.ENTITY).build()) @@ -224,7 +226,7 @@ public class ODataJsonSerializerTest { public void entityCompAllPrim() throws Exception { final EdmEntitySet edmEntitySet = entityContainer.getEntitySet("ESCompAllPrim"); final Entity entity = data.readAll(edmEntitySet).getEntities().get(0); - InputStream result = serializer.entity(edmEntitySet.getEntityType(), entity, + InputStream result = serializer.entity(metadata, edmEntitySet.getEntityType(), entity, EntitySerializerOptions.with() .contextURL(ContextURL.with().entitySet(edmEntitySet).suffix(Suffix.ENTITY).build()) .build()); @@ -257,7 +259,7 @@ public class ODataJsonSerializerTest { public void entityMixPrimCollComp() throws Exception { final EdmEntitySet edmEntitySet = entityContainer.getEntitySet("ESMixPrimCollComp"); final Entity entity = data.readAll(edmEntitySet).getEntities().get(0); - InputStream result = serializer.entity(edmEntitySet.getEntityType(), entity, + InputStream result = serializer.entity(metadata, edmEntitySet.getEntityType(), entity, EntitySerializerOptions.with() .contextURL(ContextURL.with().entitySet(edmEntitySet).suffix(Suffix.ENTITY).build()) .build()); @@ -280,7 +282,7 @@ public class ODataJsonSerializerTest { final EdmEntitySet edmEntitySet = entityContainer.getEntitySet("ESMixPrimCollComp"); Entity entity = data.readAll(edmEntitySet).getEntities().get(0); entity.getProperties().retainAll(Arrays.asList(entity.getProperties().get(0))); - final String resultString = IOUtils.toString(serializer.entity(edmEntitySet.getEntityType(), entity, + final String resultString = IOUtils.toString(serializer.entity(metadata, edmEntitySet.getEntityType(), entity, EntitySerializerOptions.with() .contextURL(ContextURL.with().entitySet(edmEntitySet).suffix(Suffix.ENTITY).build()) .build())); @@ -295,7 +297,7 @@ public class ODataJsonSerializerTest { final EdmEntitySet edmEntitySet = entityContainer.getEntitySet("ESTwoPrim"); final Entity entity = data.readAll(edmEntitySet).getEntities().get(0); InputStream result = new ODataJsonSerializer(ODataFormat.JSON_NO_METADATA) - .entity(edmEntitySet.getEntityType(), entity, null); + .entity(metadata, edmEntitySet.getEntityType(), entity, null); final String resultString = IOUtils.toString(result); final String expectedResult = "{\"PropertyInt16\":32766,\"PropertyString\":\"Test String1\"}"; Assert.assertEquals(expectedResult, resultString); @@ -306,7 +308,7 @@ public class ODataJsonSerializerTest { final EdmEntitySet edmEntitySet = entityContainer.getEntitySet("ESTwoPrim"); final EntitySet entitySet = data.readAll(edmEntitySet); InputStream result = new ODataJsonSerializer(ODataFormat.JSON_NO_METADATA) - .entityCollection(edmEntitySet.getEntityType(), entitySet, + .entityCollection(metadata, edmEntitySet.getEntityType(), entitySet, EntityCollectionSerializerOptions.with() .contextURL(ContextURL.with().entitySet(edmEntitySet).build()).build()); final String resultString = IOUtils.toString(result); @@ -323,7 +325,8 @@ public class ODataJsonSerializerTest { final EdmEntitySet edmEntitySet = entityContainer.getEntitySet("ESMedia"); Entity entity = data.readAll(edmEntitySet).getEntities().get(0); entity.setMediaETag("theMediaETag"); - final String resultString = IOUtils.toString(serializer.entity(edmEntitySet.getEntityType(), entity, + final String resultString = IOUtils.toString(serializer.entity(metadata, edmEntitySet.getEntityType(), + entity, EntitySerializerOptions.with() .contextURL(ContextURL.with().entitySet(edmEntitySet).suffix(Suffix.ENTITY).build()) .build())); @@ -337,7 +340,8 @@ public class ODataJsonSerializerTest { public void entitySetMedia() throws Exception { final EdmEntitySet edmEntitySet = entityContainer.getEntitySet("ESMedia"); final EntitySet entitySet = data.readAll(edmEntitySet); - final String resultString = IOUtils.toString(serializer.entityCollection(edmEntitySet.getEntityType(), entitySet, + final String resultString = IOUtils.toString(serializer.entityCollection(metadata, + edmEntitySet.getEntityType(), entitySet, EntityCollectionSerializerOptions.with() .contextURL(ContextURL.with().entitySet(edmEntitySet).build()).build())); final String expectedResult = "{\"@odata.context\":\"$metadata#ESMedia\",\"value\":[" @@ -358,7 +362,7 @@ public class ODataJsonSerializerTest { final SelectOption select = ExpandSelectMock.mockSelectOption(Arrays.asList( selectItem1, selectItem2, selectItem2)); InputStream result = serializer - .entity(entityType, entity, + .entity(metadata, entityType, entity, EntitySerializerOptions.with() .contextURL(ContextURL.with().entitySet(edmEntitySet) .selectList(helper.buildContextURLSelectList(entityType, null, select)) @@ -380,7 +384,7 @@ public class ODataJsonSerializerTest { SelectItem selectItem2 = Mockito.mock(SelectItem.class); Mockito.when(selectItem2.isStar()).thenReturn(true); final SelectOption select = ExpandSelectMock.mockSelectOption(Arrays.asList(selectItem1, selectItem2)); - InputStream result = serializer.entity(edmEntitySet.getEntityType(), entity, + InputStream result = serializer.entity(metadata, edmEntitySet.getEntityType(), entity, EntitySerializerOptions.with() .contextURL(ContextURL.with().entitySet(edmEntitySet).suffix(Suffix.ENTITY).build()) .select(select) @@ -399,7 +403,7 @@ public class ODataJsonSerializerTest { final SelectOption select = ExpandSelectMock.mockSelectOption(Arrays.asList( ExpandSelectMock.mockSelectItem(edmEntitySet, "PropertyComp", "PropertyComp", "PropertyString"))); InputStream result = serializer - .entityCollection(entityType, entitySet, + .entityCollection(metadata, entityType, entitySet, EntityCollectionSerializerOptions.with() .contextURL(ContextURL.with().entitySet(edmEntitySet) .selectList(helper.buildContextURLSelectList(entityType, null, select)) @@ -424,7 +428,7 @@ public class ODataJsonSerializerTest { ExpandSelectMock.mockSelectItem(edmEntitySet, "PropertyComp", "PropertyComp", "PropertyString"), ExpandSelectMock.mockSelectItem(edmEntitySet, "PropertyComp", "PropertyComp"))); final String resultString = IOUtils.toString(serializer - .entityCollection(entityType, entitySet, + .entityCollection(metadata, entityType, entitySet, EntityCollectionSerializerOptions.with() .contextURL(ContextURL.with().entitySet(edmEntitySet) .selectList(helper.buildContextURLSelectList(entityType, null, select)) @@ -445,7 +449,7 @@ public class ODataJsonSerializerTest { final Entity entity = data.readAll(edmEntitySet).getEntities().get(3); final ExpandOption expand = ExpandSelectMock.mockExpandOption(Arrays.asList( ExpandSelectMock.mockExpandItem(edmEntitySet, "NavPropertyETAllPrimOne"))); - InputStream result = serializer.entity(edmEntitySet.getEntityType(), entity, + InputStream result = serializer.entity(metadata, edmEntitySet.getEntityType(), entity, EntitySerializerOptions.with() .contextURL(ContextURL.with().entitySet(edmEntitySet).suffix(Suffix.ENTITY).build()) .expand(expand) @@ -484,7 +488,7 @@ public class ODataJsonSerializerTest { Mockito.when(expandItem.getSelectOption()).thenReturn(select); final ExpandOption expand = ExpandSelectMock.mockExpandOption(Arrays.asList(expandItem)); final String resultString = IOUtils.toString(serializer - .entity(entityType, entity, + .entity(metadata, entityType, entity, EntitySerializerOptions.with() .contextURL(ContextURL.with().entitySet(edmEntitySet) .selectList(helper.buildContextURLSelectList(entityType, expand, select)) @@ -511,7 +515,7 @@ public class ODataJsonSerializerTest { final SelectOption select = ExpandSelectMock.mockSelectOption(Arrays.asList( ExpandSelectMock.mockSelectItem(edmEntitySet, "PropertySByte"))); final String resultString = IOUtils.toString(serializer - .entity(entityType, entity, + .entity(metadata, entityType, entity, EntitySerializerOptions.with() .contextURL(ContextURL.with().entitySet(edmEntitySet) .selectList(helper.buildContextURLSelectList(entityType, expand, select)) @@ -538,7 +542,7 @@ public class ODataJsonSerializerTest { final SelectOption select = ExpandSelectMock.mockSelectOption(Arrays.asList( ExpandSelectMock.mockSelectItem(edmEntitySet, "PropertyTimeOfDay"))); final String resultString = IOUtils.toString(serializer - .entity(entityType, entity, + .entity(metadata, entityType, entity, EntitySerializerOptions.with() .contextURL(ContextURL.with().entitySet(edmEntitySet) .selectList(helper.buildContextURLSelectList(entityType, expand, select)) @@ -569,7 +573,7 @@ public class ODataJsonSerializerTest { Mockito.when(expandItemFirst.getSelectOption()).thenReturn(select); final ExpandOption expand = ExpandSelectMock.mockExpandOption(Arrays.asList(expandItemFirst)); final String resultString = IOUtils.toString(serializer - .entity(entityType, entity, + .entity(metadata, entityType, entity, EntitySerializerOptions.with() .contextURL(ContextURL.with().entitySet(edmEntitySet) .selectList(helper.buildContextURLSelectList(entityType, expand, select)) @@ -646,7 +650,7 @@ public class ODataJsonSerializerTest { final Property property = data.readAll(edmEntitySet).getEntities().get(0).getProperty("PropertyComp"); final String resultString = IOUtils.toString(serializer - .complex((EdmComplexType) edmProperty.getType(), property, + .complex(metadata, (EdmComplexType) edmProperty.getType(), property, ComplexSerializerOptions.with() .contextURL(ContextURL.with() .entitySet(edmEntitySet).keyPath("32767").navOrPropertyPath(edmProperty.getName()) @@ -665,7 +669,7 @@ public class ODataJsonSerializerTest { final Property property = data.readAll(edmEntitySet).getEntities().get(0).getProperty(edmProperty.getName()); final String resultString = IOUtils.toString(serializer - .complexCollection((EdmComplexType) edmProperty.getType(), property, + .complexCollection(metadata, (EdmComplexType) edmProperty.getType(), property, ComplexSerializerOptions.with() .contextURL(ContextURL.with() .entitySet(edmEntitySet).keyPath("32767").navOrPropertyPath(edmProperty.getName()) diff --git a/lib/server-test/src/test/java/org/apache/olingo/server/core/uri/antlr/TestUriParserImpl.java b/lib/server-test/src/test/java/org/apache/olingo/server/core/uri/antlr/TestUriParserImpl.java index 798c5c0a5..86cbf0eb1 100644 --- a/lib/server-test/src/test/java/org/apache/olingo/server/core/uri/antlr/TestUriParserImpl.java +++ b/lib/server-test/src/test/java/org/apache/olingo/server/core/uri/antlr/TestUriParserImpl.java @@ -6,9 +6,9 @@ * 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 @@ -1064,13 +1064,16 @@ public class TestUriParserImpl { public void testAlias() throws Exception { testUri.run("ESAllPrim", "$filter=PropertyInt16 eq @p1&@p1=1)") .goFilter().is("< eq <@p1>>"); - } - + } + @Test public void testLambda() throws Exception { testUri.run("ESTwoKeyNav", "$filter=CollPropertyComp/all( l : true )") .goFilter().is(">>"); + testUri.run("ESTwoKeyNav", "$filter=CollPropertyComp/all( x : x/PropertyInt16 eq 2)") + .goFilter().is(" eq <2>>>>"); + testUri.run("ESTwoKeyNav", "$filter=CollPropertyComp/any( l : true )") .goFilter().is(">>"); testUri.run("ESTwoKeyNav", "$filter=CollPropertyComp/any( )") diff --git a/lib/server-test/src/test/java/org/apache/olingo/server/core/uri/validator/UriValidatorTest.java b/lib/server-test/src/test/java/org/apache/olingo/server/core/uri/validator/UriValidatorTest.java index 1835befd7..db3930e99 100644 --- a/lib/server-test/src/test/java/org/apache/olingo/server/core/uri/validator/UriValidatorTest.java +++ b/lib/server-test/src/test/java/org/apache/olingo/server/core/uri/validator/UriValidatorTest.java @@ -6,9 +6,9 @@ * 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 @@ -18,6 +18,9 @@ */ package org.apache.olingo.server.core.uri.validator; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.fail; + import org.apache.olingo.commons.api.edm.Edm; import org.apache.olingo.commons.api.http.HttpMethod; import org.apache.olingo.commons.core.edm.provider.EdmProviderImpl; @@ -31,9 +34,6 @@ import org.apache.olingo.server.tecsvc.provider.ContainerProvider; import org.apache.olingo.server.tecsvc.provider.EdmTechProvider; import org.junit.Test; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.fail; - public class UriValidatorTest { private static final String URI_ALL = "$all"; @@ -77,7 +77,7 @@ public class UriValidatorTest { private static final String QO_SKIPTOKEN = "$skiptoken=123"; private static final String QO_TOP = "$top=1"; - private String[][] urisWithValidSystemQueryOptions = { + private final String[][] urisWithValidSystemQueryOptions = { { URI_ALL, QO_FILTER }, { URI_ALL, QO_FORMAT }, { URI_ALL, QO_EXPAND }, { URI_ALL, QO_COUNT }, { URI_ALL, QO_ORDERBY }, /* { URI_ALL, QO_SEARCH }, */{ URI_ALL, QO_SELECT }, { URI_ALL, QO_SKIP }, { URI_ALL, QO_SKIPTOKEN }, { URI_ALL, QO_TOP }, @@ -105,7 +105,7 @@ public class UriValidatorTest { { URI_REFERENCES, QO_FILTER }, { URI_REFERENCES, QO_FORMAT }, { URI_REFERENCES, QO_ORDERBY }, /* { URI_REFERENCES, QO_SEARCH }, */{ URI_REFERENCES, QO_SKIP }, { URI_REFERENCES, QO_SKIPTOKEN }, - { URI_REFERENCES, QO_TOP }, + { URI_REFERENCES, QO_TOP }, { URI_REFERENCES, QO_ID }, { URI_REFERENCE, QO_FORMAT }, @@ -160,7 +160,7 @@ public class UriValidatorTest { { ContainerProvider.AIRT_STRING } }; - private String[][] urisWithNonValidSystemQueryOptions = { + private final String[][] urisWithNonValidSystemQueryOptions = { { URI_ALL, QO_ID }, { URI_BATCH, QO_FILTER }, { URI_BATCH, QO_FORMAT }, { URI_BATCH, QO_ID }, { URI_BATCH, QO_EXPAND }, @@ -199,7 +199,7 @@ public class UriValidatorTest { /* { URI_MEDIA_STREAM, QO_SEARCH }, */ { URI_MEDIA_STREAM, QO_SELECT }, { URI_MEDIA_STREAM, QO_SKIP }, { URI_MEDIA_STREAM, QO_SKIPTOKEN }, { URI_MEDIA_STREAM, QO_TOP }, - { URI_REFERENCES, QO_ID }, { URI_REFERENCES, QO_EXPAND }, { URI_REFERENCES, QO_COUNT }, + { URI_REFERENCES, QO_EXPAND }, { URI_REFERENCES, QO_COUNT }, { URI_REFERENCES, QO_SELECT }, { URI_REFERENCE, QO_FILTER }, { URI_REFERENCE, QO_ID }, { URI_REFERENCE, QO_EXPAND }, diff --git a/samples/server/src/main/java/org/apache/olingo/server/sample/data/DataProvider.java b/samples/server/src/main/java/org/apache/olingo/server/sample/data/DataProvider.java index d59d25154..2e44e3596 100644 --- a/samples/server/src/main/java/org/apache/olingo/server/sample/data/DataProvider.java +++ b/samples/server/src/main/java/org/apache/olingo/server/sample/data/DataProvider.java @@ -6,9 +6,9 @@ * 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 @@ -37,10 +37,11 @@ import org.apache.olingo.commons.core.data.EntityImpl; import org.apache.olingo.commons.core.data.EntitySetImpl; import org.apache.olingo.commons.core.data.PropertyImpl; import org.apache.olingo.server.api.uri.UriParameter; +import org.apache.olingo.server.sample.edmprovider.CarsEdmProvider; public class DataProvider { - private Map data; + private final Map data; public DataProvider() { data = new HashMap(); @@ -133,6 +134,9 @@ public class DataProvider { .addProperty(createPrimitive("Price", 167189.00)) .addProperty(createPrimitive("Currency", "EUR"))); + for (Entity entity:entitySet.getEntities()) { + entity.setType(CarsEdmProvider.ET_CAR.getFullQualifiedNameAsString()); + } return entitySet; } @@ -149,6 +153,9 @@ public class DataProvider { .addProperty(createPrimitive("Name", "Horse Powered Racing")) .addProperty(createAddress("Horse Street 1", "Maranello", "41053", "Italy"))); + for (Entity entity:entitySet.getEntities()) { + entity.setType(CarsEdmProvider.ET_MANUFACTURER.getFullQualifiedNameAsString()); + } return entitySet; } diff --git a/samples/server/src/main/java/org/apache/olingo/server/sample/processor/CarsProcessor.java b/samples/server/src/main/java/org/apache/olingo/server/sample/processor/CarsProcessor.java index 891acbbb6..71b827daf 100644 --- a/samples/server/src/main/java/org/apache/olingo/server/sample/processor/CarsProcessor.java +++ b/samples/server/src/main/java/org/apache/olingo/server/sample/processor/CarsProcessor.java @@ -6,9 +6,9 @@ * 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 @@ -74,7 +74,8 @@ public class CarsProcessor implements EntityCollectionProcessor, EntityProcessor PrimitiveProcessor, PrimitiveValueProcessor, ComplexProcessor { private OData odata; - private DataProvider dataProvider; + private final DataProvider dataProvider; + private ServiceMetadata edm; // This constructor is application specific and not mandatory for the Olingo library. We use it here to simulate the // database access @@ -85,6 +86,7 @@ public class CarsProcessor implements EntityCollectionProcessor, EntityProcessor @Override public void init(OData odata, ServiceMetadata edm) { this.odata = odata; + this.edm = edm; } @Override @@ -105,7 +107,7 @@ public class CarsProcessor implements EntityCollectionProcessor, EntityProcessor // Now the content is serialized using the serializer. final ExpandOption expand = uriInfo.getExpandOption(); final SelectOption select = uriInfo.getSelectOption(); - InputStream serializedContent = serializer.entityCollection(edmEntitySet.getEntityType(), entitySet, + InputStream serializedContent = serializer.entityCollection(edm, edmEntitySet.getEntityType(), entitySet, EntityCollectionSerializerOptions.with() .contextURL(format == ODataFormat.JSON_NO_METADATA ? null : getContextUrl(edmEntitySet, false, expand, select, null)) @@ -143,7 +145,7 @@ public class CarsProcessor implements EntityCollectionProcessor, EntityProcessor ODataSerializer serializer = odata.createSerializer(format); final ExpandOption expand = uriInfo.getExpandOption(); final SelectOption select = uriInfo.getSelectOption(); - InputStream serializedContent = serializer.entity(edmEntitySet.getEntityType(), entity, + InputStream serializedContent = serializer.entity(edm, edmEntitySet.getEntityType(), entity, EntitySerializerOptions.with() .contextURL(format == ODataFormat.JSON_NO_METADATA ? null : getContextUrl(edmEntitySet, true, expand, select, null)) @@ -256,7 +258,7 @@ public class CarsProcessor implements EntityCollectionProcessor, EntityProcessor final ContextURL contextURL = format == ODataFormat.JSON_NO_METADATA ? null : getContextUrl(edmEntitySet, true, null, null, edmProperty.getName()); InputStream serializerContent = complex ? - serializer.complex((EdmComplexType) edmProperty.getType(), property, + serializer.complex(edm, (EdmComplexType) edmProperty.getType(), property, ComplexSerializerOptions.with().contextURL(contextURL).build()) : serializer.primitive((EdmPrimitiveType) edmProperty.getType(), property, PrimitiveSerializerOptions.with() @@ -273,7 +275,7 @@ public class CarsProcessor implements EntityCollectionProcessor, EntityProcessor } } } - + private Entity readEntityInternal(final UriInfoResource uriInfo, final EdmEntitySet entitySet) throws DataProvider.DataProviderException { // This method will extract the key values and pass them to the data provider