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}