package org.odata4j.producer.resources; import java.io.InputStream; import java.io.InputStreamReader; import java.io.StringWriter; import java.util.ArrayList; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import javax.ws.rs.GET; import javax.ws.rs.POST; import javax.ws.rs.PathParam; import javax.ws.rs.Produces; import javax.ws.rs.QueryParam; import javax.ws.rs.core.Context; import javax.ws.rs.core.HttpHeaders; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; import javax.ws.rs.core.Response.Status; import javax.ws.rs.core.SecurityContext; import javax.ws.rs.core.UriInfo; import javax.ws.rs.ext.ContextResolver; import org.odata4j.core.ODataConstants; import org.odata4j.core.ODataHttpMethod; import org.odata4j.core.ODataVersion; import org.odata4j.core.OEntity; import org.odata4j.core.OEntityId; import org.odata4j.core.OEntityIds; import org.odata4j.core.OEntityKey; import org.odata4j.core.OFunctionParameter; import org.odata4j.core.OFunctionParameters; import org.odata4j.edm.EdmCollectionType; import org.odata4j.edm.EdmDataServices; import org.odata4j.edm.EdmEntitySet; import org.odata4j.edm.EdmEntityType; import org.odata4j.edm.EdmFunctionImport; import org.odata4j.edm.EdmFunctionParameter; import org.odata4j.edm.EdmProperty.CollectionKind; import org.odata4j.edm.EdmType; import org.odata4j.exceptions.MethodNotAllowedException; import org.odata4j.exceptions.NotImplementedException; import org.odata4j.format.FormatParser; import org.odata4j.format.FormatParserFactory; import org.odata4j.format.FormatType; import org.odata4j.format.FormatWriter; import org.odata4j.format.FormatWriterFactory; import org.odata4j.format.Parameters; import org.odata4j.format.Settings; import org.odata4j.format.jsonlite.OdataJsonLiteConstant; import org.odata4j.producer.BaseResponse; import org.odata4j.producer.CollectionResponse; import org.odata4j.producer.ComplexObjectResponse; import org.odata4j.producer.EntitiesResponse; import org.odata4j.producer.EntityResponse; import org.odata4j.producer.OBindingResolverExtension; import org.odata4j.producer.OBindingResolverExtensions; import org.odata4j.producer.ODataContext; import org.odata4j.producer.ODataContextImpl; import org.odata4j.producer.ODataProducer; import org.odata4j.producer.PropertyResponse; import org.odata4j.producer.QueryInfo; import org.odata4j.producer.Responses; import org.odata4j.producer.SimpleResponse; /** * Handles function calls. * * <p>Unfortunately the OData URI scheme makes it * impossible to differentiate a function call "resource" from an EntitySet. * So, we hack: EntitiesRequestResource and EntityRequestResource * delegates to this class if it determines that a function is being referenced. * * <ul>TODO: * <li>function parameter facets (required, value ranges, etc). For now, all * validation is up to the function handler in the producer. * <li>non-simple function parameter types * <li>make sure this works for GET and POST */ public class FunctionResource extends BaseResource { @GET @Produces({ ODataConstants.APPLICATION_ATOM_XML_CHARSET_UTF8, ODataConstants.TEXT_JAVASCRIPT_CHARSET_UTF8, ODataConstants.APPLICATION_JAVASCRIPT_CHARSET_UTF8 }) public Response callBoundFunction( @Context HttpHeaders httpHeaders, @Context UriInfo uriInfo, @Context ContextResolver<ODataProducer> producerResolver, @Context SecurityContext securityContext, @PathParam("entitySetName") String entitySetName, @PathParam("id") String id, @PathParam("navProp") String fqFunction, @QueryParam("$inlinecount") String inlineCount, @QueryParam("$top") String top, @QueryParam("$skip") String skip, @QueryParam("$filter") String filter, @QueryParam("$orderby") String orderBy, @QueryParam("$format") String format, @QueryParam("$callback") String callback, @QueryParam("$skiptoken") String skipToken, @QueryParam("$expand") String expand, @QueryParam("$select") String select) throws Exception { ODataProducer producer = producerResolver.getContext(ODataProducer.class); QueryInfo query = new QueryInfo( OptionsQueryParser.parseInlineCount(inlineCount), OptionsQueryParser.parseTop(top), OptionsQueryParser.parseSkip(skip), OptionsQueryParser.parseFilter(filter), OptionsQueryParser.parseOrderBy(orderBy), OptionsQueryParser.parseSkipToken(skipToken), OptionsQueryParser.parseCustomOptions(uriInfo), OptionsQueryParser.parseExpand(expand), OptionsQueryParser.parseSelect(select)); int separatorPos = fqFunction.indexOf("."); String functionName = fqFunction.substring(separatorPos + 1); OEntityKey key = null; if (id != null){ key = OEntityKey.parse(id); } if (producer.getMetadata().containsEdmFunctionImport(functionName)) { // functions that return collections of entities should support the // same set of query options as entity set queries so give them everything. return callFunction( ODataHttpMethod.GET, httpHeaders, uriInfo, securityContext, producer, functionName, format, callback, query, entitySetName, key, null); } return Response.status(Status.NOT_FOUND).build(); } @POST @Produces({ ODataConstants.APPLICATION_ATOM_XML_CHARSET_UTF8, ODataConstants.TEXT_JAVASCRIPT_CHARSET_UTF8, ODataConstants.APPLICATION_JAVASCRIPT_CHARSET_UTF8 }) public Response callBoundAction( @Context HttpHeaders httpHeaders, @Context UriInfo uriInfo, @Context ContextResolver<ODataProducer> producerResolver, @Context SecurityContext securityContext, @PathParam("entitySetName") String entitySetName, @PathParam("id") String id, @PathParam("navProp") String fqAction, @QueryParam("$inlinecount") String inlineCount, @QueryParam("$top") String top, @QueryParam("$skip") String skip, @QueryParam("$filter") String filter, @QueryParam("$orderby") String orderBy, @QueryParam("$format") String format, @QueryParam("$callback") String callback, @QueryParam("$skiptoken") String skipToken, @QueryParam("$expand") String expand, @QueryParam("$select") String select, InputStream payload) throws Exception { ODataProducer producer = producerResolver.getContext(ODataProducer.class); QueryInfo query = new QueryInfo( OptionsQueryParser.parseInlineCount(inlineCount), OptionsQueryParser.parseTop(top), OptionsQueryParser.parseSkip(skip), OptionsQueryParser.parseFilter(filter), OptionsQueryParser.parseOrderBy(orderBy), OptionsQueryParser.parseSkipToken(skipToken), OptionsQueryParser.parseCustomOptions(uriInfo), OptionsQueryParser.parseExpand(expand), OptionsQueryParser.parseSelect(select)); int separatorPos = fqAction.indexOf("."); String functionName = fqAction.substring(separatorPos + 1); OEntityKey key = OEntityKey.parse(id); if (producer.getMetadata().containsEdmFunctionImport(functionName)) { // functions that return collections of entities should support the // same set of query options as entity set queries so give them everything. return callFunction( ODataHttpMethod.POST, httpHeaders, uriInfo, securityContext, producer, functionName, format, callback, query, entitySetName, key, payload); } return Response.status(Status.NOT_FOUND).build(); } /** * Handles function call resource access by gathering function call info from * the request and delegating to the producer. */ public static Response callFunction( ODataHttpMethod callingMethod, HttpHeaders httpHeaders, UriInfo uriInfo, SecurityContext securityContext, ODataProducer producer, String functionName, String format, String callback, QueryInfo queryInfo) throws Exception { return callFunction(callingMethod, httpHeaders, uriInfo, securityContext, producer, functionName, format, callback, queryInfo, null, null, null); } /** * Handles function call resource access by gathering function call info from * the request and delegating to the producer. */ @SuppressWarnings("rawtypes") public static Response callFunction( ODataHttpMethod callingMethod, HttpHeaders httpHeaders, UriInfo uriInfo, SecurityContext securityContext, ODataProducer producer, String functionName, String format, String callback, QueryInfo queryInfo, String boundEntitySetName, OEntityKey boundEntityKey, InputStream payload) throws Exception { // do we have this function? EdmType bindingType = null; if (boundEntitySetName != null) { EdmEntitySet entitySet = producer.getMetadata().findEdmEntitySet(boundEntitySetName); if (entitySet != null){ bindingType = entitySet.getType(); if (boundEntityKey == null) { // The binding type is a collection as we don't have the entity key bindingType = new EdmCollectionType(CollectionKind.Collection, bindingType); } } } EdmFunctionImport function = producer.getMetadata().findEdmFunctionImport(functionName, bindingType); if (function == null) { return Response.status(Status.NOT_FOUND).build(); } ODataContext context = ODataContextImpl.builder().aspect(httpHeaders).aspect(securityContext).aspect(producer).build(); // Check HTTP method String expectedHttpMethodString = function.getHttpMethod(); if (expectedHttpMethodString != null && !"".equals(expectedHttpMethodString)) { ODataHttpMethod expectedHttpMethod = ODataHttpMethod.fromString(expectedHttpMethodString); if (expectedHttpMethod != callingMethod) { throw new MethodNotAllowedException("Method " + callingMethod + " not allowed, expecting " + expectedHttpMethodString); } } // Prepare binding resolver OBindingResolverExtension resolverExtension = producer.findExtension(OBindingResolverExtension.class); if (resolverExtension == null){ resolverExtension = OBindingResolverExtensions.getPartialBindingResolver(); } // First take the parameters from the query Map<String, OFunctionParameter> parameters = getFunctionParameters(function, queryInfo.customOptions, resolverExtension, context, queryInfo); // Then try the payload if any if (payload != null) { parameters.putAll(getFunctionParameters(producer.getMetadata(), function, payload, httpHeaders.getAcceptableMediaTypes())); } // Use the bound parameter if any if (boundEntitySetName != null && function.isBindable()) { OFunctionParameter boundParam = resolverExtension.resolveBindingParameter(context, function, boundEntitySetName, boundEntityKey, queryInfo); parameters.put(boundParam.getName(), boundParam); } // Execute the call BaseResponse response = producer.callFunction(context, function, parameters, queryInfo); if (response == null) { return Response.status(Status.NO_CONTENT).build(); } ODataVersion version = ODataConstants.DATA_SERVICE_VERSION; StringWriter sw = new StringWriter(); FormatWriter<?> fwBase; // hmmh...we are missing an abstraction somewhere.. if (response instanceof ComplexObjectResponse) { FormatWriter<ComplexObjectResponse> fw = FormatWriterFactory.getFormatWriter( ComplexObjectResponse.class, httpHeaders.getAcceptableMediaTypes(), format, callback); fw.write(uriInfo, sw, (ComplexObjectResponse) response); fwBase = fw; } else if (response instanceof CollectionResponse) { CollectionResponse<?> collectionResponse = (CollectionResponse<?>) response; if (collectionResponse.getCollection().getType() instanceof EdmEntityType) { FormatWriter<EntitiesResponse> fw = FormatWriterFactory.getFormatWriter( EntitiesResponse.class, httpHeaders.getAcceptableMediaTypes(), format, callback); // collection of entities. // Does anyone else see this in the v2 spec? I sure don't. This seems // reasonable though given that inlinecount and skip tokens might be included... ArrayList<OEntity> entities = new ArrayList<OEntity>(collectionResponse.getCollection().size()); Iterator iter = collectionResponse.getCollection().iterator(); while (iter.hasNext()) { entities.add((OEntity) iter.next()); } EntitiesResponse er = Responses.entities(entities, collectionResponse.getEntitySet(), collectionResponse.getInlineCount(), collectionResponse.getSkipToken()); fw.write(uriInfo, sw, er); fwBase = fw; } else { // non-entities FormatWriter<CollectionResponse> fw = FormatWriterFactory.getFormatWriter( CollectionResponse.class, httpHeaders.getAcceptableMediaTypes(), format, callback); fw.write(uriInfo, sw, collectionResponse); fwBase = fw; } } else if (response instanceof EntitiesResponse) { FormatWriter<EntitiesResponse> fw = FormatWriterFactory.getFormatWriter( EntitiesResponse.class, httpHeaders.getAcceptableMediaTypes(), format, callback); fw.write(uriInfo, sw, (EntitiesResponse) response); fwBase = fw; } else if (response instanceof PropertyResponse) { FormatWriter<PropertyResponse> fw = FormatWriterFactory.getFormatWriter( PropertyResponse.class, httpHeaders.getAcceptableMediaTypes(), format, callback); fw.write(uriInfo, sw, (PropertyResponse) response); fwBase = fw; } else if (response instanceof SimpleResponse) { FormatWriter<SimpleResponse> fw = FormatWriterFactory.getFormatWriter( SimpleResponse.class, httpHeaders.getAcceptableMediaTypes(), format, callback); fw.write(uriInfo, sw, (SimpleResponse) response); fwBase = fw; } else if (response instanceof EntityResponse) { FormatWriter<EntityResponse> fw = FormatWriterFactory.getFormatWriter( EntityResponse.class, httpHeaders.getAcceptableMediaTypes(), format, callback); fw.write(uriInfo, sw, (EntityResponse) response); fwBase = fw; } else { // TODO add in other response types. throw new NotImplementedException("Unknown BaseResponse type: " + response.getClass().getName()); } String entity = sw.toString(); return Response.ok(entity, fwBase.getContentType()) .header(ODataConstants.Headers.DATA_SERVICE_VERSION, version.asString) .build(); } /** * Takes a Map<String,String> filled with the request URIs custom parameters and * turns them into a map of strongly-typed OFunctionParameter objects. * * @param function the current function * @param opts the query string * @param resolver a binding resolver * @param context the current context * @param queryInfo the current queryInfo * @return a map of function parameters */ private static Map<String, OFunctionParameter> getFunctionParameters( EdmFunctionImport function, Map<String, String> opts, OBindingResolverExtension resolver, ODataContext context, QueryInfo queryInfo) { // first get the producer, we need it to get metadata to pase entity and collections ODataProducer producer = context.getContextAspect(ODataProducer.class); Map<String, OFunctionParameter> m = new HashMap<String, OFunctionParameter>(); for (EdmFunctionParameter p : function.getParameters()) { String val = opts.get(p.getName()); if (function.isBindable() && p.isBound() && val != null){ String entitySetName = null; OEntityKey entityKey = null; if (p.getType() instanceof EdmCollectionType){ entitySetName = val; } else { OEntityId entityId = OEntityIds.parse(val); entitySetName = entityId.getEntitySetName(); entityKey = entityId.getEntityKey(); } m.put(p.getName(), resolver.resolveBindingParameter(context, function, entitySetName, entityKey, queryInfo)); } else { m.put(p.getName(), val == null ? null : OFunctionParameters.parse(producer.getMetadata(), p.getName(), p.getType(), val)); } } return m; } /** * Takes the payload and turns it into a map of strongly-typed * OFunctionParameter objects. * * @param dataServices the service metadata * @param function the function being called * @param payload the post payload * @param acceptTypes the accept types * @return the function parameters */ private static Map<String, OFunctionParameter> getFunctionParameters(EdmDataServices dataServices, EdmFunctionImport function, InputStream payload, List<MediaType> acceptTypes) { Map<String, OFunctionParameter> m = new HashMap<String, OFunctionParameter>(); Settings settings = new Settings(ODataConstants.DATA_SERVICE_VERSION, dataServices, null, null, null, false, null, function); FormatType type = FormatType.JSONVERBOSE; for (MediaType acceptType : acceptTypes) { if (acceptType.getType().equals(MediaType.APPLICATION_JSON_TYPE.getType()) && acceptType.getSubtype().equals(MediaType.APPLICATION_JSON_TYPE.getSubtype())) { Map<String, String> parameters = acceptType.getParameters(); if (parameters.containsValue(OdataJsonLiteConstant.VERBOSE_VALUE)) { type = FormatType.JSONVERBOSE; break; } else { type = FormatType.JSON; break; } } } FormatParser<Parameters> parser = FormatParserFactory.getParser(Parameters.class, type, settings); Parameters params = parser.parse(new InputStreamReader(payload)); for (OFunctionParameter param : params.getParameters()) { m.put(param.getName(), param); } return m; } }