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.apache.isis.core.metamodel.adapter.ObjectAdapter;
022import org.apache.isis.core.metamodel.adapter.oid.OidMarshaller;
023import org.apache.isis.core.metamodel.consent.Consent;
024import org.apache.isis.core.metamodel.facets.object.notpersistable.NotPersistableFacet;
025import org.apache.isis.core.metamodel.facets.object.title.TitleFacet;
026import org.apache.isis.core.metamodel.services.ServiceUtil;
027import org.apache.isis.core.metamodel.spec.ObjectSpecification;
028import org.apache.isis.core.metamodel.spec.feature.Contributed;
029import org.apache.isis.core.metamodel.spec.feature.ObjectAction;
030import org.apache.isis.core.metamodel.spec.feature.ObjectAssociation;
031import org.apache.isis.core.metamodel.spec.feature.OneToManyAssociation;
032import org.apache.isis.core.metamodel.spec.feature.OneToOneAssociation;
033import org.apache.isis.core.runtime.system.context.IsisContext;
034import org.apache.isis.viewer.restfulobjects.applib.JsonRepresentation;
035import org.apache.isis.viewer.restfulobjects.applib.Rel;
036import org.apache.isis.viewer.restfulobjects.applib.RepresentationType;
037import org.apache.isis.viewer.restfulobjects.applib.RestfulHttpMethod;
038import org.apache.isis.viewer.restfulobjects.rendering.LinkBuilder;
039import org.apache.isis.viewer.restfulobjects.rendering.LinkFollowSpecs;
040import org.apache.isis.viewer.restfulobjects.rendering.RendererContext;
041import org.apache.isis.viewer.restfulobjects.rendering.ReprRendererAbstract;
042import org.apache.isis.viewer.restfulobjects.rendering.domaintypes.DomainTypeReprRenderer;
043import org.apache.isis.viewer.restfulobjects.rendering.util.OidUtils;
044
045public class DomainObjectReprRenderer extends ReprRendererAbstract<DomainObjectReprRenderer, ObjectAdapter> {
046
047    private static final String X_RO_DOMAIN_TYPE = "x-ro-domain-type";
048
049    public static LinkBuilder newLinkToBuilder(final RendererContext rendererContext, final Rel rel, final ObjectAdapter objectAdapter) {
050        String domainType = OidUtils.getDomainType(objectAdapter);
051        String instanceId = OidUtils.getInstanceId(rendererContext, objectAdapter);
052        final String url = "objects/" + domainType + "/" + instanceId;
053        return LinkBuilder.newBuilder(rendererContext, rel.getName(), RepresentationType.DOMAIN_OBJECT, url).withTitle(objectAdapter.titleString(null));
054    }
055
056    private static enum Mode {
057        REGULAR, 
058        PERSIST_LINK_ARGUMENTS, 
059        UPDATE_PROPERTIES_LINK_ARGUMENTS,
060        EVENT_SERIALIZATION;
061
062        public boolean isRegular() {
063            return this == REGULAR;
064        }
065
066        public boolean isPersistLinkArgs() {
067            return this == PERSIST_LINK_ARGUMENTS;
068        }
069        
070        public boolean isUpdatePropertiesLinkArgs() {
071            return this == UPDATE_PROPERTIES_LINK_ARGUMENTS;
072        }
073
074        public boolean isEventSerialization() {
075            return this == EVENT_SERIALIZATION;
076        }
077
078        public boolean includeDescribedBy() {
079            return isRegular() || isPersistLinkArgs();
080        }
081
082        public boolean includeUp() {
083            return isRegular();
084        }
085
086        public boolean checkVisibility() {
087            return isRegular() || isUpdatePropertiesLinkArgs();
088        }
089
090        public boolean isArgs() {
091            return isPersistLinkArgs() || isUpdatePropertiesLinkArgs();
092        }
093    }
094
095    private ObjectAdapterLinkTo linkToBuilder;
096    private ObjectAdapter objectAdapter;
097    private Mode mode = Mode.REGULAR;
098
099    public DomainObjectReprRenderer(final RendererContext resourceContext, final LinkFollowSpecs linkFollower, final JsonRepresentation representation) {
100        super(resourceContext, linkFollower, RepresentationType.DOMAIN_OBJECT, representation);
101        usingLinkToBuilder(new DomainObjectLinkTo());
102    }
103
104    /**
105     * Override the default {@link ObjectAdapterLinkTo} (that is used for
106     * generating links in {@link #linkTo(ObjectAdapter)}).
107     */
108    public DomainObjectReprRenderer usingLinkToBuilder(final ObjectAdapterLinkTo objectAdapterLinkToBuilder) {
109        this.linkToBuilder = objectAdapterLinkToBuilder.usingUrlBase(rendererContext);
110        return this;
111    }
112
113    @Override
114    public DomainObjectReprRenderer with(final ObjectAdapter objectAdapter) {
115        this.objectAdapter = objectAdapter;
116        String domainTypeHref = DomainTypeReprRenderer.newLinkToBuilder(getRendererContext(), Rel.DOMAIN_TYPE, objectAdapter.getSpecification()).build().getString("href");
117        addMediaTypeParams(X_RO_DOMAIN_TYPE, domainTypeHref);
118        return this;
119    }
120    
121    @Override
122    public JsonRepresentation render() {
123
124        final boolean isService = objectAdapter.getSpecification().isService();
125
126        if (!(mode.isArgs())) {
127            
128            // self, extensions.oid
129            if (objectAdapter.representsPersistent()) {
130                if (includesSelf) {
131                    addLinkToSelf();
132                }
133                getExtensions().mapPut("oid", getOidStr());
134            }
135            
136            // title
137            final String title = objectAdapter.titleString(null);
138            representation.mapPut("title", title);
139            
140            // serviceId or instance Id
141            if (isService) {
142                representation.mapPut("serviceId", ServiceUtil.id(objectAdapter.getObject()));
143            } else {
144                final String domainType = getDomainType();
145                final String instanceId = getInstanceId();
146                if (domainType != null) {
147                    representation.mapPut("domainType", domainType);
148                    representation.mapPut("instanceId", instanceId);
149                    
150                }
151            }
152        }
153
154        // members
155        withMembers(objectAdapter);
156
157        // described by
158        if (mode.includeDescribedBy()) {
159            addLinkToDescribedBy();
160        }
161        if(isService && mode.includeUp()) {
162            addLinkToUp();
163        }
164
165        if (!mode.isArgs()) {
166            // update/persist
167            addPersistLinkIfTransientAndPersistable();
168            addUpdatePropertiesLinkIfRequired();
169
170            // extensions
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 void addLinkToUp() {
204        final JsonRepresentation link = LinkBuilder.newBuilder(rendererContext, Rel.UP.getName(), RepresentationType.LIST, "services").build();
205        getLinks().arrayAdd(link);
206    }
207
208    private String getDomainType() {
209        return org.apache.isis.viewer.restfulobjects.rendering.util.OidUtils.getDomainType(objectAdapter);
210    }
211
212    private String getInstanceId() {
213        return org.apache.isis.viewer.restfulobjects.rendering.util.OidUtils.getInstanceId(rendererContext, objectAdapter);
214    }
215
216    private String getOidStr() {
217        return org.apache.isis.viewer.restfulobjects.rendering.util.OidUtils.getOidStr(rendererContext, objectAdapter);
218    }
219
220    private DomainObjectReprRenderer withMembers(final ObjectAdapter objectAdapter) {
221        final JsonRepresentation appendTo = 
222                mode.isUpdatePropertiesLinkArgs() ? representation : JsonRepresentation.newMap();
223        final List<ObjectAssociation> associations = objectAdapter.getSpecification().getAssociations(Contributed.EXCLUDED);
224        addAssociations(objectAdapter, appendTo, associations);
225
226        if (mode.isRegular()) {
227            final List<ObjectAction> actions = objectAdapter.getSpecification().getObjectActions(Contributed.INCLUDED);
228            addActions(objectAdapter, actions, appendTo);
229        }
230        if(!mode.isUpdatePropertiesLinkArgs()) {
231            representation.mapPut("members", appendTo);
232        }
233        return this;
234    }
235
236    private void addAssociations(final ObjectAdapter objectAdapter, final JsonRepresentation members, final List<ObjectAssociation> associations) {
237        final LinkFollowSpecs linkFollower = getLinkFollowSpecs().follow("members");
238        for (final ObjectAssociation assoc : associations) {
239
240            if (mode.checkVisibility()) {
241                final Consent visibility = assoc.isVisible(getRendererContext().getAuthenticationSession(), objectAdapter, rendererContext.getWhere());
242                if (!visibility.isAllowed()) {
243                    continue;
244                }
245            }
246            if (assoc instanceof OneToOneAssociation) {
247                final OneToOneAssociation property = (OneToOneAssociation) assoc;
248
249                final ObjectPropertyReprRenderer renderer = new ObjectPropertyReprRenderer(getRendererContext(), linkFollower, property.getId(), JsonRepresentation.newMap());
250
251                renderer.with(new ObjectAndProperty(objectAdapter, property)).usingLinkTo(linkToBuilder);
252
253                if (mode.isArgs()) {
254                    renderer.asArguments();
255                }
256                if(mode.isEventSerialization()) {
257                    renderer.asEventSerialization();
258                }
259
260                members.mapPut(assoc.getId(), renderer.render());
261            }
262
263            if (mode.isArgs()) {
264                continue;
265            }
266            if (assoc instanceof OneToManyAssociation) {
267                final OneToManyAssociation collection = (OneToManyAssociation) assoc;
268
269                final ObjectCollectionReprRenderer renderer = new ObjectCollectionReprRenderer(getRendererContext(), linkFollower, collection.getId(), JsonRepresentation.newMap());
270
271                renderer.with(new ObjectAndCollection(objectAdapter, collection)).usingLinkTo(linkToBuilder);
272                if(mode.isEventSerialization()) {
273                    renderer.asEventSerialization();
274                }
275
276                members.mapPut(assoc.getId(), renderer.render());
277            }
278        }
279    }
280
281    private void addActions(final ObjectAdapter objectAdapter, final List<ObjectAction> actions, final JsonRepresentation members) {
282        for (final ObjectAction action : actions) {
283            final Consent visibility = action.isVisible(getRendererContext().getAuthenticationSession(), objectAdapter, rendererContext.getWhere());
284            if (!visibility.isAllowed()) {
285                continue;
286            }
287            final LinkFollowSpecs linkFollowSpecs = getLinkFollowSpecs().follow("members["+action.getId()+"]");
288            
289            final ObjectActionReprRenderer renderer = new ObjectActionReprRenderer(getRendererContext(), linkFollowSpecs, action.getId(), JsonRepresentation.newMap());
290
291            renderer.with(new ObjectAndAction(objectAdapter, action)).usingLinkTo(linkToBuilder);
292
293            members.mapPut(action.getId(), renderer.render());
294        }
295    }
296
297    private void addPersistLinkIfTransientAndPersistable() {
298        if (objectAdapter.representsPersistent()) {
299            return;
300        }
301        if(objectAdapter.getSpecification().containsDoOpFacet(NotPersistableFacet.class)) {
302            return;
303        }
304        final DomainObjectReprRenderer renderer = new DomainObjectReprRenderer(getRendererContext(), null, JsonRepresentation.newMap());
305        final JsonRepresentation domainObjectRepr = renderer.with(objectAdapter).asPersistLinkArguments().render();
306
307        final String domainType = objectAdapter.getSpecification().getSpecId().asString();
308        final LinkBuilder persistLinkBuilder = LinkBuilder.newBuilder(getRendererContext(), Rel.PERSIST.getName(), RepresentationType.DOMAIN_OBJECT, "objects/%s", domainType).withHttpMethod(RestfulHttpMethod.POST).withArguments(domainObjectRepr);
309        getLinks().arrayAdd(persistLinkBuilder.build());
310    }
311
312    private DomainObjectReprRenderer asPersistLinkArguments() {
313        this.mode = Mode.PERSIST_LINK_ARGUMENTS;
314        return this;
315    }
316
317    private DomainObjectReprRenderer asUpdatePropertiesLinkArguments() {
318        this.mode = Mode.UPDATE_PROPERTIES_LINK_ARGUMENTS;
319        return this;
320    }
321
322    // not part of the spec
323    public DomainObjectReprRenderer asEventSerialization() {
324        this.mode = Mode.EVENT_SERIALIZATION;
325        return this;
326    }
327
328
329    private void addUpdatePropertiesLinkIfRequired() {
330        if(mode.isEventSerialization()) {
331            return;
332        }
333        if (!objectAdapter.representsPersistent()) {
334            return;
335        }
336        final boolean isService = objectAdapter.getSpecification().isService();
337        if(isService) {
338            return;
339        }
340
341        final DomainObjectReprRenderer renderer = new DomainObjectReprRenderer(getRendererContext(), null, JsonRepresentation.newMap());
342        final JsonRepresentation domainObjectRepr = renderer.with(objectAdapter).asUpdatePropertiesLinkArguments().render();
343
344        final LinkBuilder updateLinkBuilder = LinkBuilder.newBuilder(getRendererContext(), Rel.UPDATE.getName(), RepresentationType.DOMAIN_OBJECT, "objects/%s/%s", getDomainType(), getInstanceId()).withHttpMethod(RestfulHttpMethod.PUT).withArguments(domainObjectRepr);
345        getLinks().arrayAdd(updateLinkBuilder.build());
346    }
347
348    // ///////////////////////////////////////////////////////////////////
349    //
350    // ///////////////////////////////////////////////////////////////////
351
352    public static Object valueOrRef(final RendererContext resourceContext, final ObjectAdapter objectAdapter, final ObjectSpecification objectSpec) {
353        if(objectAdapter.isValue()) {
354            String format = null; // TODO
355            return JsonValueEncoder.asObject(objectAdapter, format);
356        }
357        final TitleFacet titleFacet = objectSpec.getFacet(TitleFacet.class);
358        final String title = titleFacet.title(objectAdapter, resourceContext.getLocalization());
359        return DomainObjectReprRenderer.newLinkToBuilder(resourceContext, Rel.VALUE, objectAdapter).withTitle(title).build();
360    }
361
362 
363    
364    // ///////////////////////////////////////////////////////////////////
365    // dependencies (from context)
366    // ///////////////////////////////////////////////////////////////////
367
368    protected static OidMarshaller getOidMarshaller() {
369                return IsisContext.getOidMarshaller();
370        }
371
372}