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