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