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, @QueryParam("max_result_size") int maxResultSize) { 107 try { 108 logger.info("typeUri=\"" + typeUri + "\", maxResultSize=" + maxResultSize); 109 String searchTerm = dms.getTopicType(typeUri).getSimpleValue() + "(s)"; 110 List<RelatedTopic> topics = dms.getTopics(typeUri, maxResultSize).getItems(); 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 // 233 // associate result items 234 logger.fine("Associating " + resultItems.size() + " result items to search (ID " + searchTopic.getId() + ")"); 235 for (Topic resultItem : resultItems) { 236 logger.fine("Associating " + resultItem); 237 dms.createAssociation(new AssociationModel("dm4.webclient.search_result_item", 238 new TopicRoleModel(searchTopic.getId(), "dm4.core.default"), 239 new TopicRoleModel(resultItem.getId(), "dm4.core.default") 240 )); 241 } 242 return searchTopic; 243 } 244 245 // --- 246 247 private boolean searchableAsUnit(Topic topic) { 248 TopicType topicType = dms.getTopicType(topic.getTypeUri()); 249 Boolean searchableAsUnit = (Boolean) getViewConfig(topicType, "searchable_as_unit"); 250 return searchableAsUnit != null ? searchableAsUnit.booleanValue() : false; // default is false 251 } 252 253 /** 254 * Read out a view configuration setting. 255 * <p> 256 * Compare to client-side counterpart: function get_view_config() in webclient.js 257 * 258 * @param topicType The topic type whose view configuration is read out. 259 * @param setting Last component of the setting URI, e.g. "icon". 260 * 261 * @return The setting value, or <code>null</code> if there is no such setting 262 */ 263 private Object getViewConfig(TopicType topicType, String setting) { 264 return topicType.getViewConfig("dm4.webclient.view_config", "dm4.webclient." + setting); 265 } 266 267 268 269 // === View Configuration === 270 271 private void updateType(Topic viewConfig) { 272 Topic type = viewConfig.getRelatedTopic("dm4.core.aggregation", "dm4.core.view_config", "dm4.core.type", null); 273 if (type != null) { 274 String typeUri = type.getTypeUri(); 275 if (typeUri.equals("dm4.core.topic_type") || typeUri.equals("dm4.core.meta_type")) { 276 updateTopicType(type, viewConfig); 277 } else if (typeUri.equals("dm4.core.assoc_type")) { 278 updateAssociationType(type, viewConfig); 279 } else { 280 throw new RuntimeException("View Configuration " + viewConfig.getId() + " is associated to an " + 281 "unexpected topic (type=" + type + "\nviewConfig=" + viewConfig + ")"); 282 } 283 } else { 284 // ### TODO: association definitions 285 } 286 } 287 288 // --- 289 290 private void updateTopicType(Topic type, Topic viewConfig) { 291 logger.info("### Updating view configuration of topic type \"" + type.getUri() + "\" (viewConfig=" + 292 viewConfig + ")"); 293 TopicType topicType = dms.getTopicType(type.getUri()); 294 updateViewConfig(topicType, viewConfig); 295 Directives.get().add(Directive.UPDATE_TOPIC_TYPE, topicType); 296 } 297 298 private void updateAssociationType(Topic type, Topic viewConfig) { 299 logger.info("### Updating view configuration of association type \"" + type.getUri() + "\" (viewConfig=" + 300 viewConfig + ")"); 301 AssociationType assocType = dms.getAssociationType(type.getUri()); 302 updateViewConfig(assocType, viewConfig); 303 Directives.get().add(Directive.UPDATE_ASSOCIATION_TYPE, assocType); 304 } 305 306 // --- 307 308 private void updateViewConfig(Type type, Topic viewConfig) { 309 type.getViewConfig().updateConfigTopic(viewConfig.getModel()); 310 } 311 312 // --- Label --- 313 314 private void setViewConfigLabel(ViewConfiguration viewConfig) { 315 for (Topic configTopic : viewConfig.getConfigTopics()) { 316 setConfigTopicLabel(configTopic); 317 } 318 } 319 320 private void setConfigTopicLabel(Topic viewConfig) { 321 viewConfig.setSimpleValue(VIEW_CONFIG_LABEL); 322 } 323 324 325 326 // === Webclient Start === 327 328 private String getWebclientUrl() { 329 boolean isHttpsEnabled = Boolean.getBoolean("org.osgi.service.http.secure.enabled"); 330 String protocol, port; 331 if (isHttpsEnabled) { 332 // Note: if both protocols are enabled HTTPS takes precedence 333 protocol = "https"; 334 port = System.getProperty("org.osgi.service.http.port.secure"); 335 } else { 336 protocol = "http"; 337 port = System.getProperty("org.osgi.service.http.port"); 338 } 339 return protocol + "://localhost:" + port + "/de.deepamehta.webclient/"; 340 } 341 342 343 344 // === Misc === 345 346 private boolean isDirectModelledChildTopic(RelatedTopic childTopic, Topic parentTopic) { 347 // association definition 348 TopicType parentType = dms.getTopicType(parentTopic.getTypeUri()); 349 String childTypeUri = childTopic.getTypeUri(); 350 if (parentType.hasAssocDef(childTypeUri)) { 351 // association type 352 AssociationDefinition assocDef = parentType.getAssocDef(childTypeUri); 353 Association assoc = childTopic.getRelatingAssociation(); 354 if (assocDef.getInstanceLevelAssocTypeUri().equals(assoc.getTypeUri())) { 355 // role types 356 if (assoc.isPlayer(new TopicRoleModel(parentTopic.getId(), "dm4.core.parent")) && 357 assoc.isPlayer(new TopicRoleModel(childTopic.getId(), "dm4.core.child"))) { 358 return true; 359 } 360 } 361 } 362 return false; 363 } 364 }