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}