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 }