001package de.deepamehta.webclient;
002
003import de.deepamehta.core.Association;
004import de.deepamehta.core.AssociationType;
005import de.deepamehta.core.DeepaMehtaType;
006import de.deepamehta.core.RelatedTopic;
007import de.deepamehta.core.Topic;
008import de.deepamehta.core.TopicType;
009import de.deepamehta.core.ViewConfiguration;
010import de.deepamehta.core.model.TopicModel;
011import de.deepamehta.core.osgi.PluginActivator;
012import de.deepamehta.core.service.Directive;
013import de.deepamehta.core.service.Directives;
014import de.deepamehta.core.service.event.AllPluginsActiveListener;
015import de.deepamehta.core.service.event.IntroduceTopicTypeListener;
016import de.deepamehta.core.service.event.IntroduceAssociationTypeListener;
017import de.deepamehta.core.service.event.PostUpdateTopicListener;
018import de.deepamehta.core.service.Transactional;
019
020import javax.ws.rs.Consumes;
021import javax.ws.rs.GET;
022import javax.ws.rs.Path;
023import javax.ws.rs.PathParam;
024import javax.ws.rs.Produces;
025import javax.ws.rs.QueryParam;
026
027import java.awt.Desktop;
028import java.net.URI;
029import java.util.Collection;
030import java.util.Iterator;
031import java.util.LinkedHashSet;
032import java.util.List;
033import java.util.Set;
034import java.util.concurrent.Callable;
035import java.util.logging.Logger;
036
037
038
039@Path("/webclient")
040@Consumes("application/json")
041@Produces("application/json")
042public class WebclientPlugin extends PluginActivator implements AllPluginsActiveListener,
043                                                                IntroduceTopicTypeListener,
044                                                                IntroduceAssociationTypeListener,
045                                                                PostUpdateTopicListener {
046
047    // ------------------------------------------------------------------------------------------------------- Constants
048
049    private static final String VIEW_CONFIG_LABEL = "View Configuration";
050
051    // ---------------------------------------------------------------------------------------------- Instance Variables
052
053    private boolean hasWebclientLaunched = false;
054
055    private Logger logger = Logger.getLogger(getClass().getName());
056
057    // -------------------------------------------------------------------------------------------------- Public Methods
058
059
060
061    // *************************
062    // *** Webclient Service ***
063    // *************************
064
065    // Note: the client service is provided as REST service only (OSGi service not required for the moment).
066
067
068
069    /**
070     * Performs a fulltext search and creates a search result topic.
071     */
072    @GET
073    @Path("/search")
074    @Transactional
075    public Topic searchTopics(@QueryParam("search") String searchTerm, @QueryParam("field")  String fieldUri) {
076        try {
077            logger.info("searchTerm=\"" + searchTerm + "\", fieldUri=\"" + fieldUri + "\"");
078            List<Topic> singleTopics = dm4.searchTopics(searchTerm, fieldUri);
079            Set<Topic> topics = findSearchableUnits(singleTopics);
080            logger.info(singleTopics.size() + " single topics found, " + topics.size() + " searchable units");
081            //
082            return createSearchTopic(searchTerm, topics);
083        } catch (Exception e) {
084            throw new RuntimeException("Searching topics failed", e);
085        }
086    }
087
088    /**
089     * Performs a by-type search and creates a search result topic.
090     * <p>
091     * Note: this resource method is actually part of the Type Search plugin.
092     * TODO: proper modularization. Either let the Type Search plugin provide its own REST resource (with
093     * another namespace again) or make the Type Search plugin an integral part of the Webclient plugin.
094     */
095    @GET
096    @Path("/search/by_type/{type_uri}")
097    @Transactional
098    public Topic getTopics(@PathParam("type_uri") String typeUri) {
099        try {
100            logger.info("typeUri=\"" + typeUri + "\"");
101            String searchTerm = dm4.getTopicType(typeUri).getSimpleValue() + "(s)";
102            List<Topic> topics = dm4.getTopicsByType(typeUri);
103            //
104            return createSearchTopic(searchTerm, topics);
105        } catch (Exception e) {
106            throw new RuntimeException("Searching topics of type \"" + typeUri + "\" failed", e);
107        }
108    }
109
110    // ---
111
112    @GET
113    @Path("/topic/{id}/related_topics")
114    public List<RelatedTopic> getRelatedTopics(@PathParam("id") long topicId) {
115        Topic topic = dm4.getTopic(topicId);
116        List<RelatedTopic> relTopics = topic.getRelatedTopics(null);   // assocTypeUri=null
117        Iterator<RelatedTopic> i = relTopics.iterator();
118        int removed = 0;
119        while (i.hasNext()) {
120            RelatedTopic relTopic = i.next();
121            if (isDirectModelledChildTopic(topic, relTopic)) {
122                i.remove();
123                removed++;
124            }
125        }
126        logger.fine("### " + removed + " topics are removed from result set of topic " + topicId);
127        return relTopics;
128    }
129
130
131
132    // ********************************
133    // *** Listener Implementations ***
134    // ********************************
135
136
137
138    @Override
139    public void allPluginsActive() {
140        String webclientUrl = getWebclientUrl();
141        //
142        if (hasWebclientLaunched == true) {
143            logger.info("### Launching webclient (url=\"" + webclientUrl + "\") ABORTED -- already launched");
144            return;
145        }
146        //
147        try {
148            logger.info("### Launching webclient (url=\"" + webclientUrl + "\")");
149            Desktop.getDesktop().browse(new URI(webclientUrl));
150            hasWebclientLaunched = true;
151        } catch (Exception e) {
152            logger.warning("### Launching webclient failed (" + e + ")");
153            logger.warning("### To launch it manually: " + webclientUrl);
154        }
155    }
156
157    /**
158     * Once a view configuration is updated in the DB we must update the cached view configuration model.
159     */
160    @Override
161    public void postUpdateTopic(Topic topic, TopicModel newModel, TopicModel oldModel) {
162        if (topic.getTypeUri().equals("dm4.webclient.view_config")) {
163            updateType(topic);
164            setConfigTopicLabel(topic);
165        }
166    }
167
168    // ---
169
170    @Override
171    public void introduceTopicType(TopicType topicType) {
172        setViewConfigLabel(topicType.getViewConfig());
173    }
174
175    @Override
176    public void introduceAssociationType(AssociationType assocType) {
177        setViewConfigLabel(assocType.getViewConfig());
178    }
179
180    // ------------------------------------------------------------------------------------------------- Private Methods
181
182
183
184    // === Search ===
185
186    // ### TODO: use Collection instead of Set
187    private Set<Topic> findSearchableUnits(List<? extends Topic> topics) {
188        Set<Topic> searchableUnits = new LinkedHashSet();
189        for (Topic topic : topics) {
190            if (searchableAsUnit(topic)) {
191                searchableUnits.add(topic);
192            } else {
193                List<RelatedTopic> parentTopics = topic.getRelatedTopics((String) null, "dm4.core.child",
194                    "dm4.core.parent", null);
195                if (parentTopics.isEmpty()) {
196                    searchableUnits.add(topic);
197                } else {
198                    searchableUnits.addAll(findSearchableUnits(parentTopics));
199                }
200            }
201        }
202        return searchableUnits;
203    }
204
205    /**
206     * Creates a "Search" topic.
207     */
208    private Topic createSearchTopic(final String searchTerm, final Collection<Topic> resultItems) {
209        try {
210            // We suppress standard workspace assignment here as a Search topic requires a special assignment.
211            // That is done by the Access Control module. ### TODO: refactoring. Do the assignment here.
212            return dm4.getAccessControl().runWithoutWorkspaceAssignment(new Callable<Topic>() {
213                @Override
214                public Topic call() {
215                    Topic searchTopic = dm4.createTopic(mf.newTopicModel("dm4.webclient.search",
216                        mf.newChildTopicsModel().put("dm4.webclient.search_term", searchTerm)
217                    ));
218                    // associate result items
219                    for (Topic resultItem : resultItems) {
220                        dm4.createAssociation(mf.newAssociationModel("dm4.webclient.search_result_item",
221                            mf.newTopicRoleModel(searchTopic.getId(), "dm4.core.default"),
222                            mf.newTopicRoleModel(resultItem.getId(), "dm4.core.default")
223                        ));
224                    }
225                    //
226                    return searchTopic;
227                }
228            });
229        } catch (Exception e) {
230            throw new RuntimeException("Creating search topic for \"" + searchTerm + "\" failed", e);
231        }
232    }
233
234    // ---
235
236    private boolean searchableAsUnit(Topic topic) {
237        TopicType topicType = dm4.getTopicType(topic.getTypeUri());
238        Boolean searchableAsUnit = (Boolean) getViewConfig(topicType, "searchable_as_unit");
239        return searchableAsUnit != null ? searchableAsUnit.booleanValue() : false;  // default is false
240    }
241
242    /**
243     * Read out a view configuration setting.
244     * <p>
245     * Compare to client-side counterpart: function get_view_config() in webclient.js
246     *
247     * @param   topicType   The topic type whose view configuration is read out.
248     * @param   setting     Last component of the setting URI, e.g. "icon".
249     *
250     * @return  The setting value, or <code>null</code> if there is no such setting
251     */
252    private Object getViewConfig(TopicType topicType, String setting) {
253        return topicType.getViewConfig("dm4.webclient.view_config", "dm4.webclient." + setting);
254    }
255
256
257
258    // === View Configuration ===
259
260    private void updateType(Topic viewConfig) {
261        Topic type = viewConfig.getRelatedTopic("dm4.core.aggregation", "dm4.core.view_config", "dm4.core.type", null);
262        if (type != null) {
263            String typeUri = type.getTypeUri();
264            if (typeUri.equals("dm4.core.topic_type") || typeUri.equals("dm4.core.meta_type")) {
265                updateTopicType(type, viewConfig);
266            } else if (typeUri.equals("dm4.core.assoc_type")) {
267                updateAssociationType(type, viewConfig);
268            } else {
269                throw new RuntimeException("View Configuration " + viewConfig.getId() + " is associated to an " +
270                    "unexpected topic (type=" + type + "\nviewConfig=" + viewConfig + ")");
271            }
272        } else {
273            // ### TODO: association definitions
274        }
275    }
276
277    // ---
278
279    private void updateTopicType(Topic type, Topic viewConfig) {
280        logger.info("### Updating view configuration of topic type \"" + type.getUri() + "\" (viewConfig=" +
281            viewConfig + ")");
282        TopicType topicType = dm4.getTopicType(type.getUri());
283        updateViewConfig(topicType, viewConfig);
284        Directives.get().add(Directive.UPDATE_TOPIC_TYPE, topicType);           // ### TODO: should be implicit
285    }
286
287    private void updateAssociationType(Topic type, Topic viewConfig) {
288        logger.info("### Updating view configuration of association type \"" + type.getUri() + "\" (viewConfig=" +
289            viewConfig + ")");
290        AssociationType assocType = dm4.getAssociationType(type.getUri());
291        updateViewConfig(assocType, viewConfig);
292        Directives.get().add(Directive.UPDATE_ASSOCIATION_TYPE, assocType);     // ### TODO: should be implicit
293    }
294
295    // ---
296
297    private void updateViewConfig(DeepaMehtaType type, Topic viewConfig) {
298        type.getViewConfig().updateConfigTopic(viewConfig.getModel());
299    }
300
301    // --- Label ---
302
303    private void setViewConfigLabel(ViewConfiguration viewConfig) {
304        for (Topic configTopic : viewConfig.getConfigTopics()) {
305            setConfigTopicLabel(configTopic);
306        }
307    }
308
309    private void setConfigTopicLabel(Topic viewConfig) {
310        viewConfig.setSimpleValue(VIEW_CONFIG_LABEL);
311    }
312
313
314
315    // === Webclient Start ===
316
317    private String getWebclientUrl() {
318        boolean isHttpsEnabled = Boolean.getBoolean("org.apache.felix.https.enable");
319        String protocol, port;
320        if (isHttpsEnabled) {
321            // Note: if both protocols are enabled HTTPS takes precedence
322            protocol = "https";
323            port = System.getProperty("org.osgi.service.http.port.secure");
324        } else {
325            protocol = "http";
326            port = System.getProperty("org.osgi.service.http.port");
327        }
328        return protocol + "://localhost:" + port + "/de.deepamehta.webclient/";
329    }
330
331
332
333    // === Misc ===
334
335    private boolean isDirectModelledChildTopic(Topic parentTopic, RelatedTopic childTopic) {
336        // association definition
337        if (hasAssocDef(parentTopic, childTopic)) {
338            // role types
339            Association assoc = childTopic.getRelatingAssociation();
340            if (assoc.isPlayer(mf.newTopicRoleModel(parentTopic.getId(), "dm4.core.parent")) &&
341                assoc.isPlayer(mf.newTopicRoleModel(childTopic.getId(),  "dm4.core.child"))) {
342                return true;
343            }
344        }
345        return false;
346    }
347
348    private boolean hasAssocDef(Topic parentTopic, RelatedTopic childTopic) {
349        // Note: the user might have no explicit READ permission for the type.
350        // We must enforce the *implicit* READ permission.
351        TopicType parentType = dm4.getTopicTypeImplicitly(parentTopic.getId());
352        //
353        String childTypeUri = childTopic.getTypeUri();
354        String assocTypeUri = childTopic.getRelatingAssociation().getTypeUri();
355        String assocDefUri = childTypeUri + "#" + assocTypeUri;
356        if (parentType.hasAssocDef(assocDefUri)) {
357            return true;
358        } else if (parentType.hasAssocDef(childTypeUri)) {
359            return parentType.getAssocDef(childTypeUri).getInstanceLevelAssocTypeUri().equals(assocTypeUri);
360        }
361        return false;
362    }
363}