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}