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    }