001/**
002 *  Licensed to the Apache Software Foundation (ASF) under one or more
003 *  contributor license agreements.  See the NOTICE file distributed with
004 *  this work for additional information regarding copyright ownership.
005 *  The ASF licenses this file to You under the Apache License, Version 2.0
006 *  (the "License"); you may not use this file except in compliance with
007 *  the License.  You may obtain a copy of the License at
008 *
009 *     http://www.apache.org/licenses/LICENSE-2.0
010 *
011 *  Unless required by applicable law or agreed to in writing, software
012 *  distributed under the License is distributed on an "AS IS" BASIS,
013 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 *  See the License for the specific language governing permissions and
015 *  limitations under the License.
016 */
017package org.apache.isis.viewer.restfulobjects.rendering.domainobjects;
018
019import java.util.List;
020
021import org.codehaus.jackson.node.NullNode;
022
023import org.apache.isis.applib.annotation.Render.Type;
024import org.apache.isis.core.metamodel.adapter.ObjectAdapter;
025import org.apache.isis.core.metamodel.adapter.oid.Oid;
026import org.apache.isis.core.metamodel.adapter.oid.OidMarshaller;
027import org.apache.isis.core.metamodel.adapter.oid.TypedOid;
028import org.apache.isis.core.metamodel.consent.Consent;
029import org.apache.isis.core.metamodel.facets.members.resolve.RenderFacet;
030import org.apache.isis.core.metamodel.facets.object.notpersistable.NotPersistableFacet;
031import org.apache.isis.core.metamodel.facets.object.title.TitleFacet;
032import org.apache.isis.core.metamodel.facets.object.value.ValueFacet;
033import org.apache.isis.core.metamodel.services.ServiceUtil;
034import org.apache.isis.core.metamodel.spec.ObjectActionSet;
035import org.apache.isis.core.metamodel.spec.ObjectSpecification;
036import org.apache.isis.core.metamodel.spec.feature.ObjectAction;
037import org.apache.isis.core.metamodel.spec.feature.ObjectActionContainer.Contributed;
038import org.apache.isis.core.metamodel.spec.feature.ObjectAssociation;
039import org.apache.isis.core.metamodel.spec.feature.OneToManyAssociation;
040import org.apache.isis.core.metamodel.spec.feature.OneToOneAssociation;
041import org.apache.isis.core.runtime.system.context.IsisContext;
042import org.apache.isis.viewer.restfulobjects.applib.JsonRepresentation;
043import org.apache.isis.viewer.restfulobjects.applib.Rel;
044import org.apache.isis.viewer.restfulobjects.applib.RepresentationType;
045import org.apache.isis.viewer.restfulobjects.applib.RestfulHttpMethod;
046import org.apache.isis.viewer.restfulobjects.rendering.LinkBuilder;
047import org.apache.isis.viewer.restfulobjects.rendering.LinkFollowSpecs;
048import org.apache.isis.viewer.restfulobjects.rendering.RendererContext;
049import org.apache.isis.viewer.restfulobjects.rendering.ReprRendererAbstract;
050import org.apache.isis.viewer.restfulobjects.rendering.domaintypes.DomainTypeReprRenderer;
051import org.apache.isis.viewer.restfulobjects.rendering.util.OidUtils;
052
053public class DomainObjectReprRenderer extends ReprRendererAbstract<DomainObjectReprRenderer, ObjectAdapter> {
054
055    private static final String X_RO_DOMAIN_TYPE = "x-ro-domain-type";
056
057    public static LinkBuilder newLinkToBuilder(final RendererContext rendererContext, final Rel rel, final ObjectAdapter objectAdapter) {
058        String domainType = OidUtils.getDomainType(objectAdapter);
059        String instanceId = OidUtils.getInstanceId(rendererContext, objectAdapter);
060        final String url = "objects/" + domainType + "/" + instanceId;
061        final LinkBuilder builder = LinkBuilder.newBuilder(rendererContext, rel.getName(), RepresentationType.DOMAIN_OBJECT, url).withTitle(objectAdapter.titleString());
062        return builder;
063    }
064
065    private static enum Mode {
066        REGULAR(false, true, true), 
067        PERSIST_LINK_ARGUMENTS(true, true, false), 
068        MODIFY_PROPERTIES_LINK_ARGUMENTS(true, false, false),
069        EVENT_SERIALIZATION(false, false, true);
070
071        private final boolean representsArguments;
072        private final boolean describedBy;
073        private boolean checkVisibility;
074
075        private Mode(final boolean representsArguments, final boolean describedBy, final boolean visibilityCheck) {
076            this.representsArguments = representsArguments;
077            this.describedBy = describedBy;
078            this.checkVisibility = visibilityCheck;
079        }
080
081        public boolean representsArguments() {
082            return representsArguments;
083        }
084
085        public boolean includesDescribedBy() {
086            return describedBy;
087        }
088        
089        public boolean checkVisibility() {
090            return checkVisibility;
091        }
092    }
093
094    private ObjectAdapterLinkTo linkToBuilder;
095    private ObjectAdapter objectAdapter;
096    private Mode mode = Mode.REGULAR;
097
098    public DomainObjectReprRenderer(final RendererContext resourceContext, final LinkFollowSpecs linkFollower, final JsonRepresentation representation) {
099        super(resourceContext, linkFollower, RepresentationType.DOMAIN_OBJECT, representation);
100        usingLinkToBuilder(new DomainObjectLinkTo());
101    }
102
103    /**
104     * Override the default {@link ObjectAdapterLinkTo} (that is used for
105     * generating links in {@link #linkTo(ObjectAdapter)}).
106     */
107    public DomainObjectReprRenderer usingLinkToBuilder(final ObjectAdapterLinkTo objectAdapterLinkToBuilder) {
108        this.linkToBuilder = objectAdapterLinkToBuilder.usingUrlBase(rendererContext);
109        return this;
110    }
111
112    @Override
113    public DomainObjectReprRenderer with(final ObjectAdapter objectAdapter) {
114        this.objectAdapter = objectAdapter;
115        String domainTypeHref = DomainTypeReprRenderer.newLinkToBuilder(getRendererContext(), Rel.DOMAIN_TYPE, objectAdapter.getSpecification()).build().getString("href");
116        addMediaTypeParams(X_RO_DOMAIN_TYPE, domainTypeHref);
117        return this;
118    }
119    
120    @Override
121    public JsonRepresentation render() {
122
123        // self, extensions.oid
124        if (!mode.representsArguments()) {
125            if (objectAdapter.representsPersistent()) {
126                if (includesSelf) {
127                    addLinkToSelf();
128                }
129                getExtensions().mapPut("oid", getOidStr());
130            }
131        }
132
133        // title
134        if (!mode.representsArguments()) {
135            final String title = objectAdapter.titleString(null);
136            representation.mapPut("title", title);
137
138        }
139
140        // serviceId or instance Id
141        if (!mode.representsArguments()) {
142            final boolean isService = objectAdapter.getSpecification().isService();
143            if (isService) {
144                representation.mapPut("serviceId", ServiceUtil.id(objectAdapter.getObject()));
145            } else {
146                final String domainType = getDomainType();
147                final String instanceId = getInstanceId();
148                if(domainType != null) {
149                    representation.mapPut("domainType", domainType);
150                    representation.mapPut("instanceId", instanceId);
151                    
152                }
153            }
154        }
155
156        // members
157        withMembers(objectAdapter);
158
159        // described by
160        if (mode.includesDescribedBy()) {
161            addLinkToDescribedBy();
162        }
163
164        if (!mode.representsArguments()) {
165            // update/persist
166            addPersistLinkIfTransientAndPersistable();
167            addUpdatePropertiesLinkIfPersistentAndNotService();
168
169            // extensions
170            final boolean isService = objectAdapter.getSpecification().isService();
171            getExtensions().mapPut("isService", isService);
172            getExtensions().mapPut("isPersistent", objectAdapter.representsPersistent());
173        }
174
175        return representation;
176    }
177
178    private void addLinkToSelf() {
179        final JsonRepresentation link = linkToBuilder.with(objectAdapter).builder(Rel.SELF).build();
180
181        final LinkFollowSpecs linkFollower = getLinkFollowSpecs().follow("links");
182        if (linkFollower.matches(link)) {
183            final DomainObjectReprRenderer renderer = new DomainObjectReprRenderer(getRendererContext(), linkFollower, JsonRepresentation.newMap());
184            renderer.with(objectAdapter);
185            link.mapPut("value", renderer.render());
186        }
187
188        getLinks().arrayAdd(link);
189    }
190
191    private void addLinkToDescribedBy() {
192        final JsonRepresentation link = DomainTypeReprRenderer.newLinkToBuilder(getRendererContext(), Rel.DESCRIBEDBY, objectAdapter.getSpecification()).build();
193
194        final LinkFollowSpecs linkFollower = getLinkFollowSpecs().follow("links");
195        if (linkFollower.matches(link)) {
196            final DomainTypeReprRenderer renderer = new DomainTypeReprRenderer(getRendererContext(), linkFollower, JsonRepresentation.newMap());
197            renderer.with(objectAdapter.getSpecification());
198            link.mapPut("value", renderer.render());
199        }
200        getLinks().arrayAdd(link);
201    }
202
203    private String getDomainType() {
204        return org.apache.isis.viewer.restfulobjects.rendering.util.OidUtils.getDomainType(objectAdapter);
205    }
206
207    private String getInstanceId() {
208        return org.apache.isis.viewer.restfulobjects.rendering.util.OidUtils.getInstanceId(rendererContext, objectAdapter);
209    }
210
211    private String getOidStr() {
212        return org.apache.isis.viewer.restfulobjects.rendering.util.OidUtils.getOidStr(rendererContext, objectAdapter);
213    }
214
215    private DomainObjectReprRenderer withMembers(final ObjectAdapter objectAdapter) {
216        final JsonRepresentation members = JsonRepresentation.newMap();
217        final List<ObjectAssociation> associations = objectAdapter.getSpecification().getAssociations();
218        addAssociations(objectAdapter, members, associations);
219
220        if (!mode.representsArguments()) {
221            final List<ObjectAction> actions = objectAdapter.getSpecification().getObjectActions(Contributed.INCLUDED);
222            addActions(objectAdapter, actions, members);
223        }
224        representation.mapPut("members", members);
225        return this;
226    }
227
228    private void addAssociations(final ObjectAdapter objectAdapter, final JsonRepresentation members, final List<ObjectAssociation> associations) {
229        final LinkFollowSpecs linkFollower = getLinkFollowSpecs().follow("members");
230        for (final ObjectAssociation assoc : associations) {
231
232            if (mode.checkVisibility()) {
233                final Consent visibility = assoc.isVisible(getRendererContext().getAuthenticationSession(), objectAdapter, rendererContext.getWhere());
234                if (!visibility.isAllowed()) {
235                    continue;
236                }
237            }
238            if (assoc instanceof OneToOneAssociation) {
239                final OneToOneAssociation property = (OneToOneAssociation) assoc;
240
241                final ObjectPropertyReprRenderer renderer = new ObjectPropertyReprRenderer(getRendererContext(), linkFollower, property.getId(), JsonRepresentation.newMap());
242
243                renderer.with(new ObjectAndProperty(objectAdapter, property)).usingLinkTo(linkToBuilder);
244
245                if (mode.representsArguments()) {
246                    renderer.asArguments();
247                }
248
249                members.mapPut(assoc.getId(), renderer.render());
250            }
251
252            if (mode.representsArguments()) {
253                // don't include collections
254                continue;
255            }
256            if (assoc instanceof OneToManyAssociation) {
257                final OneToManyAssociation collection = (OneToManyAssociation) assoc;
258
259                final ObjectCollectionReprRenderer renderer = new ObjectCollectionReprRenderer(getRendererContext(), linkFollower, collection.getId(), JsonRepresentation.newMap());
260
261                renderer.with(new ObjectAndCollection(objectAdapter, collection)).usingLinkTo(linkToBuilder);
262
263                members.mapPut(assoc.getId(), renderer.render());
264            }
265        }
266    }
267
268    private void addActions(final ObjectAdapter objectAdapter, final List<ObjectAction> actions, final JsonRepresentation members) {
269        for (final ObjectAction action : actions) {
270            final Consent visibility = action.isVisible(getRendererContext().getAuthenticationSession(), objectAdapter, rendererContext.getWhere());
271            if (!visibility.isAllowed()) {
272                continue;
273            }
274            if (action.getType().isSet()) {
275                final ObjectActionSet objectActionSet = (ObjectActionSet) action;
276                final List<ObjectAction> subactions = objectActionSet.getActions();
277                addActions(objectAdapter, subactions, members);
278
279            } else {
280                final LinkFollowSpecs linkFollowSpecs = getLinkFollowSpecs().follow("members["+action.getId()+"]");
281                
282                final ObjectActionReprRenderer renderer = new ObjectActionReprRenderer(getRendererContext(), linkFollowSpecs, action.getId(), JsonRepresentation.newMap());
283
284                renderer.with(new ObjectAndAction(objectAdapter, action)).usingLinkTo(linkToBuilder);
285
286                members.mapPut(action.getId(), renderer.render());
287            }
288        }
289    }
290
291    private void addPersistLinkIfTransientAndPersistable() {
292        if (objectAdapter.representsPersistent()) {
293            return;
294        }
295        if(objectAdapter.getSpecification().containsDoOpFacet(NotPersistableFacet.class)) {
296            return;
297        }
298        final DomainObjectReprRenderer renderer = new DomainObjectReprRenderer(getRendererContext(), null, JsonRepresentation.newMap());
299        final JsonRepresentation domainObjectRepr = renderer.with(objectAdapter).asPersistLinkArguments().render();
300
301        final String domainType = objectAdapter.getSpecification().getSpecId().asString();
302        final LinkBuilder persistLinkBuilder = LinkBuilder.newBuilder(getRendererContext(), Rel.PERSIST.getName(), RepresentationType.DOMAIN_OBJECT, "objects/%s", domainType).withHttpMethod(RestfulHttpMethod.POST).withArguments(domainObjectRepr);
303        getLinks().arrayAdd(persistLinkBuilder.build());
304    }
305
306    private DomainObjectReprRenderer asPersistLinkArguments() {
307        this.mode = Mode.PERSIST_LINK_ARGUMENTS;
308        return this;
309    }
310
311    private DomainObjectReprRenderer asModifyPropertiesLinkArguments() {
312        this.mode = Mode.MODIFY_PROPERTIES_LINK_ARGUMENTS;
313        return this;
314    }
315
316    // not part of the spec
317    public DomainObjectReprRenderer asEventSerialization() {
318        this.mode = Mode.EVENT_SERIALIZATION;
319        return this;
320    }
321
322
323    private void addUpdatePropertiesLinkIfPersistentAndNotService() {
324        if (!objectAdapter.representsPersistent()) {
325            return;
326        }
327        final boolean isService = objectAdapter.getSpecification().isService();
328        if(isService) {
329            return;
330        }
331
332        final DomainObjectReprRenderer renderer = new DomainObjectReprRenderer(getRendererContext(), null, JsonRepresentation.newMap());
333        final JsonRepresentation domainObjectRepr = renderer.with(objectAdapter).asModifyPropertiesLinkArguments().render();
334
335        final LinkBuilder persistLinkBuilder = LinkBuilder.newBuilder(getRendererContext(), Rel.MODIFY.getName(), RepresentationType.DOMAIN_OBJECT, "objects/%s/%s", getDomainType(), getInstanceId()).withHttpMethod(RestfulHttpMethod.PUT).withArguments(domainObjectRepr);
336        getLinks().arrayAdd(persistLinkBuilder.build());
337    }
338
339    // ///////////////////////////////////////////////////////////////////
340    //
341    // ///////////////////////////////////////////////////////////////////
342
343    public static void appendValueAndFormatOrRef(final RendererContext resourceContext, final ObjectAdapter objectAdapter, final ObjectSpecification objectSpec, JsonRepresentation repr) {
344
345        final ValueFacet valueFacet = objectSpec.getFacet(ValueFacet.class);
346        if (valueFacet != null) {
347            JsonValueEncoder.appendValueAndFormat(objectSpec, objectAdapter, repr);
348            return;
349        }
350
351        if(objectAdapter == null) {
352            repr.mapPut("value", NullNode.getInstance());
353        } else {
354            final TitleFacet titleFacet = objectSpec.getFacet(TitleFacet.class);
355            final String title = titleFacet.title(objectAdapter, resourceContext.getLocalization());
356            JsonRepresentation ref = DomainObjectReprRenderer.newLinkToBuilder(resourceContext, Rel.VALUE, objectAdapter).withTitle(title).build();
357            
358            repr.mapPut("value", ref);
359        }
360
361    }
362
363    public static Object valueOrRef(final RendererContext resourceContext, final ObjectAdapter objectAdapter, final ObjectSpecification objectSpec) {
364        final ValueFacet valueFacet = objectSpec.getFacet(ValueFacet.class);
365        if (valueFacet != null) {
366            return JsonValueEncoder.asObject(objectAdapter);
367        }
368        final TitleFacet titleFacet = objectSpec.getFacet(TitleFacet.class);
369        final String title = titleFacet.title(objectAdapter, resourceContext.getLocalization());
370        return DomainObjectReprRenderer.newLinkToBuilder(resourceContext, Rel.VALUE, objectAdapter).withTitle(title).build();
371    }
372
373 
374    
375    // ///////////////////////////////////////////////////////////////////
376    // dependencies (from context)
377    // ///////////////////////////////////////////////////////////////////
378
379    protected static OidMarshaller getOidMarshaller() {
380                return IsisContext.getOidMarshaller();
381        }
382
383}