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}