001 002/** 003 * Licensed to the Apache Software Foundation (ASF) under one or more 004 * contributor license agreements. See the NOTICE file distributed with 005 * this work for additional information regarding copyright ownership. 006 * The ASF licenses this file to You under the Apache License, Version 2.0 007 * (the "License"); you may not use this file except in compliance with 008 * the License. You may obtain a copy of the License at 009 * 010 * http://www.apache.org/licenses/LICENSE-2.0 011 * 012 * Unless required by applicable law or agreed to in writing, software 013 * distributed under the License is distributed on an "AS IS" BASIS, 014 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 015 * See the License for the specific language governing permissions and 016 * limitations under the License. 017 */ 018package org.apache.isis.viewer.restfulobjects.server.resources; 019 020import java.io.IOException; 021import java.io.InputStream; 022import java.text.DateFormat; 023import java.text.SimpleDateFormat; 024import java.util.List; 025import java.util.Map; 026import java.util.Map.Entry; 027 028import javax.ws.rs.core.Response; 029import javax.ws.rs.core.Response.ResponseBuilder; 030 031import org.apache.isis.applib.annotation.ActionSemantics; 032import org.apache.isis.applib.annotation.Where; 033import org.apache.isis.core.commons.authentication.AuthenticationSession; 034import org.apache.isis.core.metamodel.adapter.ObjectAdapter; 035import org.apache.isis.core.metamodel.adapter.version.Version; 036import org.apache.isis.core.metamodel.consent.Consent; 037import org.apache.isis.core.metamodel.facets.object.value.ValueFacet; 038import org.apache.isis.core.metamodel.spec.ObjectSpecification; 039import org.apache.isis.core.metamodel.spec.feature.ObjectAction; 040import org.apache.isis.core.metamodel.spec.feature.ObjectActionParameter; 041import org.apache.isis.core.metamodel.spec.feature.ObjectAssociation; 042import org.apache.isis.core.metamodel.spec.feature.ObjectAssociationFilters; 043import org.apache.isis.core.metamodel.spec.feature.ObjectMember; 044import org.apache.isis.core.metamodel.spec.feature.OneToManyAssociation; 045import org.apache.isis.core.metamodel.spec.feature.OneToOneAssociation; 046import org.apache.isis.viewer.restfulobjects.applib.JsonRepresentation; 047import org.apache.isis.viewer.restfulobjects.applib.client.RestfulResponse.HttpStatusCode; 048import org.apache.isis.viewer.restfulobjects.applib.util.JsonMapper; 049import org.apache.isis.viewer.restfulobjects.applib.util.UrlEncodingUtils; 050import org.apache.isis.viewer.restfulobjects.rendering.RendererContext; 051import org.apache.isis.viewer.restfulobjects.rendering.RendererFactory; 052import org.apache.isis.viewer.restfulobjects.rendering.domainobjects.AbstractObjectMemberReprRenderer; 053import org.apache.isis.viewer.restfulobjects.rendering.domainobjects.ActionResultReprRenderer; 054import org.apache.isis.viewer.restfulobjects.rendering.domainobjects.ActionResultReprRenderer.SelfLink; 055import org.apache.isis.viewer.restfulobjects.rendering.domainobjects.DomainObjectLinkTo; 056import org.apache.isis.viewer.restfulobjects.rendering.domainobjects.DomainObjectReprRenderer; 057import org.apache.isis.viewer.restfulobjects.rendering.domainobjects.JsonValueEncoder; 058import org.apache.isis.viewer.restfulobjects.rendering.domainobjects.MemberType; 059import org.apache.isis.viewer.restfulobjects.rendering.domainobjects.ObjectActionReprRenderer; 060import org.apache.isis.viewer.restfulobjects.rendering.domainobjects.ObjectAdapterLinkTo; 061import org.apache.isis.viewer.restfulobjects.rendering.domainobjects.ObjectAndAction; 062import org.apache.isis.viewer.restfulobjects.rendering.domainobjects.ObjectAndActionInvocation; 063import org.apache.isis.viewer.restfulobjects.rendering.domainobjects.ObjectAndCollection; 064import org.apache.isis.viewer.restfulobjects.rendering.domainobjects.ObjectAndProperty; 065import org.apache.isis.viewer.restfulobjects.rendering.domainobjects.ObjectCollectionReprRenderer; 066import org.apache.isis.viewer.restfulobjects.rendering.domainobjects.ObjectPropertyReprRenderer; 067import org.apache.isis.viewer.restfulobjects.server.ResourceContext; 068import org.apache.isis.viewer.restfulobjects.server.RestfulObjectsApplicationException; 069import org.apache.isis.viewer.restfulobjects.server.resources.ResourceAbstract.Caching; 070import org.apache.isis.viewer.restfulobjects.server.util.OidUtils; 071import org.apache.isis.viewer.restfulobjects.server.util.UrlDecoderUtils; 072import org.apache.isis.viewer.restfulobjects.server.util.UrlParserUtils; 073import org.codehaus.jackson.JsonParseException; 074import org.codehaus.jackson.map.JsonMappingException; 075 076import com.google.common.base.Charsets; 077import com.google.common.collect.Lists; 078import com.google.common.io.ByteStreams; 079 080public final class DomainResourceHelper { 081 082 private static final DateFormat ETAG_FORMAT = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSZ"); 083 084 private final RendererContext resourceContext; 085 private ObjectAdapterLinkTo adapterLinkTo; 086 087 private final ObjectAdapter objectAdapter; 088 089 public DomainResourceHelper(final RendererContext resourceContext, final ObjectAdapter objectAdapter) { 090 this.resourceContext = resourceContext; 091 this.objectAdapter = objectAdapter; 092 using(new DomainObjectLinkTo()); 093 } 094 095 public DomainResourceHelper using(final ObjectAdapterLinkTo linkTo) { 096 adapterLinkTo = linkTo; 097 adapterLinkTo.usingUrlBase(resourceContext).with(objectAdapter); 098 return this; 099 } 100 101 // ////////////////////////////////////////////////////////////// 102 // multiple properties (persist or multi-property update) 103 // ////////////////////////////////////////////////////////////// 104 105 static boolean copyOverProperties(final RendererContext resourceContext, final ObjectAdapter objectAdapter, final JsonRepresentation propertiesList) { 106 final ObjectSpecification objectSpec = objectAdapter.getSpecification(); 107 final List<ObjectAssociation> properties = objectSpec.getAssociations(ObjectAssociationFilters.PROPERTIES); 108 boolean allOk = true; 109 110 for (final ObjectAssociation association : properties) { 111 final OneToOneAssociation property = (OneToOneAssociation) association; 112 final ObjectSpecification propertySpec = property.getSpecification(); 113 final String id = property.getId(); 114 final JsonRepresentation propertyRepr = propertiesList.getRepresentation("[id=%s]", id); 115 if (propertyRepr == null) { 116 if (property.isMandatory()) { 117 throw new IllegalArgumentException(String.format("Mandatory field %s missing", property.getName())); 118 } 119 continue; 120 } 121 final JsonRepresentation valueRepr = propertyRepr.getRepresentation("value"); 122 final Consent usable = property.isUsable(resourceContext.getAuthenticationSession() , objectAdapter, resourceContext.getWhere()); 123 if (usable.isVetoed()) { 124 propertyRepr.mapPut("invalidReason", usable.getReason()); 125 allOk = false; 126 continue; 127 } 128 final ObjectAdapter valueAdapter; 129 try { 130 valueAdapter = objectAdapterFor(resourceContext, propertySpec, valueRepr); 131 } catch(IllegalArgumentException ex) { 132 propertyRepr.mapPut("invalidReason", ex.getMessage()); 133 allOk = false; 134 continue; 135 } 136 final Consent consent = property.isAssociationValid(objectAdapter, valueAdapter); 137 if (consent.isAllowed()) { 138 try { 139 property.set(objectAdapter, valueAdapter); 140 } catch (final IllegalArgumentException ex) { 141 propertyRepr.mapPut("invalidReason", ex.getMessage()); 142 allOk = false; 143 } 144 } else { 145 propertyRepr.mapPut("invalidReason", consent.getReason()); 146 allOk = false; 147 } 148 } 149 150 return allOk; 151 } 152 153 // ////////////////////////////////////////////////////////////// 154 // propertyDetails 155 // ////////////////////////////////////////////////////////////// 156 157 public Response objectRepresentation() { 158 final DomainObjectReprRenderer renderer = new DomainObjectReprRenderer(resourceContext, null, JsonRepresentation.newMap()); 159 renderer.with(objectAdapter).includesSelf(); 160 161 final ResponseBuilder respBuilder = ResourceAbstract.responseOfOk(renderer, Caching.NONE); 162 163 final Version version = objectAdapter.getVersion(); 164 if (version != null && version.getTime() != null) { 165 respBuilder.tag(ETAG_FORMAT.format(version.getTime())); 166 } 167 return respBuilder.build(); 168 } 169 170 // ////////////////////////////////////////////////////////////// 171 // propertyDetails 172 // ////////////////////////////////////////////////////////////// 173 174 public enum MemberMode { 175 NOT_MUTATING { 176 @Override 177 public void apply(final AbstractObjectMemberReprRenderer<?, ?> renderer) { 178 renderer.asStandalone(); 179 } 180 }, 181 MUTATING { 182 @Override 183 public void apply(final AbstractObjectMemberReprRenderer<?, ?> renderer) { 184 renderer.asMutated(); 185 } 186 }; 187 188 public abstract void apply(AbstractObjectMemberReprRenderer<?, ?> renderer); 189 } 190 191 Response propertyDetails(final String propertyId, final MemberMode memberMode, final Caching caching, Where where) { 192 193 final OneToOneAssociation property = getPropertyThatIsVisibleForIntent(propertyId, Intent.ACCESS, where); 194 195 final ObjectPropertyReprRenderer renderer = new ObjectPropertyReprRenderer(resourceContext, null, null, JsonRepresentation.newMap()); 196 197 renderer.with(new ObjectAndProperty(objectAdapter, property)).usingLinkTo(adapterLinkTo); 198 199 memberMode.apply(renderer); 200 201 return ResourceAbstract.responseOfOk(renderer, caching).build(); 202 } 203 204 // ////////////////////////////////////////////////////////////// 205 // collectionDetails 206 // ////////////////////////////////////////////////////////////// 207 208 Response collectionDetails(final String collectionId, final MemberMode memberMode, final Caching caching, Where where) { 209 210 final OneToManyAssociation collection = getCollectionThatIsVisibleForIntent(collectionId, Intent.ACCESS, where); 211 212 final ObjectCollectionReprRenderer renderer = new ObjectCollectionReprRenderer(resourceContext, null, null, JsonRepresentation.newMap()); 213 214 renderer.with(new ObjectAndCollection(objectAdapter, collection)).usingLinkTo(adapterLinkTo); 215 216 memberMode.apply(renderer); 217 218 return ResourceAbstract.responseOfOk(renderer, caching).build(); 219 } 220 221 // ////////////////////////////////////////////////////////////// 222 // action Prompt 223 // ////////////////////////////////////////////////////////////// 224 225 Response actionPrompt(final String actionId, Where where) { 226 final ObjectAction action = getObjectActionThatIsVisibleForIntent(actionId, Intent.ACCESS, where); 227 228 final ObjectActionReprRenderer renderer = new ObjectActionReprRenderer(resourceContext, null, null, JsonRepresentation.newMap()); 229 230 renderer.with(new ObjectAndAction(objectAdapter, action)).usingLinkTo(adapterLinkTo).asStandalone(); 231 232 return ResourceAbstract.responseOfOk(renderer, Caching.NONE).build(); 233 } 234 235 // ////////////////////////////////////////////////////////////// 236 // invoke action 237 // ////////////////////////////////////////////////////////////// 238 239 enum Intent { 240 ACCESS, MUTATE; 241 242 public boolean isMutate() { 243 return this == MUTATE; 244 } 245 } 246 247 Response invokeActionQueryOnly(final String actionId, final JsonRepresentation arguments, Where where) { 248 final ObjectAction action = getObjectActionThatIsVisibleForIntent(actionId, Intent.MUTATE, where); 249 250 final ActionSemantics.Of actionSemantics = action.getSemantics(); 251 if (actionSemantics != ActionSemantics.Of.SAFE) { 252 throw RestfulObjectsApplicationException.createWithMessage(HttpStatusCode.METHOD_NOT_ALLOWED, "Method not allowed; action '%s' is not query only", action.getId()); 253 } 254 255 return invokeActionUsingAdapters(action, arguments, SelfLink.INCLUDED); 256 } 257 258 Response invokeActionIdempotent(final String actionId, final JsonRepresentation arguments, Where where) { 259 260 final ObjectAction action = getObjectActionThatIsVisibleForIntent(actionId, Intent.MUTATE, where); 261 262 final ActionSemantics.Of actionSemantics = action.getSemantics(); 263 if (!actionSemantics.isIdempotentInNature()) { 264 throw RestfulObjectsApplicationException.createWithMessage(HttpStatusCode.METHOD_NOT_ALLOWED, "Method not allowed; action '%s' is not idempotent", action.getId()); 265 } 266 return invokeActionUsingAdapters(action, arguments, SelfLink.EXCLUDED); 267 } 268 269 Response invokeAction(final String actionId, final JsonRepresentation arguments, Where where) { 270 final ObjectAction action = getObjectActionThatIsVisibleForIntent(actionId, Intent.MUTATE, where); 271 272 return invokeActionUsingAdapters(action, arguments, SelfLink.EXCLUDED); 273 } 274 275 private Response invokeActionUsingAdapters(final ObjectAction action, final JsonRepresentation arguments, SelfLink selfLink) { 276 277 final List<ObjectAdapter> argAdapters = parseAndValidateArguments(action, arguments); 278 279 // invoke 280 final ObjectAdapter[] argArray2 = argAdapters.toArray(new ObjectAdapter[0]); 281 final ObjectAdapter returnedAdapter = action.execute(objectAdapter, argArray2); 282 283 // response (void) 284 final ActionResultReprRenderer renderer = new ActionResultReprRenderer(resourceContext, null, selfLink, JsonRepresentation.newMap()); 285 286 renderer.with(new ObjectAndActionInvocation(objectAdapter, action, arguments, returnedAdapter)).using(adapterLinkTo); 287 288 final ResponseBuilder respBuilder = ResourceAbstract.responseOfOk(renderer, Caching.NONE); 289 290 final Version version = objectAdapter.getVersion(); 291 ResourceAbstract.addLastModifiedAndETagIfAvailable(respBuilder, version); 292 293 return respBuilder.build(); 294 } 295 296 /** 297 * 298 * @param resourceContext 299 * @param objectSpec 300 * - the {@link ObjectSpecification} to interpret the object as. 301 * @param argRepr 302 * - expected to be either a String or a Map (ie from within a 303 * List, built by parsing a JSON structure). 304 */ 305 private static ObjectAdapter objectAdapterFor(final RendererContext resourceContext, final ObjectSpecification objectSpec, final JsonRepresentation argRepr) { 306 307 if (argRepr == null) { 308 return null; 309 } 310 311 // value (encodable) 312 if (objectSpec.isEncodeable()) { 313 return JsonValueEncoder.asAdapter(objectSpec, argRepr); 314 } 315 316 final JsonRepresentation argValueRepr = argRepr.getRepresentation("value"); 317 if(argValueRepr == null) { 318 String reason = "No 'value' key"; 319 argRepr.mapPut("invalidReason", reason); 320 throw new IllegalArgumentException(reason); 321 } 322 323 // reference 324 if (!argValueRepr.isLink()) { 325 final String reason = "Expected a link (because this object's type is not a value) but found no 'href'"; 326 argRepr.mapPut("invalidReason", reason); 327 throw new IllegalArgumentException(reason); 328 } 329 final String oidFromHref = UrlParserUtils.encodedOidFromLink(argValueRepr); 330 if (oidFromHref == null) { 331 final String reason = "Could not parse 'href' to identify the object's OID"; 332 argRepr.mapPut("invalidReason", reason); 333 throw new IllegalArgumentException(reason); 334 } 335 336 final ObjectAdapter objectAdapter = OidUtils.getObjectAdapterElseNull(resourceContext, oidFromHref); 337 if (objectAdapter == null) { 338 final String reason = "'href' does not reference a known entity"; 339 argRepr.mapPut("invalidReason", reason); 340 throw new IllegalArgumentException(reason); 341 } 342 return objectAdapter; 343 } 344 345 /** 346 * Similar to 347 * {@link #objectAdapterFor(ResourceContext, ObjectSpecification, Object)}, 348 * however the object being interpreted is a String holding URL encoded JSON 349 * (rather than having already been parsed into a Map representation). 350 * 351 * @throws IOException 352 * @throws JsonMappingException 353 * @throws JsonParseException 354 */ 355 ObjectAdapter objectAdapterFor(final ObjectSpecification spec, final String urlEncodedJson) throws JsonParseException, JsonMappingException, IOException { 356 357 final String json = UrlDecoderUtils.urlDecode(urlEncodedJson); 358 final JsonRepresentation representation = JsonMapper.instance().read(json); 359 return objectAdapterFor(resourceContext, spec, representation); 360 } 361 362 363 // /////////////////////////////////////////////////////////////////// 364 // get{MemberType}ThatIsVisibleAndUsable 365 // /////////////////////////////////////////////////////////////////// 366 367 protected OneToOneAssociation getPropertyThatIsVisibleForIntent(final String propertyId, final Intent intent, Where where) { 368 369 final ObjectAssociation association; 370 try { 371 final ObjectSpecification specification = objectAdapter.getSpecification(); 372 association = specification.getAssociation(propertyId); 373 } catch(Exception ex) { 374 // fall through 375 throwNotFoundException(propertyId, MemberType.PROPERTY); 376 return null; // to keep compiler happy. 377 } 378 379 if (association == null || !association.isOneToOneAssociation()) { 380 throwNotFoundException(propertyId, MemberType.PROPERTY); 381 } 382 383 final OneToOneAssociation property = (OneToOneAssociation) association; 384 return memberThatIsVisibleForIntent(property, MemberType.PROPERTY, intent, where); 385 } 386 387 protected OneToManyAssociation getCollectionThatIsVisibleForIntent(final String collectionId, final Intent intent, Where where) { 388 389 final ObjectAssociation association; 390 try { 391 final ObjectSpecification specification = objectAdapter.getSpecification(); 392 association = specification.getAssociation(collectionId); 393 } catch(Exception ex) { 394 // fall through 395 throwNotFoundException(collectionId, MemberType.COLLECTION); 396 return null; // to keep compiler happy. 397 } 398 if (association == null || !association.isOneToManyAssociation()) { 399 throwNotFoundException(collectionId, MemberType.COLLECTION); 400 } 401 final OneToManyAssociation collection = (OneToManyAssociation) association; 402 return memberThatIsVisibleForIntent(collection, MemberType.COLLECTION, intent, where); 403 } 404 405 protected ObjectAction getObjectActionThatIsVisibleForIntent(final String actionId, final Intent intent, Where where) { 406 407 final ObjectAction action; 408 try { 409 final ObjectSpecification specification = objectAdapter.getSpecification(); 410 action = specification.getObjectAction(actionId); 411 } catch(Exception ex) { 412 throwNotFoundException(actionId, MemberType.ACTION); 413 return null; // to keep compiler happy. 414 } 415 if (action == null) { 416 throwNotFoundException(actionId, MemberType.ACTION); 417 } 418 return memberThatIsVisibleForIntent(action, MemberType.ACTION, intent, where); 419 } 420 421 protected <T extends ObjectMember> T memberThatIsVisibleForIntent(final T objectMember, final MemberType memberType, final Intent intent, Where where) { 422 final String memberId = objectMember.getId(); 423 final AuthenticationSession authenticationSession = resourceContext.getAuthenticationSession(); 424 if (objectMember.isVisible(authenticationSession, objectAdapter, where).isVetoed()) { 425 throwNotFoundException(memberId, memberType); 426 } 427 if (intent.isMutate()) { 428 final Consent usable = objectMember.isUsable(authenticationSession, objectAdapter, where); 429 if (usable.isVetoed()) { 430 throw RestfulObjectsApplicationException.createWithMessage(HttpStatusCode.FORBIDDEN, usable.getReason()); 431 } 432 } 433 return objectMember; 434 } 435 436 protected static void throwNotFoundException(final String memberId, final MemberType memberType) { 437 final String memberTypeStr = memberType.name().toLowerCase(); 438 throw RestfulObjectsApplicationException.createWithMessage(HttpStatusCode.NOT_FOUND, "%s '%s' either does not exist or is not visible", memberTypeStr, memberId); 439 } 440 441 // /////////////////////////////////////////////////////////////////// 442 // parseBody 443 // /////////////////////////////////////////////////////////////////// 444 445 /** 446 * 447 * @param objectSpec 448 * @param bodyAsString 449 * - as per {@link #asStringUtf8(InputStream)} 450 * @return 451 */ 452 ObjectAdapter parseAsMapWithSingleValue(final ObjectSpecification objectSpec, final String bodyAsString) { 453 final JsonRepresentation arguments = readAsMap(bodyAsString); 454 return parseAsMapWithSingleValue(objectSpec, arguments); 455 } 456 457 ObjectAdapter parseAsMapWithSingleValue(final ObjectSpecification objectSpec, final JsonRepresentation arguments) { 458 final JsonRepresentation representation = arguments.getRepresentation("value"); 459 if (arguments.size() != 1 || representation == null) { 460 throw RestfulObjectsApplicationException.createWithMessage(HttpStatusCode.BAD_REQUEST, "Body should be a map with a single key 'value' whose value represents an instance of type '%s'", resourceFor(objectSpec)); 461 } 462 463 return objectAdapterFor(resourceContext, objectSpec, representation); 464 } 465 466 private List<ObjectAdapter> parseAndValidateArguments(final ObjectAction action, final JsonRepresentation arguments) { 467 final List<JsonRepresentation> argList = argListFor(action, arguments); 468 469 final List<ObjectAdapter> argAdapters = Lists.newArrayList(); 470 final List<ObjectActionParameter> parameters = action.getParameters(); 471 boolean valid = true; 472 for (int i = 0; i < argList.size(); i++) { 473 final JsonRepresentation argRepr = argList.get(i); 474 final ObjectSpecification paramSpec = parameters.get(i).getSpecification(); 475 try { 476 final ObjectAdapter argAdapter = objectAdapterFor(resourceContext, paramSpec, argRepr); 477 argAdapters.add(argAdapter); 478 479 // validate individual arg 480 final ObjectActionParameter parameter = parameters.get(i); 481 final Object argPojo = argAdapter!=null?argAdapter.getObject():null; 482 final String reasonNotValid = parameter.isValid(objectAdapter, argPojo, null); 483 if (reasonNotValid != null) { 484 argRepr.mapPut("invalidReason", reasonNotValid); 485 valid = false; 486 } 487 } catch (final IllegalArgumentException e) { 488 argAdapters.add(null); 489 valid = false; 490 } 491 } 492 493 // validate all args 494 final ObjectAdapter[] argArray = argAdapters.toArray(new ObjectAdapter[0]); 495 final Consent consent = action.isProposedArgumentSetValid(objectAdapter, argArray); 496 if (consent.isVetoed()) { 497 arguments.mapPut("x-ro-invalidReason", consent.getReason()); 498 valid = false; 499 } 500 501 if(!valid) { 502 throw RestfulObjectsApplicationException.createWithBody(HttpStatusCode.VALIDATION_FAILED, arguments, "Validation failed, see body for details"); 503 } 504 505 return argAdapters; 506 } 507 508 private static List<JsonRepresentation> argListFor(final ObjectAction action, final JsonRepresentation arguments) { 509 final List<JsonRepresentation> argList = Lists.newArrayList(); 510 511 // ensure that we have no arguments that are not parameters 512 for (final Entry<String, JsonRepresentation> arg : arguments.mapIterable()) { 513 final String argName = arg.getKey(); 514 if (action.getParameterById(argName) == null) { 515 String reason = String.format("Argument '%s' found but no such parameter", argName); 516 arguments.mapPut("x-ro-invalidReason", reason); 517 throw RestfulObjectsApplicationException.createWithBody(HttpStatusCode.BAD_REQUEST, arguments, reason); 518 } 519 } 520 521 // ensure that an argument value has been provided for all non-optional 522 // parameters 523 final List<ObjectActionParameter> parameters = action.getParameters(); 524 for (final ObjectActionParameter param : parameters) { 525 final String paramId = param.getId(); 526 final JsonRepresentation argRepr = arguments.getRepresentation(paramId); 527 if (argRepr == null && !param.isOptional()) { 528 String reason = String.format("No argument found for (mandatory) parameter '%s'", paramId); 529 arguments.mapPut("x-ro-invalidReason", reason); 530 throw RestfulObjectsApplicationException.createWithBody(HttpStatusCode.BAD_REQUEST, arguments, reason); 531 } 532 argList.add(argRepr); 533 } 534 return argList; 535 } 536 537 public static JsonRepresentation readParameterMapAsMap(final Map<String, String[]> parameterMap) { 538 final JsonRepresentation map = JsonRepresentation.newMap(); 539 for (final Map.Entry<String, String[]> parameter : parameterMap.entrySet()) { 540 map.mapPut(parameter.getKey(), parameter.getValue()[0]); 541 } 542 return map; 543 } 544 545 public static JsonRepresentation readQueryStringAsMap(final String queryString) { 546 if (queryString == null) { 547 return JsonRepresentation.newMap(); 548 } 549 final String queryStringTrimmed = queryString.trim(); 550 if (queryStringTrimmed.isEmpty()) { 551 return JsonRepresentation.newMap(); 552 } 553 554 String queryStringUrlDecoded; 555 try { 556 // this is a bit hacky... 557 queryStringUrlDecoded = UrlEncodingUtils.urlDecode(queryStringTrimmed); 558 } catch(Exception ex) { 559 queryStringUrlDecoded = queryStringTrimmed; 560 } 561 562 if (queryStringUrlDecoded.isEmpty()) { 563 return JsonRepresentation.newMap(); 564 } 565 566 return read(queryStringUrlDecoded, "query string"); 567 } 568 569 public static JsonRepresentation readAsMap(final String body) { 570 if (body == null) { 571 return JsonRepresentation.newMap(); 572 } 573 final String bodyTrimmed = body.trim(); 574 if (bodyTrimmed.isEmpty()) { 575 return JsonRepresentation.newMap(); 576 } 577 return read(bodyTrimmed, "body"); 578 } 579 580 private static JsonRepresentation read(final String args, final String argsNature) { 581 try { 582 final JsonRepresentation jsonRepr = JsonMapper.instance().read(args); 583 if (!jsonRepr.isMap()) { 584 throw RestfulObjectsApplicationException.createWithMessage(HttpStatusCode.BAD_REQUEST, "could not read %s as a JSON map", argsNature); 585 } 586 return jsonRepr; 587 } catch (final JsonParseException e) { 588 throw RestfulObjectsApplicationException.createWithCauseAndMessage(HttpStatusCode.BAD_REQUEST, e, "could not parse %s", argsNature); 589 } catch (final JsonMappingException e) { 590 throw RestfulObjectsApplicationException.createWithCauseAndMessage(HttpStatusCode.BAD_REQUEST, e, "could not read %s as JSON", argsNature); 591 } catch (final IOException e) { 592 throw RestfulObjectsApplicationException.createWithCauseAndMessage(HttpStatusCode.BAD_REQUEST, e, "could not parse %s", argsNature); 593 } 594 } 595 596 public static String asStringUtf8(final InputStream body) { 597 try { 598 final byte[] byteArray = ByteStreams.toByteArray(body); 599 return new String(byteArray, Charsets.UTF_8); 600 } catch (final IOException e) { 601 throw RestfulObjectsApplicationException.createWithCauseAndMessage(HttpStatusCode.BAD_REQUEST, e, "could not read body"); 602 } 603 } 604 605 // ////////////////////////////////////////////////////////////// 606 // misc 607 // ////////////////////////////////////////////////////////////// 608 609 private static String resourceFor(final ObjectSpecification objectSpec) { 610 // TODO: should return a string in the form 611 // http://localhost:8080/types/xxx 612 return objectSpec.getFullIdentifier(); 613 } 614 615}