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