001package org.deepamehta.plugins.littlehelpers; 002 003import de.deepamehta.core.RelatedTopic; 004import java.util.logging.Logger; 005import java.util.List; 006 007import javax.ws.rs.GET; 008import javax.ws.rs.Path; 009import javax.ws.rs.PathParam; 010import javax.ws.rs.Produces; 011import javax.ws.rs.Consumes; 012 013import de.deepamehta.core.Topic; 014import de.deepamehta.core.TopicType; 015import de.deepamehta.core.model.ChildTopicsModel; 016 017import de.deepamehta.core.osgi.PluginActivator; 018import de.deepamehta.core.service.Inject; 019import de.deepamehta.plugins.time.TimeService; 020import de.deepamehta.plugins.workspaces.WorkspacesService; 021import java.util.ArrayList; 022import java.util.Collection; 023import java.util.Collections; 024import java.util.Comparator; 025import java.util.Date; 026import java.util.HashMap; 027import java.util.Iterator; 028import javax.ws.rs.core.MediaType; 029import org.codehaus.jettison.json.JSONArray; 030 031 032/** 033 * @author Malte Reißig (<malte@mikromedia.de>) 034 * @website http://github.com/mukil/dm4-littlehelpers 035 * @version 0.2 - compatible with DM 4.7 036 * 037 */ 038@Path("/helpers") 039@Consumes(MediaType.APPLICATION_JSON) 040@Produces(MediaType.APPLICATION_JSON) 041public class LittleHelpersPlugin extends PluginActivator implements LittleHelpersService { 042 043 private Logger log = Logger.getLogger(getClass().getName()); 044 045 // --- DeepaMehta Standard URIs 046 047 private final static String PROP_URI_CREATED = "dm4.time.created"; 048 private final static String PROP_URI_MODIFIED = "dm4.time.modified"; 049 050 // --- Custom Type Cache 051 private HashMap<String, TopicType> viewConfigTypeCache = new HashMap<String, TopicType>(); 052 053 /** private final static String CHILD_URI = "dm4.core.child"; 054 private final static String PARENT_URI = "dm4.core.parent"; 055 private final static String AGGREGATION = "dm4.core.aggregation"; **/ 056 057 @Inject WorkspacesService wsService; 058 @Inject TimeService timeService; 059 060 061 062 // -- 063 // --- Stableviews Utility Service 064 // -- 065 066 @GET 067 @Override 068 @Path("/suggest/topics/{input}") 069 public List<SuggestionViewModel> getTopicSuggestions(@PathParam("input") String query) { 070 if(query == null || query.length() < 2) throw new IllegalArgumentException("To receive " 071 + "suggestions, please provide at least two characters."); 072 List<SuggestionViewModel> suggestions = new ArrayList<SuggestionViewModel>(); 073 // three explicit search for topicmap name, usernames and note-titles ### add IndexMode.FULLTEXT_KEY ? 074 List<Topic> results = getTopicSuggestions(query, "dm4.topicmaps.name"); 075 results.addAll(getTopicSuggestions(query, "dm4.notes.title")); 076 results.addAll(getTopicSuggestions(query, "dm4.accesscontrol.username")); 077 // append the results of a generic fulltext search 078 List<Topic> naives = dms.searchTopics(query + "*", null); 079 if (naives != null) { 080 log.info("Naive search " + naives.size() + " length"); 081 results.addAll(naives); 082 } 083 // 084 log.info("> Checking for searchable units.. in " + results.size() ); 085 List<Topic> new_results = findSearchableUnits(results); 086 for (Topic t : new_results) { 087 log.fine("Suggesting \"" + t.getSimpleValue() + "\" topics (workspace=" + wsService.getAssignedWorkspace(t.getId())+ ")"); 088 suggestions.add(new SuggestionViewModel(t, wsService.getAssignedWorkspace(t.getId()))); 089 } 090 log.info("Suggesting " + suggestions.size() + " topics for input: " + query); 091 return suggestions; 092 } 093 094 @GET 095 @Override 096 @Path("/suggest/topics/{input}/{typeUri}") 097 public List<Topic> getTopicSuggestions(@PathParam("input") String query, 098 @PathParam("typeUri") String typeUri) { 099 return dms.searchTopics(query + "*", typeUri); 100 } 101 102 103 104 // -- 105 // --- Timeline Utility Service (Formerly eduZEN Notizen) 106 // -- 107 108 /** 109 * Fetches standard topics by time-range and time-value (created || modified). 110 */ 111 @GET 112 @Path("/by_time/{time_value}/{since}/{to}") 113 @Produces("application/json") 114 public String getStandardTopicsInTimeRange(@PathParam("time_value") String type, @PathParam("since") long since, 115 @PathParam("to") long to) { 116 JSONArray results = new JSONArray(); 117 try { 118 // 1) Fetch all topics in either "created" or "modified"-timestamp timerange 119 log.info("Fetching Standard Topics (\"" + type + "\") since: " + new Date(since) + " and " + new Date(to)); 120 ArrayList<Topic> standardTopics = new ArrayList<Topic>(); // items of interest 121 Collection<Topic> overallTopics = fetchAllTopicsInTimerange(type, since, to); 122 if (overallTopics.isEmpty()) log.info("getStandardTopicsInTimeRange("+type+") got NO result."); 123 Iterator<Topic> resultset = overallTopics.iterator(); 124 while (resultset.hasNext()) { 125 Topic in_question = resultset.next(); 126 if (in_question.getTypeUri().equals("dm4.notes.note") || 127 in_question.getTypeUri().equals("dm4.files.file") || 128 in_question.getTypeUri().equals("dm4.files.folder") || 129 in_question.getTypeUri().equals("dm4.contacts.person") || 130 in_question.getTypeUri().equals("dm4.contacts.institution") || 131 in_question.getTypeUri().equals("dm4.webbrowser.web_resource")) { 132 standardTopics.add(in_question); 133 } else { 134 // log.info("> Result \"" +in_question.getSimpleValue()+ "\" (" +in_question.getTypeUri()+ ")"); 135 } 136 } 137 log.info("Topics " + type + " in timerange query found " + standardTopics.size() + " standard topics"); 138 // 2) Sort all fetched items by their "created" or "modified" timestamps 139 ArrayList<Topic> in_memory_resources = null; 140 if (type.equals("created")) { 141 in_memory_resources = getTopicListSortedByCreationTime(standardTopics); 142 } else if (type.equals("modified")) { 143 in_memory_resources = getTopicListSortedByModificationTime(standardTopics); 144 } 145 // 3) Prepare the notes page-results view-model (per type of interest) 146 for (Topic item : in_memory_resources) { 147 try { 148 item.loadChildTopics(); 149 enrichTopicModelAboutCreationTimestamp(item); 150 enrichTopicModelAboutModificationTimestamp(item); 151 enrichTopicModelAboutIconConfigURL(item); 152 results.put(item.toJSON()); 153 } catch (RuntimeException rex) { 154 log.warning("Could not add fetched item to results, caused by: " + rex.getMessage()); 155 } 156 } 157 } catch (Exception e) { // e.g. a "RuntimeException" is thrown if the moodle-plugin is not installed 158 throw new RuntimeException("something went wrong", e); 159 } 160 return results.toString(); 161 } 162 163 /** 164 * Getting composites of all standard topics in given timerange. 165 */ 166 @GET 167 @Path("/timeindex/{time_value}/{since}/{to}") 168 @Produces("application/json") 169 public String getTopicIndexForTimeRange(@PathParam("time_value") String type, @PathParam("since") long since, 170 @PathParam("to") long to) { 171 // 172 JSONArray results = new JSONArray(); 173 try { 174 log.info("Populating Topic Index (\"" + type + "\") since: " + new Date(since) + " and " + new Date(to)); 175 // 1) Fetch Resultset of Resources 176 ArrayList<Topic> standardTopics = new ArrayList<Topic>(); 177 Collection<Topic> overallTopics = fetchAllTopicsInTimerange(type, since, to); 178 Iterator<Topic> resultset = overallTopics.iterator(); 179 while (resultset.hasNext()) { 180 Topic in_question = resultset.next(); 181 if (in_question.getTypeUri().equals("dm4.notes.note") || 182 in_question.getTypeUri().equals("dm4.files.file") || 183 in_question.getTypeUri().equals("dm4.files.folder") || 184 in_question.getTypeUri().equals("dm4.contacts.person") || 185 in_question.getTypeUri().equals("dm4.contacts.institution") || 186 in_question.getTypeUri().equals("dm4.webbrowser.web_resource")) { 187 // log.info("> " +in_question.getSimpleValue()+ " of type \"" +in_question.getTypeUri()+ "\""); 188 standardTopics.add(in_question); 189 } 190 } 191 log.info(type+" Topic Index for timerange query found " + standardTopics.size() + " standard topics (" + overallTopics.size() + " overall)"); 192 // 2) Sort and fetch resources 193 // ArrayList<RelatedTopic> in_memory_resources = getResultSetSortedByCreationTime(all_resources); 194 for (Topic item : standardTopics) { // 2) prepare resource items 195 // 3) Prepare the notes page-results view-model 196 item.loadChildTopics(); 197 enrichTopicModelAboutCreationTimestamp(item); 198 enrichTopicModelAboutModificationTimestamp(item); 199 results.put(item.toJSON()); 200 } 201 } catch (Exception e) { // e.g. a "RuntimeException" is thrown if the moodle-plugin is not installed 202 throw new RuntimeException("something went wrong", e); 203 } 204 return results.toString(); 205 } 206 207 public ArrayList<Topic> getTopicListSortedByCreationTime(ArrayList<Topic> all) { 208 Collections.sort(all, new Comparator<Topic>() { 209 public int compare(Topic t1, Topic t2) { 210 try { 211 Object one = t1.getProperty(PROP_URI_CREATED); 212 Object two = t2.getProperty(PROP_URI_CREATED); 213 if ( Long.parseLong(one.toString()) < Long.parseLong(two.toString()) ) return 1; 214 if ( Long.parseLong(one.toString()) > Long.parseLong(two.toString()) ) return -1; 215 } catch (Exception nfe) { 216 log.warning("Error while accessing timestamp of Topic 1: " + t1.getId() + " Topic2: " 217 + t2.getId() + " nfe: " + nfe.getMessage()); 218 return 0; 219 } 220 return 0; 221 } 222 }); 223 return all; 224 } 225 226 public ArrayList<Topic> getTopicListSortedByModificationTime(ArrayList<Topic> all) { 227 Collections.sort(all, new Comparator<Topic>() { 228 public int compare(Topic t1, Topic t2) { 229 try { 230 Object one = t1.getProperty(PROP_URI_MODIFIED); 231 Object two = t2.getProperty(PROP_URI_MODIFIED); 232 if ( Long.parseLong(one.toString()) < Long.parseLong(two.toString()) ) return 1; 233 if ( Long.parseLong(one.toString()) > Long.parseLong(two.toString()) ) return -1; 234 } catch (Exception nfe) { 235 log.warning("Error while accessing timestamp of Topic 1: " + t1.getId() + " Topic2: " 236 + t2.getId() + " nfe: " + nfe.getMessage()); 237 return 0; 238 } 239 return 0; 240 } 241 }); 242 return all; 243 } 244 245 // --- Private Utility Methods 246 247 private Collection<Topic> fetchAllTopicsInTimerange(String type, long since, long to) { 248 Collection<Topic> topics = null; 249 if (type.equals("created")) { 250 topics = timeService.getTopicsByCreationTime(since, to); 251 log.fine("> Queried " +topics.size()+ " elements CREATED since: " + new Date(since) + " and " + new Date(to)); 252 } else if (type.equals("modified")) { 253 topics = timeService.getTopicsByModificationTime(since, to); 254 log.fine("> Queried " +topics.size()+ " elements MODIFIED since: " + new Date(since) + " and " + new Date(to)); 255 } else { 256 throw new RuntimeException("Wrong parameter: set time_value either to \"created\" or \"modified\""); 257 } 258 return topics; 259 } 260 261 private void enrichTopicModelAboutIconConfigURL(Topic element) { 262 TopicType topicType = null; 263 if (viewConfigTypeCache.containsKey(element.getTypeUri())) { 264 topicType = viewConfigTypeCache.get(element.getTypeUri()); 265 } else { 266 topicType = dms.getTopicType(element.getTypeUri()); 267 viewConfigTypeCache.put(element.getTypeUri(), topicType); 268 } 269 Object iconUrl = getViewConfig(topicType, "icon"); 270 if (iconUrl != null) { 271 ChildTopicsModel resourceModel = element.getChildTopics().getModel(); 272 resourceModel.put("dm4.webclient.icon", iconUrl.toString()); 273 } 274 } 275 276 private void enrichTopicModelAboutCreationTimestamp(Topic resource) { 277 long created = timeService.getCreationTime(resource.getId()); 278 ChildTopicsModel resourceModel = resource.getChildTopics().getModel(); 279 resourceModel.put(PROP_URI_CREATED, created); 280 } 281 282 private void enrichTopicModelAboutModificationTimestamp(Topic resource) { 283 long created = timeService.getModificationTime(resource.getId()); 284 ChildTopicsModel resourceModel = resource.getChildTopics().getModel(); 285 resourceModel.put(PROP_URI_MODIFIED, created); 286 } 287 288 /** Taken from the WebclientPlugin.java by Jörg Richter */ 289 private List<Topic> findSearchableUnits(List<? extends Topic> topics) { 290 List<Topic> searchableUnits = new ArrayList<Topic>(); 291 for (Topic topic : topics) { 292 if (searchableAsUnit(topic)) { 293 searchableUnits.add(topic); 294 } else { 295 List<RelatedTopic> parentTopics = topic.getRelatedTopics((String) null, "dm4.core.child", 296 "dm4.core.parent", null, 0).getItems(); 297 if (parentTopics.isEmpty()) { 298 searchableUnits.add(topic); 299 } else { 300 searchableUnits.addAll(findSearchableUnits(parentTopics)); 301 } 302 } 303 } 304 return searchableUnits; 305 } 306 307 private boolean searchableAsUnit(Topic topic) { 308 TopicType topicType = dms.getTopicType(topic.getTypeUri()); 309 Boolean searchableAsUnit = (Boolean) getViewConfig(topicType, "searchable_as_unit"); 310 return searchableAsUnit != null ? searchableAsUnit.booleanValue() : false; // default is false 311 } 312 313 /** 314 * Read out a view configuration setting. 315 * <p> 316 * Compare to client-side counterpart: function get_view_config() in webclient.js 317 * 318 * @param topicType The topic type whose view configuration is read out. 319 * @param setting Last component of the setting URI, e.g. "icon". 320 * 321 * @return The setting value, or <code>null</code> if there is no such setting 322 */ 323 private Object getViewConfig(TopicType topicType, String setting) { 324 return topicType.getViewConfig("dm4.webclient.view_config", "dm4.webclient." + setting); 325 } 326 327}