[OLINGO-663] conditional GET in technical service, part 1 Change-Id: I89b0e765e1d3618c318522d1c2a22988f9d017fd
Signed-off-by: Christian Amend <[email protected]> Project: http://git-wip-us.apache.org/repos/asf/olingo-odata4/repo Commit: http://git-wip-us.apache.org/repos/asf/olingo-odata4/commit/db47760e Tree: http://git-wip-us.apache.org/repos/asf/olingo-odata4/tree/db47760e Diff: http://git-wip-us.apache.org/repos/asf/olingo-odata4/diff/db47760e Branch: refs/heads/OLINGO-632_OSGi-Support Commit: db47760e355be674569c1f15a7aeaa67614f4970 Parents: 6ff644d Author: Klaus Straubinger <[email protected]> Authored: Fri May 22 15:48:43 2015 +0200 Committer: Christian Amend <[email protected]> Committed: Fri May 22 15:55:49 2015 +0200 ---------------------------------------------------------------------- .../fit/tecsvc/client/ConditionalITCase.java | 80 +++++++++++ .../olingo/server/api/EtagInformation.java | 72 ++++++++++ .../org/apache/olingo/server/api/OData.java | 15 ++- .../apache/olingo/server/core/EtagParser.java | 86 ++++++++++++ .../apache/olingo/server/core/ODataImpl.java | 11 ++ .../olingo/server/core/EtagParserTest.java | 131 +++++++++++++++++++ .../processor/TechnicalEntityProcessor.java | 14 ++ 7 files changed, 407 insertions(+), 2 deletions(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/olingo-odata4/blob/db47760e/fit/src/test/java/org/apache/olingo/fit/tecsvc/client/ConditionalITCase.java ---------------------------------------------------------------------- diff --git a/fit/src/test/java/org/apache/olingo/fit/tecsvc/client/ConditionalITCase.java b/fit/src/test/java/org/apache/olingo/fit/tecsvc/client/ConditionalITCase.java new file mode 100644 index 0000000..2dcbc26 --- /dev/null +++ b/fit/src/test/java/org/apache/olingo/fit/tecsvc/client/ConditionalITCase.java @@ -0,0 +1,80 @@ +/* + * 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.fit.tecsvc.client; + +import static org.hamcrest.CoreMatchers.containsString; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertThat; +import static org.junit.Assert.fail; + +import org.apache.olingo.client.api.ODataClient; +import org.apache.olingo.client.api.communication.ODataClientErrorException; +import org.apache.olingo.client.api.communication.request.retrieve.ODataEntityRequest; +import org.apache.olingo.client.api.communication.response.ODataRetrieveResponse; +import org.apache.olingo.client.api.domain.ClientEntity; +import org.apache.olingo.client.core.ODataClientFactory; +import org.apache.olingo.commons.api.ODataError; +import org.apache.olingo.commons.api.format.ODataFormat; +import org.apache.olingo.commons.api.http.HttpStatusCode; +import org.apache.olingo.fit.AbstractBaseTestITCase; +import org.apache.olingo.fit.tecsvc.TecSvcConst; +import org.junit.Test; + +public final class ConditionalITCase extends AbstractBaseTestITCase { + + @Test + public void readWithWrongIfMatch() throws Exception { + final ODataClient client = getClient(); + ODataEntityRequest<ClientEntity> request = client.getRetrieveRequestFactory().getEntityRequest( + client.newURIBuilder(TecSvcConst.BASE_URI) + .appendEntitySetSegment("ESCompAllPrim").appendKeySegment(0).build()); + request.setIfMatch("W/\"1\""); + assertNotNull(request); + + try { + request.execute(); + fail("Expected Exception not thrown!"); + } catch (final ODataClientErrorException e) { + assertEquals(HttpStatusCode.PRECONDITION_FAILED.getStatusCode(), e.getStatusLine().getStatusCode()); + final ODataError error = e.getODataError(); + assertThat(error.getMessage(), containsString("condition")); + } + } + + @Test + public void readNotModified() throws Exception { + final ODataClient client = getClient(); + ODataEntityRequest<ClientEntity> request = client.getRetrieveRequestFactory().getEntityRequest( + client.newURIBuilder(TecSvcConst.BASE_URI) + .appendEntitySetSegment("ESCompAllPrim").appendKeySegment(0).build()); + request.setIfNoneMatch("W/\"0\""); + assertNotNull(request); + + final ODataRetrieveResponse<ClientEntity> response = request.execute(); + assertEquals(HttpStatusCode.NOT_MODIFIED.getStatusCode(), response.getStatusCode()); + } + + @Override + protected ODataClient getClient() { + ODataClient odata = ODataClientFactory.getClient(); + odata.getConfiguration().setDefaultPubFormat(ODataFormat.JSON); + return odata; + } +} http://git-wip-us.apache.org/repos/asf/olingo-odata4/blob/db47760e/lib/server-api/src/main/java/org/apache/olingo/server/api/EtagInformation.java ---------------------------------------------------------------------- diff --git a/lib/server-api/src/main/java/org/apache/olingo/server/api/EtagInformation.java b/lib/server-api/src/main/java/org/apache/olingo/server/api/EtagInformation.java new file mode 100644 index 0000000..d6be3fa --- /dev/null +++ b/lib/server-api/src/main/java/org/apache/olingo/server/api/EtagInformation.java @@ -0,0 +1,72 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.olingo.server.api; + +import java.util.Collection; + +/** + * Information about the values of an ETag-relevant HTTP header. + */ +public class EtagInformation { + private final boolean all; + private final Collection<String> etags; + + public EtagInformation(final boolean all, final Collection<String> etags) { + this.all = all; + this.etags = etags; + } + + /** + * Gets the information whether the values contain "*". + */ + public boolean isAll() { + return all; + } + + /** + * Gets the collection of ETag values found. + * It is empty if {@link #isAll()} returns <code>true</code>. + */ + public Collection<String> getEtags() { + return etags; + } + + /** + * <p>Checks whether a given ETag value is matched by this ETag information.</p> + * <p>If the given value is <code>null</code>, or if this ETag information + * does not contain anything, the result is <code>false</code>.</p> + * @param etag the ETag value to match + * @return a boolean match result + */ + public boolean isMatchedBy(final String etag) { + if (etag == null) { + return false; + } else if (all) { + return true; + } else { + for (final String candidate : etags) { + if ((etag.startsWith("W/") ? etag.substring(2) : etag) + .equals(candidate.startsWith("W/") ? candidate.substring(2) : candidate)) { + return true; + } + } + return false; + } + } +} http://git-wip-us.apache.org/repos/asf/olingo-odata4/blob/db47760e/lib/server-api/src/main/java/org/apache/olingo/server/api/OData.java ---------------------------------------------------------------------- diff --git a/lib/server-api/src/main/java/org/apache/olingo/server/api/OData.java b/lib/server-api/src/main/java/org/apache/olingo/server/api/OData.java index 43cd3d1..1acd163 100644 --- a/lib/server-api/src/main/java/org/apache/olingo/server/api/OData.java +++ b/lib/server-api/src/main/java/org/apache/olingo/server/api/OData.java @@ -18,6 +18,7 @@ */ package org.apache.olingo.server.api; +import java.util.Collection; import java.util.List; import org.apache.olingo.commons.api.ODataRuntimeException; @@ -110,8 +111,18 @@ public abstract class OData { public abstract ODataDeserializer createDeserializer(ODataFormat format) throws DeserializerException; /** - * @param kind - * @return a {@link EdmPrimitiveType} instance for the type kind + * Creates a primitive-type instance. + * @param kind the kind of the primitive type + * @return an {@link EdmPrimitiveType} instance for the type kind */ public abstract EdmPrimitiveType createPrimitiveTypeInstance(EdmPrimitiveTypeKind kind); + + /** + * Creates Etag information from the values of a HTTP header + * containing a list of entity tags or a single star character, i.e., + * <code>If-Match</code> and <code>If-None-Match</code>. + * @param values the collection of header values + * @return an {@link EtagInformation} instance + */ + public abstract EtagInformation createEtagInformation(final Collection<String> values); } http://git-wip-us.apache.org/repos/asf/olingo-odata4/blob/db47760e/lib/server-core/src/main/java/org/apache/olingo/server/core/EtagParser.java ---------------------------------------------------------------------- diff --git a/lib/server-core/src/main/java/org/apache/olingo/server/core/EtagParser.java b/lib/server-core/src/main/java/org/apache/olingo/server/core/EtagParser.java new file mode 100644 index 0000000..f5477c0 --- /dev/null +++ b/lib/server-core/src/main/java/org/apache/olingo/server/core/EtagParser.java @@ -0,0 +1,86 @@ +/* + * 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.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * <p>Parses the values of HTTP header fields that contain a list of entity tags or a + * single star character, i.e., <code>If-Match</code> and <code>If-None-Match</code>.</p> + * <p>See <a href="https://www.ietf.org/rfc/rfc7232.txt">RFC 7232</a> for details; + * there the following grammar is defined:</p> + * <pre> + * If-Match = "*" / 1#entity-tag + * If-None-Match = "*" / 1#entity-tag + * entity-tag = [ weak ] opaque-tag + * weak = %x57.2F ; "W/", case-sensitive + * opaque-tag = DQUOTE *etagc DQUOTE + * etagc = %x21 / %x23-7E / %x80-FF + * </pre> + * <p>Values with illegal syntax do not contribute to the result but no exception is thrown.</p> + */ +public class EtagParser { + + private static final Pattern ETAG = Pattern.compile("\\s*(,\\s*)+|((?:W/)?\"[!#-~\\x80-\\xFF]*\")"); + + protected static Collection<String> parse(final Collection<String> values) { + if (values == null) { + return Collections.<String> emptySet(); + } + + Set<String> result = new HashSet<String>(); + for (final String value : values) { + final Collection<String> part = parse(value); + if (part.size() == 1 && part.iterator().next().equals("*")) { + return part; + } else { + result.addAll(part); + } + } + return result; + } + + private static Collection<String> parse(final String value) { + if (value.trim().equals("*")) { + return Collections.singleton("*"); + } else { + Set<String> result = new HashSet<String>(); + String separator = ""; + int start = 0; + Matcher matcher = ETAG.matcher(value.trim()); + while (matcher.find() && matcher.start() == start) { + start = matcher.end(); + if (matcher.group(1) != null) { + separator = matcher.group(1); + } else if (separator != null) { + result.add(matcher.group(2)); + separator = null; + } else { + return Collections.<String> emptySet(); + } + } + return matcher.hitEnd() ? result : Collections.<String> emptySet(); + } + } +} http://git-wip-us.apache.org/repos/asf/olingo-odata4/blob/db47760e/lib/server-core/src/main/java/org/apache/olingo/server/core/ODataImpl.java ---------------------------------------------------------------------- diff --git a/lib/server-core/src/main/java/org/apache/olingo/server/core/ODataImpl.java b/lib/server-core/src/main/java/org/apache/olingo/server/core/ODataImpl.java index ea936fd..3b13717 100644 --- a/lib/server-core/src/main/java/org/apache/olingo/server/core/ODataImpl.java +++ b/lib/server-core/src/main/java/org/apache/olingo/server/core/ODataImpl.java @@ -18,6 +18,8 @@ */ package org.apache.olingo.server.core; +import java.util.Collection; +import java.util.Collections; import java.util.List; import org.apache.olingo.commons.api.edm.EdmPrimitiveType; @@ -25,6 +27,7 @@ import org.apache.olingo.commons.api.edm.EdmPrimitiveTypeKind; import org.apache.olingo.commons.api.edm.provider.CsdlEdmProvider; import org.apache.olingo.commons.api.format.ODataFormat; import org.apache.olingo.commons.core.edm.primitivetype.EdmPrimitiveTypeFactory; +import org.apache.olingo.server.api.EtagInformation; import org.apache.olingo.server.api.OData; import org.apache.olingo.server.api.ODataHttpHandler; import org.apache.olingo.server.api.ServiceMetadata; @@ -114,4 +117,12 @@ public class ODataImpl extends OData { public EdmPrimitiveType createPrimitiveTypeInstance(final EdmPrimitiveTypeKind kind) { return EdmPrimitiveTypeFactory.getInstance(kind); } + + @Override + public EtagInformation createEtagInformation(final Collection<String> values) { + final Collection<String> etags = EtagParser.parse(values); + final boolean isAll = etags.size() == 1 && etags.iterator().next().equals("*"); + return new EtagInformation(isAll, + isAll ? Collections.<String> emptySet() : Collections.unmodifiableCollection(etags)); + } } http://git-wip-us.apache.org/repos/asf/olingo-odata4/blob/db47760e/lib/server-core/src/test/java/org/apache/olingo/server/core/EtagParserTest.java ---------------------------------------------------------------------- diff --git a/lib/server-core/src/test/java/org/apache/olingo/server/core/EtagParserTest.java b/lib/server-core/src/test/java/org/apache/olingo/server/core/EtagParserTest.java new file mode 100644 index 0000000..51c89d3 --- /dev/null +++ b/lib/server-core/src/test/java/org/apache/olingo/server/core/EtagParserTest.java @@ -0,0 +1,131 @@ +/* + * 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.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.CoreMatchers.hasItems; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertThat; +import static org.junit.Assert.assertTrue; + +import java.util.Arrays; +import java.util.Collections; + +import org.apache.olingo.server.api.EtagInformation; +import org.apache.olingo.server.api.OData; +import org.junit.Test; + +public class EtagParserTest { + + private static final OData odata = OData.newInstance(); + + @Test + public void empty() { + final EtagInformation etagInformation = odata.createEtagInformation(null); + assertFalse(etagInformation.isAll()); + assertNotNull(etagInformation.getEtags()); + assertTrue(etagInformation.getEtags().isEmpty()); + } + + @Test + public void loneStar() { + final EtagInformation etagInformation = odata.createEtagInformation(Collections.singleton("*")); + assertTrue(etagInformation.isAll()); + assertNotNull(etagInformation.getEtags()); + assertTrue(etagInformation.getEtags().isEmpty()); + } + + @Test + public void starWins() { + final EtagInformation etagInformation = odata.createEtagInformation(Arrays.asList("\"ETag\"", "*")); + assertTrue(etagInformation.isAll()); + assertNotNull(etagInformation.getEtags()); + assertTrue(etagInformation.getEtags().isEmpty()); + } + + @Test + public void starAsEtagAndEmptyEtag() { + final EtagInformation etagInformation = odata.createEtagInformation( + Collections.singleton("\"*\", \"\"")); + assertFalse(etagInformation.isAll()); + assertNotNull(etagInformation.getEtags()); + assertThat(etagInformation.getEtags().size(), equalTo(2)); + assertThat(etagInformation.getEtags(), hasItems("\"*\"", "\"\"")); + } + + @Test + public void severalEtags() { + final EtagInformation etagInformation = odata.createEtagInformation( + Arrays.asList("\"ETag1\"", "\"ETag2\",, , ,W/\"ETag3\", ,")); + assertFalse(etagInformation.isAll()); + assertNotNull(etagInformation.getEtags()); + assertThat(etagInformation.getEtags().size(), equalTo(3)); + assertThat(etagInformation.getEtags(), hasItems("\"ETag1\"", "\"ETag2\"", "W/\"ETag3\"")); + } + + @Test + public void duplicateEtagValues() { + final EtagInformation etagInformation = odata.createEtagInformation( + Arrays.asList("\"ETag1\"", "\"ETag2\", W/\"ETag1\", \"ETag1\"")); + assertFalse(etagInformation.isAll()); + assertNotNull(etagInformation.getEtags()); + assertThat(etagInformation.getEtags().size(), equalTo(3)); + assertThat(etagInformation.getEtags(), hasItems("\"ETag1\"", "\"ETag2\"", "W/\"ETag1\"")); + } + + @Test + public void specialCharacters() { + final EtagInformation etagInformation = odata.createEtagInformation( + Collections.singleton("\"!#$%&'()*+,-./:;<=>?@[]^_`{|}~¡\u00FF\", \"ETag2\"")); + assertFalse(etagInformation.isAll()); + assertNotNull(etagInformation.getEtags()); + assertThat(etagInformation.getEtags().size(), equalTo(2)); + assertThat(etagInformation.getEtags(), hasItems( + "\"!#$%&'()*+,-./:;<=>?@[]^_`{|}~¡\u00FF\"", "\"ETag2\"")); + } + + @Test + public void wrongFormat() { + final EtagInformation etagInformation = odata.createEtagInformation( + Arrays.asList("\"ETag1\", ETag2", "w/\"ETag3\"", "W//\"ETag4\"", "W/ETag5", + "\"\"ETag6\"\"", " \"ETag7\"\"ETag7\" ", "\"ETag8\" \"ETag8\"", + "\"ETag 9\"", "\"ETag10\"")); + assertFalse(etagInformation.isAll()); + assertNotNull(etagInformation.getEtags()); + assertThat(etagInformation.getEtags().size(), equalTo(2)); + assertThat(etagInformation.getEtags(), hasItems("\"ETag1\"", "\"ETag10\"")); + } + + @Test + public void match() { + assertFalse(odata.createEtagInformation(Collections.<String> emptySet()).isMatchedBy("\"ETag\"")); + assertFalse(odata.createEtagInformation(Collections.singleton("\"ETag\"")).isMatchedBy(null)); + assertTrue(odata.createEtagInformation(Collections.singleton("\"ETag\"")).isMatchedBy("\"ETag\"")); + assertTrue(odata.createEtagInformation(Collections.singleton("*")).isMatchedBy("\"ETag\"")); + assertTrue(odata.createEtagInformation(Collections.singleton("\"ETag\"")).isMatchedBy("W/\"ETag\"")); + assertTrue(odata.createEtagInformation(Collections.singleton("W/\"ETag\"")).isMatchedBy("\"ETag\"")); + assertFalse(odata.createEtagInformation(Collections.singleton("\"ETag\"")).isMatchedBy("W/\"ETag2\"")); + assertFalse(odata.createEtagInformation(Collections.singleton("W/\"ETag\"")).isMatchedBy("\"ETag2\"")); + assertTrue(odata.createEtagInformation(Arrays.asList("\"ETag1\",\"ETag2\"", "\"ETag3\",\"ETag4\"")) + .isMatchedBy("\"ETag4\"")); + assertFalse(odata.createEtagInformation(Arrays.asList("\"ETag1\",\"ETag2\"", "\"ETag3\",\"ETag4\"")) + .isMatchedBy("\"ETag5\"")); + } +} http://git-wip-us.apache.org/repos/asf/olingo-odata4/blob/db47760e/lib/server-tecsvc/src/main/java/org/apache/olingo/server/tecsvc/processor/TechnicalEntityProcessor.java ---------------------------------------------------------------------- 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 322d13b..0ad21bb 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 @@ -34,6 +34,7 @@ import org.apache.olingo.commons.api.http.HttpContentType; import org.apache.olingo.commons.api.http.HttpHeader; import org.apache.olingo.commons.api.http.HttpMethod; import org.apache.olingo.commons.api.http.HttpStatusCode; +import org.apache.olingo.server.api.EtagInformation; import org.apache.olingo.server.api.ODataApplicationException; import org.apache.olingo.server.api.ODataRequest; import org.apache.olingo.server.api.ODataResponse; @@ -409,6 +410,19 @@ public class TechnicalEntityProcessor extends TechnicalProcessor final Entity entity = readEntity(uriInfo); + if (entity.getETag() != null) { + final EtagInformation ifMatch = odata.createEtagInformation(request.getHeaders(HttpHeader.IF_MATCH)); + if (!ifMatch.isMatchedBy(entity.getETag()) && !ifMatch.getEtags().isEmpty()) { + throw new ODataApplicationException("The If-Match precondition is not fulfilled.", + HttpStatusCode.PRECONDITION_FAILED.getStatusCode(), Locale.ROOT); + } + if (odata.createEtagInformation(request.getHeaders(HttpHeader.IF_NONE_MATCH)) + .isMatchedBy(entity.getETag())) { + throw new ODataApplicationException("The entity has not been modified.", + HttpStatusCode.NOT_MODIFIED.getStatusCode(), Locale.ROOT); + } + } + final ODataFormat format = ODataFormat.fromContentType(requestedContentType); final ExpandOption expand = uriInfo.getExpandOption(); final SelectOption select = uriInfo.getSelectOption();
