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