/** * Similar to {@link #readMap(com.linkedin.r2.message.rest.RestMessage)}, but will throw an {@link * IOException} instead of a {@link RestLiInternalException} * * @throws IOException if the message entity cannot be parsed. */ private static DataMap readMapWithExceptions(final RestMessage message) throws IOException { try { return DataMapConverter.bytesToDataMap( message.getHeader(RestConstants.HEADER_CONTENT_TYPE), message.getEntity()); } catch (MimeTypeParseException e) { throw new RoutingException(e.getMessage(), HttpStatus.S_400_BAD_REQUEST.getCode(), e); } }
/** * @param request {@link com.linkedin.r2.message.rest.RestRequest} * @param recordClass resource value class * @param <V> resource value type which is a subclass of {@link RecordTemplate} * @return resource value */ public static <V extends RecordTemplate> V extractEntity( final RestRequest request, final Class<V> recordClass) { try { return DataMapUtils.read(request, recordClass); } catch (IOException e) { throw new RoutingException( "Error parsing entity body: " + e.getMessage(), HttpStatus.S_400_BAD_REQUEST.getCode()); } }
/** * Build a method argument from a request parameter that is NOT backed by a schema, i.e. a * primitive or an array * * @param context {@link ResourceContext} * @param param {@link Parameter} * @return argument value in the correct type */ private static Object buildRegularArgument( final ResourceContext context, final Parameter<?> param) { String value = ArgumentUtils.argumentAsString(context.getParameter(param.getName()), param.getName()); final Object convertedValue; if (value == null) { return null; } else { if (param.isArray()) { convertedValue = buildArrayArgument(context, param); } else { try { convertedValue = ArgumentUtils.convertSimpleValue(value, param.getDataSchema(), param.getType()); } catch (NumberFormatException e) { Class<?> targetClass = DataSchemaUtil.dataSchemaTypeToPrimitiveDataSchemaClass( param.getDataSchema().getDereferencedType()); // thrown from Integer.valueOf or Long.valueOf throw new RoutingException( String.format( "Argument parameter '%s' value '%s' must be of type '%s'", param.getName(), value, targetClass.getName()), HttpStatus.S_400_BAD_REQUEST.getCode()); } catch (IllegalArgumentException e) { // thrown from Enum.valueOf throw new RoutingException( String.format( "Argument parameter '%s' value '%s' is invalid", param.getName(), value), HttpStatus.S_400_BAD_REQUEST.getCode()); } catch (TemplateRuntimeException e) { // thrown from DataTemplateUtil.coerceOutput throw new RoutingException( String.format( "Argument parameter '%s' value '%s' is invalid. Reason: %s", param.getName(), value, e.getMessage()), HttpStatus.S_400_BAD_REQUEST.getCode()); } } } return convertedValue; }
/** * Convert a DataMap representation of a BatchRequest (string->record) into a Java Map appropriate * for passing into application code. Note that compound/complex keys are represented as their * string encoding in the DataMap. Since we have already parsed these keys, we simply try to match * the string representations, rather than re-parsing. * * @param data - the input DataMap to be converted * @param valueClass - the RecordTemplate type of the values * @param ids - the parsed batch ids from the request URI * @return a map using appropriate key and value classes, or null if ids is null */ public static <R extends RecordTemplate> Map<Object, R> buildBatchRequestMap( final DataMap data, final Class<R> valueClass, final Set<?> ids, final ProtocolVersion version) { if (ids == null) { return null; } BatchRequest<R> batchRequest = new BatchRequest<R>(data, new TypeSpec<R>(valueClass)); Map<String, Object> parsedKeyMap = new HashMap<String, Object>(); for (Object o : ids) { parsedKeyMap.put(URIParamUtils.encodeKeyForBody(o, true, version), o); } Map<Object, R> result = new HashMap<Object, R>( CollectionUtils.getMapInitialCapacity(batchRequest.getEntities().size(), 0.75f), 0.75f); for (Map.Entry<String, R> entry : batchRequest.getEntities().entrySet()) { Object key = parsedKeyMap.get(entry.getKey()); if (key == null) { throw new RoutingException( String.format( "Batch request mismatch, URI keys: '%s' Entity keys: '%s'", ids.toString(), batchRequest.getEntities().keySet().toString()), HttpStatus.S_400_BAD_REQUEST.getCode()); } R value = DataTemplateUtil.wrap(entry.getValue().data(), valueClass); result.put(key, value); } if (!ids.equals(result.keySet())) { throw new RoutingException( String.format( "Batch request mismatch, URI keys: '%s' Entity keys: '%s'", ids.toString(), result.keySet().toString()), HttpStatus.S_400_BAD_REQUEST.getCode()); } return result; }
@Override @SuppressWarnings("fallthrough") public RestResponse processDocumentationRequest(RestRequest request) { final String path = request.getURI().getRawPath(); final List<UriComponent.PathSegment> pathSegments = UriComponent.decodePath(path, true); String prefixSegment = null; String actionSegment = null; String typeSegment = null; String objectSegment = null; switch (pathSegments.size()) { case 5: objectSegment = pathSegments.get(4).getPath(); case 4: typeSegment = pathSegments.get(3).getPath(); case 3: actionSegment = pathSegments.get(2).getPath(); case 2: prefixSegment = pathSegments.get(1).getPath(); } assert (prefixSegment.equals(DOC_PREFIX) || (HttpMethod.valueOf(request.getMethod()) == HttpMethod.OPTIONS)); final ByteArrayOutputStream out = new ByteArrayOutputStream(BAOS_BUFFER_SIZE); final RestLiDocumentationRenderer renderer; if (HttpMethod.valueOf(request.getMethod()) == HttpMethod.OPTIONS) { renderer = _jsonRenderer; renderer.renderResource(prefixSegment, out); } else if (HttpMethod.valueOf(request.getMethod()) == HttpMethod.GET) { if (!DOC_VIEW_DOCS_ACTION.equals(actionSegment)) { throw createRoutingError(path); } final MultivaluedMap queryMap = UriComponent.decodeQuery(request.getURI().getQuery(), false); final List<String> formatList = queryMap.get("format"); if (formatList == null) { renderer = _htmlRenderer; } else if (formatList.size() > 1) { throw new RoutingException( String.format( "\"format\" query parameter must be unique, where multiple are specified: %s", Arrays.toString(formatList.toArray())), HttpStatus.S_400_BAD_REQUEST.getCode()); } else { renderer = (formatList.contains(DOC_JSON_FORMAT) ? _jsonRenderer : _htmlRenderer); } if (renderer == _htmlRenderer) { _htmlRenderer.setJsonFormatUri( UriBuilder.fromUri(request.getURI()).queryParam("format", DOC_JSON_FORMAT).build()); } try { if (typeSegment == null || typeSegment.isEmpty()) { renderer.renderHome(out); } else { if (DOC_RESOURCE_TYPE.equals(typeSegment)) { if (objectSegment == null || objectSegment.isEmpty()) { renderer.renderResourceHome(out); } else { renderer.renderResource(objectSegment, out); } } else if (DOC_DATA_TYPE.equals(typeSegment)) { if (objectSegment == null || objectSegment.isEmpty()) { renderer.renderDataModelHome(out); } else { renderer.renderDataModel(objectSegment, out); } } else { throw createRoutingError(path); } } } catch (RuntimeException e) { if (!renderer.handleException(e, out)) { throw e; } } } else { throw new RoutingException(HttpStatus.S_405_METHOD_NOT_ALLOWED.getCode()); } return new RestResponseBuilder() .setStatus(HttpStatus.S_200_OK.getCode()) .setHeader(RestConstants.HEADER_CONTENT_TYPE, renderer.getMIMEType()) .setEntity(out.toByteArray()) .build(); }
/** * Build arguments for resource method invocation. Combines various types of arguments into a * single array. * * @param positionalArguments pass-through arguments coming from {@link RestLiArgumentBuilder} * @param resourceMethod the resource method * @param context {@link ResourceContext} * @param template {@link DynamicRecordTemplate} * @return array of method argument for method invocation. */ @SuppressWarnings("deprecation") public static Object[] buildArgs( final Object[] positionalArguments, final ResourceMethodDescriptor resourceMethod, final ResourceContext context, final DynamicRecordTemplate template) { List<Parameter<?>> parameters = resourceMethod.getParameters(); Object[] arguments = Arrays.copyOf(positionalArguments, parameters.size()); fixUpComplexKeySingletonArraysInArguments(arguments); for (int i = positionalArguments.length; i < parameters.size(); ++i) { Parameter<?> param = parameters.get(i); try { if (param.getParamType() == Parameter.ParamType.KEY || param.getParamType() == Parameter.ParamType.ASSOC_KEY_PARAM) { Object value = context.getPathKeys().get(param.getName()); if (value != null) { arguments[i] = value; continue; } } else if (param.getParamType() == Parameter.ParamType.CALLBACK) { continue; } else if (param.getParamType() == Parameter.ParamType.PARSEQ_CONTEXT_PARAM || param.getParamType() == Parameter.ParamType.PARSEQ_CONTEXT) { continue; // don't know what to fill in yet } else if (param.getParamType() == Parameter.ParamType.HEADER) { HeaderParam headerParam = param.getAnnotations().get(HeaderParam.class); String value = context.getRequestHeaders().get(headerParam.value()); arguments[i] = value; continue; } // Since we have multiple different types of MaskTrees that can be passed into resource // methods, // we must evaluate based on the param type (annotation used) else if (param.getParamType() == Parameter.ParamType.PROJECTION || param.getParamType() == Parameter.ParamType.PROJECTION_PARAM) { arguments[i] = context.getProjectionMask(); continue; } else if (param.getParamType() == Parameter.ParamType.METADATA_PROJECTION_PARAM) { arguments[i] = context.getMetadataProjectionMask(); continue; } else if (param.getParamType() == Parameter.ParamType.PAGING_PROJECTION_PARAM) { arguments[i] = context.getPagingProjectionMask(); continue; } else if (param.getParamType() == Parameter.ParamType.CONTEXT || param.getParamType() == Parameter.ParamType.PAGING_CONTEXT_PARAM) { PagingContext ctx = RestUtils.getPagingContext(context, (PagingContext) param.getDefaultValue()); arguments[i] = ctx; continue; } else if (param.getParamType() == Parameter.ParamType.PATH_KEYS || param.getParamType() == Parameter.ParamType.PATH_KEYS_PARAM) { arguments[i] = context.getPathKeys(); continue; } else if (param.getParamType() == Parameter.ParamType.RESOURCE_CONTEXT || param.getParamType() == Parameter.ParamType.RESOURCE_CONTEXT_PARAM) { arguments[i] = context; continue; } else if (param.getParamType() == Parameter.ParamType.VALIDATOR_PARAM) { RestLiDataValidator validator = new RestLiDataValidator( resourceMethod.getResourceModel().getResourceClass().getAnnotations(), resourceMethod.getResourceModel().getValueClass(), resourceMethod.getMethodType()); arguments[i] = validator; continue; } else if (param.getParamType() == Parameter.ParamType.POST) { // handle action parameters if (template != null) { DataMap data = template.data(); if (data.containsKey(param.getName())) { arguments[i] = template.getValue(param); continue; } } } else if (param.getParamType() == Parameter.ParamType.QUERY) { Object value; if (DataTemplate.class.isAssignableFrom(param.getType())) { value = buildDataTemplateArgument(context, param); } else { value = buildRegularArgument(context, param); } if (value != null) { arguments[i] = value; continue; } } else if (param.getParamType() == Parameter.ParamType.BATCH || param.getParamType() == Parameter.ParamType.RESOURCE_KEY) { // should not come to this routine since it should be handled by passing in // positionalArguments throw new RoutingException( "Parameter '" + param.getName() + "' should be passed in as a positional argument", HttpStatus.S_400_BAD_REQUEST.getCode()); } else { // unknown param type throw new RoutingException( "Parameter '" + param.getName() + "' has an unknown parameter type '" + param.getParamType().name() + "'", HttpStatus.S_400_BAD_REQUEST.getCode()); } } catch (TemplateRuntimeException e) { throw new RoutingException( "Parameter '" + param.getName() + "' is invalid", HttpStatus.S_400_BAD_REQUEST.getCode()); } try { // Handling null-valued parameters not provided in resource context or entity body // check if it is optional parameter if (param.isOptional() && param.hasDefaultValue()) { arguments[i] = param.getDefaultValue(); } else if (param.isOptional() && !param.getType().isPrimitive()) { // optional primitive parameter must have default value or provided arguments[i] = null; } else { throw new RoutingException( "Parameter '" + param.getName() + "' is required", HttpStatus.S_400_BAD_REQUEST.getCode()); } } catch (ResourceConfigException e) { // Parameter default value format exception should result in server error code 500. throw new RestLiServiceException( HttpStatus.S_500_INTERNAL_SERVER_ERROR, "Parameter '" + param.getName() + "' default value is invalid", e); } } return arguments; }
/** * Build a method argument from a request parameter that is an array * * @param context {@link ResourceContext} * @param param {@link Parameter} * @return argument value in the correct type */ private static Object buildArrayArgument( final ResourceContext context, final Parameter<?> param) { final Object convertedValue; if (DataTemplate.class.isAssignableFrom(param.getItemType())) { final DataList itemsList = (DataList) context.getStructuredParameter(param.getName()); convertedValue = Array.newInstance(param.getItemType(), itemsList.size()); int j = 0; for (Object paramData : itemsList) { final DataTemplate<?> itemsElem = DataTemplateUtil.wrap(paramData, param.getItemType().asSubclass(DataTemplate.class)); ValidateDataAgainstSchema.validate( itemsElem.data(), itemsElem.schema(), new ValidationOptions( RequiredMode.CAN_BE_ABSENT_IF_HAS_DEFAULT, CoercionMode.STRING_TO_PRIMITIVE)); Array.set(convertedValue, j++, itemsElem); } } else { final List<String> itemStringValues = context.getParameterValues(param.getName()); ArrayDataSchema parameterSchema = null; if (param.getDataSchema() instanceof ArrayDataSchema) { parameterSchema = (ArrayDataSchema) param.getDataSchema(); } else { throw new RoutingException( "An array schema is expected.", HttpStatus.S_400_BAD_REQUEST.getCode()); } convertedValue = Array.newInstance(param.getItemType(), itemStringValues.size()); int j = 0; for (String itemStringValue : itemStringValues) { if (itemStringValue == null) { throw new RoutingException( "Parameter '" + param.getName() + "' cannot contain null values", HttpStatus.S_400_BAD_REQUEST.getCode()); } try { Array.set( convertedValue, j++, ArgumentUtils.convertSimpleValue( itemStringValue, parameterSchema.getItems(), param.getItemType())); } catch (NumberFormatException e) { Class<?> targetClass = DataSchemaUtil.dataSchemaTypeToPrimitiveDataSchemaClass( parameterSchema.getItems().getDereferencedType()); // thrown from Integer.valueOf or Long.valueOf throw new RoutingException( String.format( "Array parameter '%s' value '%s' must be of type '%s'", param.getName(), itemStringValue, targetClass.getName()), HttpStatus.S_400_BAD_REQUEST.getCode()); } catch (IllegalArgumentException e) { // thrown from Enum.valueOf throw new RoutingException( String.format( "Array parameter '%s' value '%s' is invalid", param.getName(), itemStringValue), HttpStatus.S_400_BAD_REQUEST.getCode()); } catch (TemplateRuntimeException e) { // thrown from DataTemplateUtil.coerceOutput throw new RoutingException( String.format( "Array parameter '%s' value '%s' is invalid. Reason: %s", param.getName(), itemStringValue, e.getMessage()), HttpStatus.S_400_BAD_REQUEST.getCode()); } } } return convertedValue; }