001package de.deepamehta.plugins.tags; 002 003import java.util.logging.Logger; 004import java.util.Iterator; 005import java.util.LinkedHashSet; 006import java.util.List; 007import java.util.Set; 008 009import javax.ws.rs.GET; 010import javax.ws.rs.POST; 011import javax.ws.rs.Path; 012import javax.ws.rs.PathParam; 013import javax.ws.rs.Produces; 014import javax.ws.rs.Consumes; 015import javax.ws.rs.WebApplicationException; 016 017import de.deepamehta.core.Topic; 018import de.deepamehta.core.model.TopicModel; 019import de.deepamehta.core.model.RelatedTopicModel; 020import de.deepamehta.core.RelatedTopic; 021import de.deepamehta.core.model.ChildTopicsModel; 022import de.deepamehta.core.model.RelatedTopicModel; 023import de.deepamehta.core.model.SimpleValue; 024 025import de.deepamehta.core.service.Inject; 026import org.codehaus.jettison.json.JSONArray; 027import org.codehaus.jettison.json.JSONException; 028import org.codehaus.jettison.json.JSONObject; 029 030import de.deepamehta.core.osgi.PluginActivator; 031import de.deepamehta.core.service.Inject; 032import de.deepamehta.core.service.ResultList; 033import de.deepamehta.plugins.workspaces.WorkspacesService; 034import de.deepamehta.core.storage.spi.DeepaMehtaTransaction; 035import de.deepamehta.plugins.tags.TaggingService; 036import de.deepamehta.plugins.workspaces.WorkspacesService; 037 038import java.util.*; 039 040 041/** 042 * A basic plugin-service for fetching topics in DeepaMehta 4 by type and <em>one</em> or <em>many</em> tags. 043 * 044 * @author Malte Reißig (<malte@mikromedia.de>) 045 * @website http://github.com/mukil/dm4.tags 046 * @version 1.3.9-SNAPSHOT compatible with DeepaMehta 4.7 047 */ 048 049@Path("/tag") 050@Consumes("application/json") 051public class TaggingPlugin extends PluginActivator implements TaggingService { 052 053 private Logger log = Logger.getLogger(getClass().getName()); 054 055 // --- DeepaMehta Standard URIs 056 057 private final static String CHILD_URI = "dm4.core.child"; 058 private final static String PARENT_URI = "dm4.core.parent"; 059 private final static String AGGREGATION = "dm4.core.aggregation"; 060 061 // --- Additional View Model URIs 062 063 public static final String VIEW_RELATED_COUNT_URI = "view_related_count"; 064 public static final String VIEW_CSS_CLASS_COUNT_URI = "view_css_class"; 065 066 // --- Service used in Migration3 to fix Workspace Assignments 067 068 @Inject 069 private WorkspacesService workspaceService; 070 071 /** 072 * Convenience method to fetch a list of topics (of a given <code>Topic Type</code>) using the "Tag" 073 * with the given <code>topic id</code>. 074 * 075 * Note: This method provides no real benefit for developers familiar with the getRelatedTopics() of the 076 * deepamehta-core API. It's just a convenient call. 077 * 078 * @param tagId A topic id ot a "dm4.tags.tag"-Topic 079 * @param relatedTopicTypeUri A Topic Type URI identifying a type of topics. The <code>Topic Type 080 * Definition</code> is expected to have an <code>Aggregation Definition</code> with 081 * cardinality set to <code>Many</code> to the Topic Type Tag (typeUri="dm4.tags.tag"). 082 */ 083 @GET 084 @Path("/{tagId}/{relatedTypeUri}") 085 @Produces("application/json") 086 @Override 087 public ResultList<RelatedTopic> getTopicsByTagAndTypeURI(@PathParam("tagId") long tagId, 088 @PathParam("relatedTypeUri") String relatedTopicTypeUri) { 089 ResultList<RelatedTopic> all_results = null; 090 try { 091 Topic givenTag = dms.getTopic(tagId); 092 all_results = givenTag.getRelatedTopics(AGGREGATION, CHILD_URI, 093 PARENT_URI, relatedTopicTypeUri, 0); 094 return all_results; 095 } catch (Exception e) { 096 throw new WebApplicationException(new RuntimeException("Something went wrong fetching tagged topics", e)); 097 } 098 } 099 100 /** 101 * Convenience method to fetch a list of topics (of a given <code>Topic Type</code>) using a list of "Tags". 102 * 103 * @param tags A String of a JSONObject containing a JSONArray (under the 104 * </code>key="tags"</code>) of "dm4.tags.tag"-Topics is expected. 105 * For example: <code>"{ "tags": [ { "id": 1234 } ] }"</code>). 106 * @param relatedTopicTypeUri A type_uri of a composite (e.g. "org.deepamehta.resources.resource") 107 * which must aggregate one or many "dm4.tags.tag". 108 */ 109 @POST 110 @Path("/by_many/{relatedTypeUri}") 111 @Consumes("application/json") 112 @Produces("application/json") 113 @Override 114 public ResultList<RelatedTopic> getTopicsByTagsAndTypeUri(String tags, @PathParam("relatedTypeUri") 115 String relatedTopicTypeUri) { 116 ResultList<RelatedTopic> result = null; 117 try { 118 JSONObject tagList = new JSONObject(tags); 119 if (tagList.has("tags")) { 120 JSONArray all_tags = tagList.getJSONArray("tags"); 121 // 1) if this method is called with more than 1 tag, we proceed with 122 if (all_tags.length() > 1) { 123 // 2) fetching all topics related to the very first tag given 124 JSONObject tagOne = all_tags.getJSONObject(0); 125 long first_id = tagOne.getLong("id"); 126 Topic givenTag = dms.getTopic(first_id); 127 result = givenTag.getRelatedTopics(AGGREGATION, CHILD_URI, PARENT_URI, 128 relatedTopicTypeUri, 0); 129 // 3) Iterate over all topics tagged with this (one) tag 130 Set<RelatedTopic> missmatches = new LinkedHashSet<RelatedTopic>(); 131 Iterator<RelatedTopic> iterator = result.iterator(); 132 while (iterator.hasNext()) { 133 // 4) To check on each resource if it does relate to ALL given tags 134 RelatedTopic resource = iterator.next(); 135 remove: 136 for (int i=1; i < all_tags.length(); i++) { 137 JSONObject tag = all_tags.getJSONObject(i); 138 long t_id = tag.getLong("id"); 139 // Topic tag_to_check = dms.getTopic(t_id, false); 140 if (!hasRelatedTopicTag(resource, t_id)) { // if just one tag is missing, mark for removal 141 missmatches.add(resource); 142 break remove; 143 } 144 } 145 } 146 // 5) remove all "not-matching" items from our initial resultset 147 for (Iterator<RelatedTopic> it = missmatches.iterator(); it.hasNext();) { 148 RelatedTopic topic = it.next(); 149 result.getItems().remove(topic); 150 // 6) check if any "not-matching" items is still part of our resultset (doubling) 151 if (result.getItems().contains(topic)) { 152 log.warning("DATA INCONSISTENCY:" + topic.getId() + " has two associations to the first " 153 + "given-tag ("+givenTag.getSimpleValue() +")"); 154 } 155 } 156 return result; 157 } else { 158 // fixme: tags-array may contain < 0 items 159 JSONObject tagOne = all_tags.getJSONObject(0); 160 long first_id = tagOne.getLong("id"); 161 return getTopicsByTagAndTypeURI(first_id, relatedTopicTypeUri); // and pass it on 162 } 163 } 164 throw new IllegalArgumentException("no tags given"); 165 } catch (JSONException ex) { 166 throw new RuntimeException("error while parsing given parameters", ex); 167 } catch (WebApplicationException e) { 168 throw new RuntimeException("something went wrong", e); 169 } 170 } 171 172 /** 173 * Generate a JSONArray with properties helpful to produce an interactive tag cloud. 174 * 175 * @param relatedTopicTypeUri Type URI of related Topic Type counted. 176 * @return String Containing a JSON Object with some properties helpful for visualization (including CSS class 177 * names). 178 */ 179 @GET 180 @Path("/with_related_count/{related_type_uri}") 181 @Produces("application/json") 182 public String getViewTagsModelWithRelatedCount(@PathParam("related_type_uri") String relatedTopicTypeUri) { 183 // 184 JSONArray results = new JSONArray(); 185 try { 186 // 1) Fetch Resultset of Resources 187 log.info("Counting all related topics of type \"" + relatedTopicTypeUri + "\""); 188 ArrayList<Topic> prepared_topics = new ArrayList<Topic>(); 189 ResultList<RelatedTopic> all_tags = dms.getTopics(TAG_URI, 0); 190 log.info("Identified " + all_tags.getSize() + " tags"); 191 // 2) Prepare view model of each result item 192 Iterator<RelatedTopic> resultset = all_tags.getItems().iterator(); 193 while (resultset.hasNext()) { 194 Topic in_question = resultset.next(); 195 int count = in_question.getRelatedTopics(AGGREGATION, CHILD_URI, PARENT_URI, 196 relatedTopicTypeUri, 0).getSize(); 197 enrichTopicViewModelAboutRelatedCount(in_question, count); 198 prepared_topics.add(in_question); 199 } 200 // 3) sort all result-items by the number of related-topics (of given type) 201 Collections.sort(prepared_topics, new Comparator<Topic>() { 202 public int compare(Topic t1, Topic t2) { 203 int one = t1.getChildTopics().getInt(VIEW_RELATED_COUNT_URI); 204 int two = t2.getChildTopics().getInt(VIEW_RELATED_COUNT_URI); 205 if ( one < two ) return 1; 206 if ( one > two ) return -1; 207 return 0; 208 } 209 }); 210 // 4) Turn over to JSON Array and add a computed css-class (indicating the "weight" of a tag) 211 for (Topic item : prepared_topics) { // 2) prepare resource items 212 enrichTopicViewModelAboutCSSClass(item, item.getChildTopics().getInt(VIEW_RELATED_COUNT_URI)); 213 results.put(item.toJSON()); 214 } 215 return results.toString(); 216 } catch (Exception e) { 217 throw new RuntimeException("something went wrong", e); 218 } 219 } 220 221 /** 222 * Creates a topic of type "dm4.tags.tag" after checking for existence by name and trimming the name. 223 * If a tag with the given name ("dm4.tags.label") already exists, an <code>IllegalArgumentException</code> is 224 * thrown. 225 * 226 * @param name Label of the tag. 227 * @param definition Definition the label stands for. 228 * @param lowerCase Do existence check after converting the given name with a <code>.toLowerCase()</code> call. 229 * 230 * @return Topic The complete tag topic created. 231 **/ 232 @Override 233 public Topic createTagTopic(String name, String definition, boolean lowerCase) throws IllegalArgumentException { 234 Topic topic = null; 235 // 1 check for existence 236 String strippedName = name.trim(); 237 if (lowerCase) strippedName = name.toLowerCase(); 238 Topic existingTag = dms.getTopic(TAG_LABEL_URI, new SimpleValue(strippedName)); 239 if (existingTag != null) { 240 throw new IllegalArgumentException("A Tag with the name \""+strippedName+"\" already exists - NOT CREATED"); 241 } 242 // 2 create 243 DeepaMehtaTransaction tx = dms.beginTx(); 244 try { 245 topic = dms.createTopic(new TopicModel(TAG_URI, new ChildTopicsModel() 246 .put(TAG_LABEL_URI, strippedName).put(TAG_DEFINITION_URI, definition))); 247 tx.success(); 248 } finally { 249 tx.finish(); 250 } 251 return topic; 252 } 253 254 /** 255 * Returns a topic (typeUri="dm4.tags.tag") with the given name. 256 * 257 * @param name label of given tag 258 * @param caseSensitive flag if to use toLowerCase when getting 259 * @return Topic The topic or <code>null</code> if no topic was found under the given name. 260 */ 261 @Override 262 public Topic getTagTopic(String name, boolean caseSensitive) { 263 Topic tagTopic = null; 264 String tagName = name.trim(); 265 if (!caseSensitive) tagName = tagName.toLowerCase(); 266 // ### This getTopic-call might throw a "Unauthorized" Exception as of DM 4.7, no? 267 Topic labelTopic = dms.getTopic(TAG_LABEL_URI, new SimpleValue(tagName)); 268 if (labelTopic != null) { 269 tagTopic = labelTopic.getRelatedTopic("dm4.core.composition", "dm4.core.child", 270 "dm4.core.parent", TAG_URI); 271 } 272 return tagTopic; 273 } 274 275 276 /** Private Helper Methods */ 277 278 private void enrichTopicViewModelAboutRelatedCount(Topic resource, int count) { 279 ChildTopicsModel resourceModel = resource.getChildTopics().getModel(); 280 resourceModel.put(VIEW_RELATED_COUNT_URI, count); 281 } 282 283 private void enrichTopicViewModelAboutCSSClass(Topic resource, int related_count) { 284 ChildTopicsModel resourceModel = resource.getChildTopics().getModel(); 285 String className = "few"; 286 if (related_count > 5) className = "some"; 287 if (related_count > 15) className = "quitesome"; 288 if (related_count > 25) className = "more"; 289 if (related_count > 50) className = "many"; 290 if (related_count > 70) className = "manymore"; 291 resourceModel.put(VIEW_CSS_CLASS_COUNT_URI, className); 292 } 293 294 private boolean hasRelatedTopicTag(RelatedTopic resource, long tagId) { 295 ChildTopicsModel topicModel = resource.getChildTopics().getModel(); 296 if (topicModel.has(TAG_URI)) { 297 List<RelatedTopicModel> tags = topicModel.getTopics(TAG_URI); 298 for (int i = 0; i < tags.size(); i++) { 299 RelatedTopicModel resourceTag = tags.get(i); 300 if (resourceTag.getId() == tagId) return true; 301 } 302 } 303 return false; 304 } 305 306}