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