| /* |
| * 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.uri.validator; |
| |
| import java.util.Collections; |
| import java.util.EnumMap; |
| import java.util.List; |
| import java.util.Map; |
| |
| import org.apache.olingo.commons.api.edm.EdmFunction; |
| import org.apache.olingo.commons.api.edm.EdmProperty; |
| import org.apache.olingo.commons.api.edm.EdmReturnType; |
| import org.apache.olingo.commons.api.edm.EdmType; |
| import org.apache.olingo.commons.api.edm.constants.EdmTypeKind; |
| import org.apache.olingo.commons.api.http.HttpMethod; |
| import org.apache.olingo.server.api.uri.UriInfo; |
| import org.apache.olingo.server.api.uri.UriResource; |
| import org.apache.olingo.server.api.uri.UriResourceAction; |
| import org.apache.olingo.server.api.uri.UriResourceFunction; |
| import org.apache.olingo.server.api.uri.UriResourceKind; |
| import org.apache.olingo.server.api.uri.UriResourcePartTyped; |
| import org.apache.olingo.server.api.uri.UriResourceProperty; |
| import org.apache.olingo.server.api.uri.queryoption.SystemQueryOption; |
| import org.apache.olingo.server.api.uri.queryoption.SystemQueryOptionKind; |
| |
| public class UriValidator { |
| |
| //@formatter:off (Eclipse formatter) |
| //CHECKSTYLE:OFF (Maven checkstyle) |
| private static final boolean[][] decisionMatrix = |
| { |
| /* 0-FILTER 1-FORMAT 2-EXPAND 3-ID 4-COUNT 5-ORDERBY 6-SEARCH 7-SELECT 8-SKIP 9-SKIPTOKEN 10-TOP 11-APPLY 12-DELTATOKEN */ |
| /* all 0 */ { true , true , true , false, true , true , true , true , true , true , true , true, true }, |
| /* batch 1 */ { false, false, false, false, false, false, false, false, false, false, false, false, false }, |
| /* crossjoin 2 */ { true , true , true , false, true , true , true , true , true , true , true , true, true }, |
| /* entityId 3 */ { false, true , true , true , false, false, false, true , false, false, false, false, false }, |
| /* metadata 4 */ { false, true , false, false, false, false, false, false, false, false, false, false, false }, |
| /* service 5 */ { false, true , false, false, false, false, false, false, false, false, false, false, false }, |
| /* entitySet 6 */ { true , true , true , false, true , true , true , true , true , true , true , true, true }, |
| /* entitySetCount 7 */ { true , false, false, false, false, false, true , false, false, false, false, true, true }, |
| /* entity 8 */ { false, true , true , false, false, false, false, true , false, false, false, false, false }, |
| /* mediaStream 9 */ { false, false, false, false, false, false, false, false, false, false, false, false, false }, |
| /* references 10 */ { true , true , false, false, true , true , true , false, true , true , true , false, true }, |
| /* reference 11 */ { false, true , false, false, false, false, false, false, false, false, false, false, false }, |
| /* propertyComplex 12 */ { false, true , true , false, false, false, false, true , false, false, false, false, false }, |
| /* propertyComplexCollection 13 */ { true , true , true , false, true , true , false, true , true , true , true , true , true }, |
| /* propertyComplexCollectionCount 14 */ { true , false, false, false, false, false, false, false, false, false, false, true , false }, |
| /* propertyPrimitive 15 */ { false, true , false, false, false, false, false, false, false, false, false, false, false }, |
| /* propertyPrimitiveCollection 16 */ { true , true , false, false, true , true , false, false, true , true , true , false, true }, |
| /* propertyPrimitiveCollectionCount 17 */ { true , false, false, false, false, false, false, false, false, false, false, false, false }, |
| /* propertyPrimitiveValue 18 */ { false, true , false, false, false, false, false, false, false, false, false, false, false }, |
| /* none 19 */ { false, true , false, false, false, false, false, false, false, false, false, false, false } |
| }; |
| //CHECKSTYLE:ON |
| //@formatter:on |
| |
| private enum UriType { |
| all(0), |
| batch(1), |
| crossjoin(2), |
| entityId(3), |
| metadata(4), |
| service(5), |
| entitySet(6), |
| entitySetCount(7), |
| entity(8), |
| mediaStream(9), |
| references(10), |
| reference(11), |
| propertyComplex(12), |
| propertyComplexCollection(13), |
| propertyComplexCollectionCount(14), |
| propertyPrimitive(15), |
| propertyPrimitiveCollection(16), |
| propertyPrimitiveCollectionCount(17), |
| propertyPrimitiveValue(18), |
| none(19); |
| |
| private final int idx; |
| |
| UriType(final int i) { |
| idx = i; |
| } |
| |
| public int getIndex() { |
| return idx; |
| } |
| } |
| |
| private static final Map<SystemQueryOptionKind, Integer> OPTION_INDEX; |
| static { |
| Map<SystemQueryOptionKind, Integer> temp = |
| new EnumMap<>(SystemQueryOptionKind.class); |
| temp.put(SystemQueryOptionKind.FILTER, 0); |
| temp.put(SystemQueryOptionKind.FORMAT, 1); |
| temp.put(SystemQueryOptionKind.EXPAND, 2); |
| temp.put(SystemQueryOptionKind.ID, 3); |
| temp.put(SystemQueryOptionKind.COUNT, 4); |
| temp.put(SystemQueryOptionKind.ORDERBY, 5); |
| temp.put(SystemQueryOptionKind.SEARCH, 6); |
| temp.put(SystemQueryOptionKind.SELECT, 7); |
| temp.put(SystemQueryOptionKind.SKIP, 8); |
| temp.put(SystemQueryOptionKind.SKIPTOKEN, 9); |
| temp.put(SystemQueryOptionKind.TOP, 10); |
| temp.put(SystemQueryOptionKind.APPLY, 11); |
| temp.put(SystemQueryOptionKind.DELTATOKEN, 12); |
| OPTION_INDEX = Collections.unmodifiableMap(temp); |
| } |
| |
| public void validate(final UriInfo uriInfo, final HttpMethod httpMethod) throws UriValidationException { |
| final UriType uriType = getUriType(uriInfo); |
| if (HttpMethod.GET == httpMethod) { |
| validateReadQueryOptions(uriType, uriInfo.getSystemQueryOptions()); |
| } else { |
| validateNonReadQueryOptions(uriType, isAction(uriInfo), uriInfo.getSystemQueryOptions(), httpMethod); |
| validatePropertyOperations(uriInfo, httpMethod); |
| } |
| } |
| |
| private UriType getUriType(final UriInfo uriInfo) throws UriValidationException { |
| UriType uriType; |
| |
| switch (uriInfo.getKind()) { |
| case all: |
| uriType = UriType.all; |
| break; |
| case batch: |
| uriType = UriType.batch; |
| break; |
| case crossjoin: |
| uriType = UriType.crossjoin; |
| break; |
| case entityId: |
| uriType = UriType.entityId; |
| break; |
| case metadata: |
| uriType = UriType.metadata; |
| break; |
| case resource: |
| uriType = getUriTypeForResource(uriInfo.getUriResourceParts()); |
| break; |
| case service: |
| uriType = UriType.service; |
| break; |
| default: |
| throw new UriValidationException("Unsupported uriInfo kind: " + uriInfo.getKind(), |
| UriValidationException.MessageKeys.UNSUPPORTED_URI_KIND, uriInfo.getKind().toString()); |
| } |
| |
| return uriType; |
| } |
| |
| /** |
| * Determines the URI type for a resource path. |
| * The URI parser has already made sure that there are enough segments for a given type of the last segment, |
| * but don't try to extract always the second-to-last segment, it could cause an {@link IndexOutOfBoundsException}. |
| */ |
| private UriType getUriTypeForResource(final List<UriResource> segments) throws UriValidationException { |
| final UriResource lastPathSegment = segments.get(segments.size() - 1); |
| |
| UriType uriType; |
| switch (lastPathSegment.getKind()) { |
| case count: |
| uriType = getUriTypeForCount(segments.get(segments.size() - 2)); |
| break; |
| case action: |
| uriType = getUriTypeForAction(lastPathSegment); |
| break; |
| case complexProperty: |
| uriType = getUriTypeForComplexProperty(lastPathSegment); |
| break; |
| case entitySet: |
| case navigationProperty: |
| uriType = getUriTypeForEntitySet(lastPathSegment); |
| break; |
| case function: |
| uriType = getUriTypeForFunction(lastPathSegment); |
| break; |
| case primitiveProperty: |
| uriType = getUriTypeForPrimitiveProperty(lastPathSegment); |
| break; |
| case ref: |
| uriType = getUriTypeForRef(segments.get(segments.size() - 2)); |
| break; |
| case singleton: |
| uriType = UriType.entity; |
| break; |
| case value: |
| uriType = getUriTypeForValue(segments.get(segments.size() - 2)); |
| break; |
| default: |
| throw new UriValidationException("Unsupported uriResource kind: " + lastPathSegment.getKind(), |
| UriValidationException.MessageKeys.UNSUPPORTED_URI_RESOURCE_KIND, lastPathSegment.getKind().toString()); |
| } |
| |
| return uriType; |
| } |
| |
| private UriType getUriTypeForValue(final UriResource secondLastPathSegment) throws UriValidationException { |
| UriType uriType; |
| switch (secondLastPathSegment.getKind()) { |
| case primitiveProperty: |
| uriType = UriType.propertyPrimitiveValue; |
| break; |
| case entitySet: |
| case navigationProperty: |
| case singleton: |
| uriType = UriType.mediaStream; |
| break; |
| case function: |
| UriResourceFunction uriFunction = (UriResourceFunction) secondLastPathSegment; |
| final EdmFunction function = uriFunction.getFunction(); |
| uriType = function.getReturnType().getType().getKind() == EdmTypeKind.ENTITY ? |
| UriType.mediaStream : UriType.propertyPrimitiveValue; |
| break; |
| default: |
| throw new UriValidationException( |
| "Unexpected kind in path segment before $value: " + secondLastPathSegment.getKind(), |
| UriValidationException.MessageKeys.UNALLOWED_KIND_BEFORE_VALUE, secondLastPathSegment.toString()); |
| } |
| return uriType; |
| } |
| |
| private UriType getUriTypeForRef(final UriResource secondLastPathSegment) throws UriValidationException { |
| return isCollection(secondLastPathSegment) ? UriType.references : UriType.reference; |
| } |
| |
| private boolean isCollection(final UriResource pathSegment) throws UriValidationException { |
| if (pathSegment instanceof UriResourcePartTyped) { |
| return ((UriResourcePartTyped) pathSegment).isCollection(); |
| } else { |
| throw new UriValidationException( |
| "Path segment is not an instance of UriResourcePartTyped but " + pathSegment.getClass(), |
| UriValidationException.MessageKeys.LAST_SEGMENT_NOT_TYPED, pathSegment.toString()); |
| } |
| } |
| |
| private UriType getUriTypeForPrimitiveProperty(final UriResource lastPathSegment) throws UriValidationException { |
| return isCollection(lastPathSegment) ? UriType.propertyPrimitiveCollection : UriType.propertyPrimitive; |
| } |
| |
| private UriType getUriTypeForFunction(final UriResource lastPathSegment) throws UriValidationException { |
| final UriResourceFunction uriFunction = (UriResourceFunction) lastPathSegment; |
| final boolean isCollection = uriFunction.isCollection(); |
| final EdmTypeKind typeKind = uriFunction.getFunction().getReturnType().getType().getKind(); |
| UriType uriType; |
| switch (typeKind) { |
| case ENTITY: |
| uriType = isCollection ? UriType.entitySet : UriType.entity; |
| break; |
| case PRIMITIVE: |
| case ENUM: |
| case DEFINITION: |
| uriType = isCollection ? UriType.propertyPrimitiveCollection : UriType.propertyPrimitive; |
| break; |
| case COMPLEX: |
| uriType = isCollection ? UriType.propertyComplexCollection : UriType.propertyComplex; |
| break; |
| default: |
| throw new UriValidationException("Unsupported function return type: " + typeKind, |
| UriValidationException.MessageKeys.UNSUPPORTED_FUNCTION_RETURN_TYPE, typeKind.toString()); |
| } |
| |
| return uriType; |
| } |
| |
| private UriType getUriTypeForEntitySet(final UriResource lastPathSegment) throws UriValidationException { |
| return isCollection(lastPathSegment) ? UriType.entitySet : UriType.entity; |
| } |
| |
| private UriType getUriTypeForComplexProperty(final UriResource lastPathSegment) throws UriValidationException { |
| return isCollection(lastPathSegment) ? UriType.propertyComplexCollection : UriType.propertyComplex; |
| } |
| |
| private UriType getUriTypeForAction(final UriResource lastPathSegment) throws UriValidationException { |
| final EdmReturnType rt = ((UriResourceAction) lastPathSegment).getAction().getReturnType(); |
| if (rt == null) { |
| return UriType.none; |
| } |
| UriType uriType; |
| switch (rt.getType().getKind()) { |
| case ENTITY: |
| uriType = rt.isCollection() ? UriType.entitySet : UriType.entity; |
| break; |
| case PRIMITIVE: |
| case ENUM: |
| case DEFINITION: |
| uriType = rt.isCollection() ? UriType.propertyPrimitiveCollection : UriType.propertyPrimitive; |
| break; |
| case COMPLEX: |
| uriType = rt.isCollection() ? UriType.propertyComplexCollection : UriType.propertyComplex; |
| break; |
| default: |
| throw new UriValidationException("Unsupported action return type: " + rt.getType().getKind(), |
| UriValidationException.MessageKeys.UNSUPPORTED_ACTION_RETURN_TYPE, rt.getType().getKind().toString()); |
| } |
| return uriType; |
| } |
| |
| private UriType getUriTypeForCount(final UriResource secondLastPathSegment) throws UriValidationException { |
| UriType uriType; |
| switch (secondLastPathSegment.getKind()) { |
| case entitySet: |
| case navigationProperty: |
| uriType = UriType.entitySetCount; |
| break; |
| case complexProperty: |
| uriType = UriType.propertyComplexCollectionCount; |
| break; |
| case primitiveProperty: |
| uriType = UriType.propertyPrimitiveCollectionCount; |
| break; |
| case function: |
| final UriResourceFunction uriFunction = (UriResourceFunction) secondLastPathSegment; |
| final EdmFunction function = uriFunction.getFunction(); |
| final EdmType returnType = function.getReturnType().getType(); |
| switch (returnType.getKind()) { |
| case ENTITY: |
| uriType = UriType.entitySetCount; |
| break; |
| case COMPLEX: |
| uriType = UriType.propertyComplexCollectionCount; |
| break; |
| case PRIMITIVE: |
| case ENUM: |
| case DEFINITION: |
| uriType = UriType.propertyPrimitiveCollectionCount; |
| break; |
| default: |
| throw new UriValidationException("Unsupported return type: " + returnType.getKind(), |
| UriValidationException.MessageKeys.UNSUPPORTED_FUNCTION_RETURN_TYPE, returnType.getKind().toString()); |
| } |
| break; |
| default: |
| throw new UriValidationException("Illegal path part kind before $count: " + secondLastPathSegment.getKind(), |
| UriValidationException.MessageKeys.UNALLOWED_KIND_BEFORE_COUNT, secondLastPathSegment.toString()); |
| } |
| |
| return uriType; |
| } |
| |
| private void validateReadQueryOptions(final UriType uriType, final List<SystemQueryOption> options) |
| throws UriValidationException { |
| for (final SystemQueryOption option : options) { |
| final SystemQueryOptionKind kind = option.getKind(); |
| if (OPTION_INDEX.containsKey(kind)) { |
| final int columnIndex = OPTION_INDEX.get(kind); |
| if (!decisionMatrix[uriType.getIndex()][columnIndex]) { |
| throw new UriValidationException("System query option not allowed: " + option.getName(), |
| UriValidationException.MessageKeys.SYSTEM_QUERY_OPTION_NOT_ALLOWED, option.getName()); |
| } |
| } else { |
| throw new UriValidationException("Unsupported option: " + kind, |
| UriValidationException.MessageKeys.UNSUPPORTED_QUERY_OPTION, kind.toString()); |
| } |
| } |
| } |
| |
| private void validateNonReadQueryOptions(final UriType uriType, final boolean isAction, |
| final List<SystemQueryOption> options, final HttpMethod httpMethod) throws UriValidationException { |
| if (httpMethod == HttpMethod.POST && isAction) { |
| // From the OData specification: |
| // For POST requests to an action URL the return type of the action determines the applicable |
| // system query options that a service MAY support, following the same rules as GET requests. |
| validateReadQueryOptions(uriType, options); |
| |
| } else if (httpMethod == HttpMethod.DELETE && uriType == UriType.references) { |
| // Only $id is allowed as SystemQueryOption for DELETE on a reference collection. |
| for (final SystemQueryOption option : options) { |
| if (SystemQueryOptionKind.ID != option.getKind()) { |
| throw new UriValidationException( |
| "System query option " + option.getName() + " is not allowed for method " + httpMethod, |
| UriValidationException.MessageKeys.SYSTEM_QUERY_OPTION_NOT_ALLOWED_FOR_HTTP_METHOD, |
| option.getName(), httpMethod.toString()); |
| } |
| } |
| |
| } else if (!options.isEmpty() && !checkIfSelectOrExpand(options, httpMethod)) { |
| StringBuilder optionsString = new StringBuilder(); |
| for (final SystemQueryOption option : options) { |
| optionsString.append(option.getName()).append(' '); |
| } |
| throw new UriValidationException( |
| "System query option(s) " + optionsString.toString() + "not allowed for method " + httpMethod, |
| UriValidationException.MessageKeys.SYSTEM_QUERY_OPTION_NOT_ALLOWED_FOR_HTTP_METHOD, |
| optionsString.toString(), httpMethod.toString()); |
| } |
| } |
| |
| private boolean checkIfSelectOrExpand(List<SystemQueryOption> options, HttpMethod httpMethod) { |
| boolean isSelectOrExpand = false; |
| for (SystemQueryOption queryOption : options) { |
| isSelectOrExpand = ((queryOption.getKind() == SystemQueryOptionKind.EXPAND) || |
| (queryOption.getKind() == SystemQueryOptionKind.SELECT)) && |
| (httpMethod == HttpMethod.PUT || httpMethod == HttpMethod.PATCH); |
| } |
| return isSelectOrExpand; |
| } |
| |
| private boolean isAction(final UriInfo uriInfo) { |
| List<UriResource> uriResourceParts = uriInfo.getUriResourceParts(); |
| return !uriResourceParts.isEmpty() |
| && UriResourceKind.action == uriResourceParts.get(uriResourceParts.size() - 1).getKind(); |
| } |
| |
| private void validatePropertyOperations(final UriInfo uriInfo, final HttpMethod method) |
| throws UriValidationException { |
| final List<UriResource> parts = uriInfo.getUriResourceParts(); |
| final UriResource last = !parts.isEmpty() ? parts.get(parts.size() - 1) : null; |
| final UriResource previous = parts.size() > 1 ? parts.get(parts.size() - 2) : null; |
| if (last != null |
| && (last.getKind() == UriResourceKind.primitiveProperty |
| || last.getKind() == UriResourceKind.complexProperty |
| || (last.getKind() == UriResourceKind.value |
| && previous != null && previous.getKind() == UriResourceKind.primitiveProperty))) { |
| final EdmProperty property = ((UriResourceProperty) |
| (last.getKind() == UriResourceKind.value ? previous : last)).getProperty(); |
| if (method == HttpMethod.PATCH && property.isCollection()) { |
| throw new UriValidationException("Attempt to patch collection property.", |
| UriValidationException.MessageKeys.UNSUPPORTED_HTTP_METHOD, method.toString()); |
| } |
| if (method == HttpMethod.DELETE && !property.isNullable()) { |
| throw new UriValidationException("Attempt to delete non-nullable property.", |
| UriValidationException.MessageKeys.UNSUPPORTED_HTTP_METHOD, method.toString()); |
| } |
| } |
| } |
| } |