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 }