001    package org.deepamehta.plugins.moodle;
002    
003    import de.deepamehta.core.Association;
004    import de.deepamehta.core.DeepaMehtaObject;
005    import de.deepamehta.core.RelatedTopic;
006    import de.deepamehta.core.Topic;
007    import de.deepamehta.core.model.*;
008    import de.deepamehta.core.osgi.PluginActivator;
009    import de.deepamehta.core.service.Directives;
010    import de.deepamehta.core.service.PluginService;
011    import de.deepamehta.core.service.ResultList;
012    import de.deepamehta.core.service.annotation.ConsumesService;
013    import de.deepamehta.core.service.event.AllPluginsActiveListener;
014    import de.deepamehta.core.storage.spi.DeepaMehtaTransaction;
015    import de.deepamehta.core.util.JavaUtils;
016    import de.deepamehta.plugins.accesscontrol.event.PostLoginUserListener;
017    import de.deepamehta.plugins.accesscontrol.model.ACLEntry;
018    import de.deepamehta.plugins.accesscontrol.model.AccessControlList;
019    import de.deepamehta.plugins.accesscontrol.model.Operation;
020    import de.deepamehta.plugins.accesscontrol.model.UserRole;
021    import de.deepamehta.plugins.accesscontrol.service.AccessControlService;
022    import java.io.*;
023    import java.net.HttpURLConnection;
024    import java.net.MalformedURLException;
025    import java.net.URL;
026    import java.security.KeyManagementException;
027    import java.security.KeyStore;
028    import java.security.KeyStoreException;
029    import java.security.NoSuchAlgorithmException;
030    import java.security.cert.CertificateException;
031    import java.util.Date;
032    import java.util.List;
033    import java.util.Properties;
034    import java.util.logging.Level;
035    import java.util.logging.Logger;
036    import javax.net.ssl.HttpsURLConnection;
037    import javax.net.ssl.SSLContext;
038    import javax.net.ssl.TrustManagerFactory;
039    import javax.ws.rs.*;
040    import javax.ws.rs.core.MediaType;
041    import org.codehaus.jettison.json.JSONArray;
042    import org.codehaus.jettison.json.JSONException;
043    import org.codehaus.jettison.json.JSONObject;
044    
045    
046    /**
047     *
048     * A webservice-client enabling collaborative mapping on course-materials (and more) for any moodle
049     * (2.4+) course. A very simple plugin to connect users of DeepaMehta 4 with a moodle installation.
050     *
051     * @author Malte Reißig (<malte@mikromedia.de>)
052     * @website https://github.com/mukil/mapping-moodle
053     * @version 1.2.1-SNAPSHOT
054     *
055     */
056    
057    @Path("/moodle")
058    public class MoodleServiceClient extends PluginActivator implements PostLoginUserListener,
059                                                                        AllPluginsActiveListener {
060    
061        private static Logger log = Logger.getLogger(MoodleServiceClient.class.getName());
062    
063        // ---------------------------------------------------------------------------------------------- Instance Variables
064    
065        // ### Consume NotifcationService
066        private AccessControlService aclService;
067    
068        // --- URIs DeepaMehta and all plugins in use
069    
070        public static final String WS_MOODLE_NAME = "ISIS / Moodle";
071        public static final String WS_MOODLE_URI = "org.deepamehta.workspaces.moodle";
072        public static final String MOODLE_PARTICIPANT_EDGE = "org.deepamehta.moodle.course_participant";
073    
074        public static final String MOODLE_COURSE_URI = "org.deepamehta.moodle.course";
075        public static final String MOODLE_COURSE_NAME_URI = "org.deepamehta.moodle.course_name";
076        public static final String MOODLE_COURSE_SHORT_NAME_URI = "org.deepamehta.moodle.course_short_name";
077    
078        public static final String MOODLE_SECTION_URI = "org.deepamehta.moodle.section";
079        public static final String MOODLE_SECTION_NAME_URI = "org.deepamehta.moodle.section_name";
080        public static final String MOODLE_SECTION_SUMMARY_URI = "org.deepamehta.moodle.section_summary";
081        public static final String MOODLE_SECTION_ORDINAL_NR = "org.deepamehta.moodle.section_ordinal_nr";
082    
083        public static final String MOODLE_ITEM_URI = "org.deepamehta.moodle.item";
084        public static final String MOODLE_ITEM_NAME_URI = "org.deepamehta.moodle.item_name";
085        public static final String MOODLE_ITEM_ICON_URI = "org.deepamehta.moodle.item_icon";
086        public static final String MOODLE_ITEM_REMOTE_URL_URI = "org.deepamehta.moodle.item_url";
087        public static final String MOODLE_ITEM_MEDIA_TYPE_URI = "org.deepamehta.moodle.item_media_type";
088        public static final String MOODLE_ITEM_DESC_URI = "org.deepamehta.moodle.item_description";
089        public static final String MOODLE_ITEM_HREF_URI = "org.deepamehta.moodle.item_href";
090        public static final String MOODLE_ITEM_TYPE_URI = "org.deepamehta.moodle.item_type";
091        public static final String MOODLE_ITEM_MODIFIED_URI = "org.deepamehta.moodle.item_modified";
092        public static final String MOODLE_ITEM_CREATED_URI = "org.deepamehta.moodle.item_created";
093        public static final String MOODLE_ITEM_AUTHOR_URI = "org.deepamehta.moodle.item_author";
094        public static final String MOODLE_ITEM_LICENSE_URI = "org.deepamehta.moodle.item_license";
095        public static final String MOODLE_ITEM_SIZE_URI = "org.deepamehta.moodle.item_size";
096    
097        // --- Tagging Notes Plugin URI
098    
099        public static final String PLUGIN_URI_TAGGING_NOTES = "org.deepamehta.eduzen-tagging-notes";
100    
101        // --- Data instance URIs for ISIS 2
102    
103        public static final String ISIS_COURSE_URI_PREFIX = "de.tu-berlin.course.";
104        public static final String ISIS_SECTION_URI_PREFIX = "de.tu-berlin.section.";
105        public static final String ISIS_ITEM_URI_PREFIX = "de.tu-berlin.item.";
106    
107        // --- ISIS 2 Webservice related URIs
108    
109        private final String CLIENT_OPTION_ENDPOINT_URL_URI = "org.deepamehta.moodle.web_service_endpoint_url";
110        private final String CLIENT_OPTION_USE_HTTPS = "org.deepamehta.moodle.use_https";
111        private final String CLIENT_OPTION_JAVA_KEY_STORE_PATH = "org.deepamehta.moodle.jks_path";
112        private final String CLIENT_OPTION_JAVA_KEY_STORE_PASS = "org.deepamehta.moodle.jks_pass";
113        // The Username eligible to
114        // a) edit the moodle service-settings (endpoint) and all other system-created items, as well is allowed to
115        // b) set "Tags" on each "Moodle Course" (which is necessary) before they can be synced
116        private final String USERNAME_OF_SETTINGS_ADMINISTRATOR = "Malte";
117        private final String MOODLE_SECURITY_KEY_URI = "org.deepamehta.moodle.security_key";
118        private final String MOODLE_USER_ID_URI = "org.deepamehta.moodle.user_id";
119        private final String MOODLE_SERVICE_NAME = "eduzen_web_service";
120        private final String MOODLE_SERVICE_FORMAT = "moodlewsrestformat=json";
121    
122        // --- Deepamehta 4 URIs
123    
124        private final String DEFAULT_ROLE_TYPE_URI = "dm4.core.default";
125        private final String CHILD_ROLE_TYPE_URI = "dm4.core.child";
126        private final String PARENT_ROLE_TYPE_URI = "dm4.core.parent";
127        private final String AGGREGATION_TYPE_URI = "dm4.core.aggregation";
128        private final String COMPOSITION_TYPE_URI = "dm4.core.composition";
129        private final String USER_ACCOUNT_TYPE_URI = "dm4.accesscontrol.user_account";
130        private final String USER_NAME_TYPE_URI = "dm4.accesscontrol.username";
131        private final String CHILD_URI = "dm4.core.child";
132        private final String PARENT_URI = "dm4.core.parent";
133        private final String TAG_URI = "dm4.tags.tag";
134        private final String REVIEW_SCORE_URI = "org.deepamehta.reviews.score";
135    
136    
137        // -------------------------------------------------------------------------------------------------- Public Methods
138    
139        // --
140        // --- Hook implementations
141        // --
142    
143        @Override
144        public void init() {
145            if (aclService != null) {
146                // 1) Note: Skipping to correct ACL of migration1 topics because of a ws-topic edit issue in DM 4.2
147                Topic moodleWs = dms.getTopic("uri", new SimpleValue(WS_MOODLE_URI), false);
148                setDefaultMoodleAdminACLEntries(moodleWs);
149                // 2) Correct ACL of topics in migration4
150                ResultList<RelatedTopic> client_options = dms.getTopics("org.deepamehta.moodle.web_service_client_option",
151                        false, 0);
152                for (RelatedTopic client_option : client_options) {
153                    setDefaultMoodleAdminACLEntries(client_option);
154                }
155            }
156        }
157    
158        @Override
159        @ConsumesService({
160            "de.deepamehta.plugins.accesscontrol.service.AccessControlService"
161        })
162        public void serviceArrived(PluginService service) {
163            if (service instanceof AccessControlService) {
164                aclService = (AccessControlService) service;
165            }
166        }
167    
168        @Override
169        public void serviceGone(PluginService service) {
170            if (service instanceof AccessControlService) {
171                aclService = null;
172            }
173        }
174    
175        @Override
176        public void postLoginUser(String username) {
177    
178            startMoodleSynchronization(username);
179    
180        }
181    
182        @Override
183        public void allPluginsActive() {
184    
185            Properties systemProps = System.getProperties();
186            // systemProps.setProperty("javax.net.ssl.trustStore", PATH_TO_JAVA_KEYSTORE);
187            // systemProps.setProperty("javax.net.ssl.trustStorePassword", PASS_FOR_JAVA_KEYSTORE);
188            log.info("MoodleCheck: JRE Keystore (Truststore) is at "
189                    + "\"" + systemProps.getProperty("javax.net.ssl.trustStore") +"\"");
190    
191        }
192    
193    
194    
195        // --
196        // --- Moodle Plugin Implementation
197        // --
198    
199        /** Relates the moodle-security-key to our currently logged-in user-account. **/
200        @POST
201        @Path("/key/{id}")
202        @Consumes(MediaType.APPLICATION_JSON)
203        @Produces(MediaType.APPLICATION_JSON)
204        public String setMoodleSecurityKey(@PathParam("id") int id, final String input ) {
205            DeepaMehtaTransaction tx = dms.beginTx();
206            try {
207                // 1) Store security key for this user
208                Topic userAccount = checkAuthorization();
209                if (userAccount.getId() != id) throw new WebApplicationException(new RuntimeException("Not allowed"), 401);
210                Topic user = dms.getTopic(id, true);
211                JSONObject payload = new JSONObject(input);
212                String moodle_key = payload.getString("moodle_key");
213                // 2) prevent setting of keys which do not look like a moodle security key
214                if (moodle_key.equals("") || moodle_key.length() != 32) {
215                    throw new RuntimeException("Sorry but that does not look like a \"Moodle security key.\"");
216                }
217                user.setProperty(MOODLE_SECURITY_KEY_URI, moodle_key, false); // addToIndex=false **/
218                // 3) Fetch user_id from moodle installation (to be able to start querying the service)
219                fetchAndSetMoodleUserId(userAccount);
220                tx.success();
221                return "{ \"result\": \"OK\"}";
222            } catch (JSONException ex) {
223                log.warning("We could not set your moodle user id because of an error while parsing the response "
224                        + ex.getMessage());
225                tx.failure();
226                return "{ \"result\": \"FAIL\"}";
227            } catch (Exception wex) {
228                tx.failure();
229                log.warning("We could not set your moodle user id " + wex.getMessage()
230                        + " (" + wex.getCause().toString() + ")");
231                return "{ \"result\": \"FAIL\"}";
232            } finally {
233                tx.finish();
234            }
235        }
236    
237        @GET
238        @Path("/key")
239        @Produces(MediaType.TEXT_PLAIN)
240        public String getMoodleSecurityKey() {
241            Topic userAccount = checkAuthorization();
242            if (userAccount.hasProperty(MOODLE_SECURITY_KEY_URI)) {
243                String token = (String) userAccount.getProperty(MOODLE_SECURITY_KEY_URI);
244                return token;
245            }
246            return null;
247        }
248    
249        @GET
250        @Path("/synchronize/{username}")
251        @Produces(MediaType.TEXT_PLAIN)
252        public String startMoodleSynchronization(@PathParam("username") String username) {
253    
254            final Topic user = checkAuthorization();
255            // 1) Twofold sanity check
256            if (!user.getSimpleValue().toString().equals(username)) {
257                log.info("MoodleServiceClient sanity check failed " + username + " != " + user.getSimpleValue().toString());
258                throw new RuntimeException();
259            }
260            // 2) Start Moodle-Sync for this username
261            startSynchronizationThreadFor(user);
262    
263            return "OK";
264    
265        }
266    
267    
268    
269        // --- Private helper methods ---
270    
271        private Topic checkAuthorization() {
272            String username = aclService.getUsername();
273            if (username == null) throw new WebApplicationException(401);
274            return getUserAccountTopic(username);
275        }
276    
277        private boolean setMoodleUserId(Topic userAccount, long moodleUserId) {
278            DeepaMehtaTransaction tx = dms.beginTx();
279            userAccount.setProperty(MOODLE_USER_ID_URI, "" + moodleUserId + "", false);
280            tx.success();
281            tx.finish();
282            return true;
283        }
284    
285        private void startSynchronizationThreadFor(Topic user) {
286    
287            final String user_name = user.getCompositeValue().getString(USER_NAME_TYPE_URI).toString();
288            final Topic local_user = user;
289    
290            new Thread() {
291    
292                @Override
293                public void run() {
294    
295                    log.info("Started MoodleServiceClient-Synchronization Thread due to a _login-Event .. ");
296                    // 2) Fetch Moodle Security Key for newly logged in user
297                    String key = getMoodleSecurityKeyWithoutAuthCheck(local_user);
298                    if (key != null && key.length() == 32) { // 2) Check if it looks like a legit one for MOODLE
299                        // todo: check if we have a MoodleUserID for this user ..
300                        log.info("MoodleServiceclient found a security key for \"" + user_name + "\" which looks legit..");
301                    } else {
302                        throw new RuntimeException("This user has no \"Moodle Security Key\" set no SYNC.. ");
303                    }
304                    // 3) Ask for potentially new \"Moodle Courses\" ..
305                    log.info("MoodleServiceClient checking for news in all \"Moodle Courses\" of \""+user_name+"\"");
306                    //    and create them if necessary / yet unknown to our system/installation
307                    getMoodleCoursesWithoutAuth(local_user, getMoodleUserId(local_user), key);
308                    // 4) Check for new \"Moodle Items\" in each tagged \"Moodle Course\" our user is "participating"
309                    ResultList<RelatedTopic> courses = getMoodleCoursesByUser(local_user);
310                    for (RelatedTopic course : courses) {
311                        // 5) excluse Moodle Courses with no "Moodle Course Hashtag" set from synchronziations
312                        course.loadChildTopics(TAG_URI);
313                        if (course.getCompositeValue().has(TAG_URI) &&
314                            course.getCompositeValue().getTopics(TAG_URI).size() > 0) {
315                            List<Topic> courseHashtags = course.getCompositeValue().getTopics(TAG_URI);
316                            log.info("MoodleServiceClient SYNC course \"" + course.getSimpleValue() +"\" "
317                                    + "under " +courseHashtags.size() + " hashtags:");
318                            for (Topic hashtag : courseHashtags) {
319                                log.info("\t#" + hashtag.getSimpleValue());
320                            }
321                            getCourseContentsWithoutAuth(course.getId(), key, courseHashtags);
322                        } else {
323                            log.info("MoodleServiceClient waiting with SYNC cause of missing #Hashtag (on \""
324                                    + course.getSimpleValue() + "\")");
325                        }
326                    }
327                }
328            }.start();
329        }
330    
331        /** Fetches and relates the internal moodle-user-id to our currently logged-in user-account. **/
332        private Topic fetchAndSetMoodleUserId(Topic userAccount) throws WebApplicationException {
333    
334            String token = getMoodleSecurityKeyWithoutAuthCheck(userAccount);
335            if (token == null) throw new WebApplicationException(new RuntimeException("User has no security key."), 500);
336            String parameter = "serviceshortnames[0]=" + MOODLE_SERVICE_NAME;
337            String data = "";
338            try {
339                data = callMoodle(token, "core_webservice_get_site_info", parameter);
340                JSONObject response = new JSONObject(data.toString());
341                long userId = response.getLong("userid");
342                log.info("Set Moodle User ID => " + userId);
343                setMoodleUserId(userAccount, userId);
344            } catch (JSONException ex) {
345                log.warning("Moodle JSONException " + ex.getMessage().toString());
346                try {
347                    JSONObject exception = new JSONObject(data.toString());
348                    log.warning("MoodleResponseException is \"" + exception.getString("message") +"\"");
349                } catch (JSONException ex1) {
350                    log.warning("Moodle JSONException " + ex.getMessage().toString());
351                }
352            } catch (MoodleConnectionException mc) {
353                log.warning("MoodleConnectionException \"" + mc.message + "\" (" + mc.status + ")");
354                throw new WebApplicationException(mc, mc.status);
355            }
356            return userAccount;
357        }
358    
359        private Topic getMoodleCoursesWithoutAuth(Topic userAccount, long moodleUserId, String token) {
360            String parameter = "userid=" + moodleUserId;
361            String data = "";
362            try {
363                data = callMoodle(token, "core_enrol_get_users_courses", parameter);
364                if (data.indexOf("webservice_access_exception") != -1) {
365                    log.warning("Looks like external service (webservice feature) is not \"Enabled\" or the called function"
366                            + " is not part of the \"External Service\" defintion on the configured Moodle installation.");
367                    throw new WebApplicationException(new MoodleConnectionException(data, 404), 404);
368                }
369                //
370                JSONArray response = new JSONArray(data.toString());
371                for (int i = 0; i < response.length(); i++) {
372                    JSONObject course = response.getJSONObject(i);
373                    Topic courseTopic = getMoodleCourseTopic(course.getLong("id"));
374                    if (courseTopic == null) {
375                        // 1) Create new item
376                        courseTopic = createMoodleCourseTopic(course);
377                        if (courseTopic != null) {
378                            // 2) Fix ACLEntries (caused by missing request-scope in Thread-local)
379                            setDefaultMoodleAdminACLEntries(courseTopic); // just "admin" can edit course-items
380                            // sendClientNotification("New Moodle Course", courseTopic);
381                        } else {
382                            log.info("OMITTING HIDDEN MoodleCourse \"" + course.getString("shortname") + "\"");
383                        }
384                    } else {
385                        // 2) Update existing item
386                        updateMoodleCourseTopic(courseTopic, course);
387                    }
388                    // 3) Relate to course item
389                    if (!hasParticipantEdge(courseTopic, userAccount)) {
390                        createParticipantEdge(courseTopic, userAccount);
391                    }
392                }
393            } catch (JSONException ex) {
394                try {
395                    JSONObject response = new JSONObject(data.toString());
396                    String exception = response.getString("exception");
397                    throw new WebApplicationException(new MoodleConnectionException(exception, 500), 500);
398                } catch (JSONException ex1) {}
399            } catch (MoodleConnectionException mc) {
400                throw new WebApplicationException(new Throwable(mc.message), mc.status);
401            } finally {
402                return userAccount;
403            }
404        }
405    
406        private Topic getCourseContentsWithoutAuth(long topicId, String token, List<Topic> hashtags) {
407            long courseId = -1;
408            Topic courseTopic = dms.getTopic(topicId, true);
409            courseId = Long.parseLong(courseTopic.getUri().replaceAll(ISIS_COURSE_URI_PREFIX, ""));
410            String parameter = "courseid=" + courseId;
411            String data = "";
412            try {
413                data = callMoodle(token, "core_course_get_contents", parameter);
414                JSONArray response = new JSONArray(data.toString());
415                for (int i = 0; i < response.length(); i++) {
416                    JSONObject section = response.getJSONObject(i);
417                    Topic sectionTopic = getMoodleSectionTopic(section.getLong("id"));
418                    // 1) Create new \"Moodle Section\"
419                    if (sectionTopic == null) {
420                        sectionTopic = createMoodleSectionTopic(section, i);
421                        if (sectionTopic != null) {
422                            // Fix Section-ACL-Entries so that "admin" can edit them
423                            setDefaultMoodleAdminACLEntries(sectionTopic);
424                            // sendClientNotification("New Moodle Section in \"" +
425                               //     courseTopic.getSimpleValue().toString() + "\"", sectionTopic);
426                        }
427                    // 2) Update existing \"Moodle section\"
428                    } else {
429                        updateMoodleSectionTopic(sectionTopic, section);
430                    }
431                    // 3) Create or Update all \"Moodle Items\"
432                    if (sectionTopic != null) { // (if section is not null/ hidden)
433                        JSONArray modules = section.getJSONArray("modules");
434                        for (int k = 0; k < modules.length(); k++) {
435                            JSONObject item = modules.getJSONObject(k);
436                            Topic itemTopic = getMoodleItemTopic(item.getLong("id"));
437                            if (itemTopic == null) {
438                                itemTopic = createMoodleItemTopic(item, hashtags);
439                                if (itemTopic != null) {
440                                    // Fix ACL so that all "Moodle"-WS Members can edit these items
441                                    setDefaultMoodleGroupACLEntries(itemTopic);
442                                    Association creator_edge = assignDefaultAuthorship(itemTopic);
443                                    if (creator_edge != null) setDefaultMoodleGroupACLEntries(creator_edge);
444                                }
445                            } else {
446                                updateMoodleItemTopic(itemTopic, courseTopic, item);
447                            }
448                            if (!hasAggregatingSectionParentEdge(itemTopic, sectionTopic)) {
449                                createAggregatingSectionEdge(sectionTopic, itemTopic);
450                            }
451                        }
452                    }
453                    // 4) Assign new \"Moodle section\" to course (if section is not null/hidden)
454                    if (!hasAggregatingCourseParentEdge(sectionTopic, courseTopic)) {
455                        createAggregatingCourseEdge(courseTopic, sectionTopic);
456                    }
457                }
458                log.info("MoodleServiceClient finished loading materials for course \""+courseTopic.getSimpleValue()+"\"");
459            } catch (JSONException ex) {
460                Logger.getLogger(MoodleServiceClient.class.getName()).log(Level.SEVERE, null, ex);
461                try {
462                    JSONObject exception = new JSONObject(data.toString());
463                    String message = exception.getString("message");
464                    throw new WebApplicationException(new MoodleConnectionException(message, 500), 500);
465                } catch (JSONException ex1) {
466                    Logger.getLogger(MoodleServiceClient.class.getName()).log(Level.SEVERE, null, ex1);
467                }
468            } catch (MoodleConnectionException mc) {
469                throw new WebApplicationException(mc, mc.status);
470            }
471            return courseTopic;
472        }
473    
474        private String callMoodle (String key, String functionName, String params) throws MoodleConnectionException {
475    
476            Topic serviceUrl = getMoodleEndpointUrl();
477            String endpointUri = serviceUrl.getSimpleValue().value().toString();
478            String queryUrl = endpointUri + "?wstoken=" + key + "&wsfunction=" + functionName + "&" + MOODLE_SERVICE_FORMAT;
479            // "&service=" + MOODLE_SERVICE_NAME +
480            try {
481                if (isHTTPSConfigured()) {
482                    // Many thanks to "Bruno" for formulating the following idea/approach at:
483                    // http://stackoverflow.com/questions/10267968/error-when-opening-https-url-keycertsign-bit-is-not-set
484                    TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
485                    KeyStore ks = KeyStore.getInstance("JKS");
486                    Topic client_jks_option = getJavaKeyStorePath();
487                    FileInputStream fis = new FileInputStream(client_jks_option.getSimpleValue().toString());
488                    ks.load(fis, getJavaKeyStorePass().getSimpleValue().toString().toCharArray());
489                    fis.close();
490                    tmf.init(ks);
491                    //
492                    SSLContext sslContext = SSLContext.getInstance("TLS");
493                    sslContext.init(null, tmf.getTrustManagers(), null);
494                    //
495                    HttpsURLConnection con = (HttpsURLConnection) new URL(queryUrl).openConnection();
496                    con.setSSLSocketFactory(sslContext.getSocketFactory());
497                    con.setRequestMethod("POST");
498                    con.setRequestProperty("Content-Type", "application/x-www-form-urlencoded");
499                    con.setRequestProperty("Content-Language", "en-US");
500                    con.setDoOutput(true);
501                    con.setUseCaches (false);
502                    con.setDoInput(true);
503                    DataOutputStream wr = new DataOutputStream (con.getOutputStream());
504                    wr.writeBytes (params);
505                    wr.flush();
506                    wr.close();
507                    if (con.getResponseCode() != 200) { // this case never occurred to me
508                        log.info("MoodleConnection HTTP Status \"" + con.getResponseCode() + "\"");
509                        throw new MoodleConnectionException("MoodleConnection has thrown an error", con.getResponseCode());
510                    }
511                    // Get Response
512                    InputStream is = con.getInputStream();
513                    BufferedReader rd = new BufferedReader(new InputStreamReader(is));
514                    String line;
515                    StringBuilder response = new StringBuilder();
516                    while((line = rd.readLine()) != null) {
517                        response.append(line);
518                        response.append('\r');
519                    }
520                    rd.close();
521                    // Handle empty response
522                    if (response.toString().isEmpty()) {
523                        String message = "MoodleConnection Response was empty. This happens even if webservice features "
524                            + "are completely deactivated or the function is not part of the \"External services\" "
525                                + "definition.";
526                        log.info(message);
527                        throw new MoodleConnectionException(message, 204);
528                    }
529                    return response.toString();
530                } else {
531                    HttpURLConnection con = (HttpURLConnection) new URL(queryUrl).openConnection();
532                    con.setRequestMethod("POST");
533                    con.setRequestProperty("Content-Type", "application/x-www-form-urlencoded");
534                    con.setRequestProperty("Content-Language", "en-US");
535                    con.setDoOutput(true);
536                    con.setUseCaches (false);
537                    con.setDoInput(true);
538                    DataOutputStream wr = new DataOutputStream (con.getOutputStream());
539                    wr.writeBytes (params);
540                    wr.flush();
541                    wr.close();
542                    if (con.getResponseCode() != 200) { // this case never occurred to me
543                        log.warning("MoodleConnection HTTP Status \"" + con.getResponseCode() + "\"");
544                        throw new MoodleConnectionException("MoodleConnection has thrown an error", con.getResponseCode());
545                    }
546                    //Get Response
547                    InputStream is = con.getInputStream();
548                    BufferedReader rd = new BufferedReader(new InputStreamReader(is));
549                    String line;
550                    StringBuilder response = new StringBuilder();
551                    while((line = rd.readLine()) != null) {
552                        response.append(line);
553                        response.append('\r');
554                    }
555                    rd.close();
556                    // Handle empty response
557                    if (response.toString().isEmpty()) {
558                        String message = "MoodleConnection Response was empty. This happens even if webservice features "
559                            + "are completely deactivated or the function is not part of the \"External services\" "
560                                + "definition.";
561                        log.warning(message);
562                        throw new MoodleConnectionException(message, 204);
563                    }
564                    return response.toString();
565                }
566            } catch (KeyManagementException ex) {
567                log.warning("Moodle KeyManagementException " + ex.getMessage());
568                throw new RuntimeException(ex);
569            } catch (CertificateException ex) {
570                log.warning("Moodle CertificatieException " + ex.getMessage());
571                throw new RuntimeException(ex);
572            } catch (KeyStoreException ex) {
573                log.warning("Moodle KeyStoreException " + ex.getMessage());
574                throw new RuntimeException(ex);
575            } catch (NoSuchAlgorithmException ex) {
576                log.warning("Moodle Cypher NoSuchAlgorithmException " + ex.getMessage());
577                throw new RuntimeException(ex);
578            } catch (MoodleConnectionException ex) {
579                log.warning("Moodle ConnectionException " + ex.message + "(" + ex.status + ")");
580                throw new MoodleConnectionException(ex.message, ex.status);
581            } catch (MalformedURLException ml) {
582                log.warning("Moodle Malformed URL Exception .. " + ml.getMessage().toString());
583                throw new MoodleConnectionException("DeepaMehta could not connect to malformed url: \"" + queryUrl + "\"",
584                        404);
585            } catch (IOException ex) {
586                log.warning("Moodle I/O Exception .. " + ex.getMessage().toString());
587                throw new MoodleConnectionException("DeepaMehta could not connect to \"" + queryUrl + "\"", 500);
588            }
589        }
590    
591        private void createParticipantEdge(Topic courseTopic, Topic userAccount) {
592            AssociationModel participantEdge = new AssociationModel(MOODLE_PARTICIPANT_EDGE,
593                    new TopicRoleModel(courseTopic.getId(), DEFAULT_ROLE_TYPE_URI),
594                    new TopicRoleModel(userAccount.getId(), DEFAULT_ROLE_TYPE_URI));
595            Association edge = dms.createAssociation(participantEdge, null);
596        }
597    
598        private void createAggregatingCourseEdge (Topic courseTopic, Topic sectionTopic) {
599            AssociationModel aggregationEdge = new AssociationModel(AGGREGATION_TYPE_URI,
600                    new TopicRoleModel(courseTopic.getId(), PARENT_ROLE_TYPE_URI),
601                    new TopicRoleModel(sectionTopic.getId(), CHILD_ROLE_TYPE_URI));
602            Association edge = dms.createAssociation(aggregationEdge, null);
603        }
604    
605        private void createAggregatingSectionEdge (Topic sectionTopic, Topic itemTopic) {
606            AssociationModel aggregationEdge = new AssociationModel(AGGREGATION_TYPE_URI,
607                    new TopicRoleModel(sectionTopic.getId(), PARENT_ROLE_TYPE_URI),
608                    new TopicRoleModel(itemTopic.getId(), CHILD_ROLE_TYPE_URI));
609            Association edge = dms.createAssociation(aggregationEdge, null);
610        }
611    
612        private Topic createMoodleCourseTopic(JSONObject object) {
613            try {
614                if (object.getInt("visible") == 0) return null;
615                long courseId = object.getLong("id");
616                String shortName = object.getString("shortname");
617                String fullName = object.getString("fullname");
618                CompositeValueModel model = new CompositeValueModel();
619                model.put(MOODLE_COURSE_NAME_URI, fullName);
620                model.put(MOODLE_COURSE_SHORT_NAME_URI, shortName);
621                TopicModel course = new TopicModel(ISIS_COURSE_URI_PREFIX + courseId, MOODLE_COURSE_URI, model);
622                course.setUri(ISIS_COURSE_URI_PREFIX + courseId);
623                Topic result = dms.createTopic(course, null); // null makes sure that a topic does not get assigned to a WS
624                assignToMoodleWorkspace(result);
625                return result;
626            } catch (JSONException ex) {
627                Logger.getLogger(MoodleServiceClient.class.getName()).log(Level.SEVERE, null, ex);
628            }
629            return null;
630        }
631    
632        private Topic updateMoodleCourseTopic(Topic course, JSONObject object) {
633            try {
634                boolean update_this = false;
635                String new_name = object.getString("shortname");
636                String new_fullname = object.getString("fullname");
637                if (!course.getCompositeValue().getString(MOODLE_COURSE_SHORT_NAME_URI).equals(new_name) ||
638                    !course.getCompositeValue().getString(MOODLE_COURSE_NAME_URI).equals(new_fullname)) {
639                    update_this = true;
640                }
641                // Update (if there were changes)
642                if (update_this) {
643                    CompositeValueModel model = new CompositeValueModel();
644                    model.put(MOODLE_COURSE_NAME_URI, new_name);
645                    model.put(MOODLE_COURSE_SHORT_NAME_URI, new_fullname);
646                    dms.updateTopic(new TopicModel(course.getId(), model), null);
647                    // todo: add to usage report
648                }
649                return course;
650            } catch (JSONException ex) {
651                Logger.getLogger(MoodleServiceClient.class.getName()).log(Level.SEVERE, null, ex);
652            }
653            return null;
654        }
655    
656        private Topic createMoodleSectionTopic(JSONObject object, int nr) {
657            try {
658                if (object.getInt("visible") == 0) return null; // item is hidden, do not create it
659                long sectionId = object.getLong("id");
660                String name = object.getString("name");
661                String summary = object.getString("summary");
662                CompositeValueModel model = new CompositeValueModel();
663                model.put(MOODLE_SECTION_NAME_URI, name);
664                model.put(MOODLE_SECTION_SUMMARY_URI, summary);
665                model.put(MOODLE_SECTION_ORDINAL_NR, nr);
666                TopicModel section = new TopicModel(ISIS_SECTION_URI_PREFIX + sectionId, MOODLE_SECTION_URI, model);
667                // OWNER AND CREATOR will be the (logged in user) who triggered this method IN ANY CASE
668                Topic result = dms.createTopic(section, null); //regardless of null, a username might be assigned to this
669                setDefaultMoodleAdminACLEntries(result); // just "admin" can edit these
670                assignToMoodleWorkspace(result);
671                return result;
672            } catch (JSONException ex) {
673                Logger.getLogger(MoodleServiceClient.class.getName()).log(Level.SEVERE, null, ex);
674            }
675            return null;
676        }
677    
678        private Topic updateMoodleSectionTopic(Topic section, JSONObject object) {
679            try {
680                boolean update_this = false;
681                String new_name = object.getString("name");
682                String new_summary = object.getString("summary");
683                // Custom comparison of values
684                if (!section.getCompositeValue().getString(MOODLE_SECTION_NAME_URI).equals(new_name) ||
685                    !section.getCompositeValue().getString(MOODLE_SECTION_SUMMARY_URI).equals(new_summary)) {
686                    update_this = true;
687                }
688                // Update (if there were changes)
689                if (update_this) {
690                    CompositeValueModel model = new CompositeValueModel();
691                    model.put(MOODLE_SECTION_NAME_URI, new_name);
692                    model.put(MOODLE_SECTION_SUMMARY_URI, new_summary);
693                    // Attached operations *need* Directives
694                    section.setCompositeValue(model, null, new Directives());
695                }
696            } catch (JSONException ex) {
697                Logger.getLogger(MoodleServiceClient.class.getName()).log(Level.SEVERE, null, ex);
698            }
699            return null;
700        }
701    
702        private Topic createMoodleItemTopic(JSONObject object, List<Topic> hashtags) {
703            DeepaMehtaTransaction tx = dms.beginTx();
704            try {
705                // 0) Checki if given moodle item is actually "hidden" and if not remember moodle-id
706                if (object.getInt("visible") == 0) return null; // item is hidden, do not create it
707                long itemId = object.getLong("id");
708                // 1) Parse some generic information for this item
709                CompositeValueModel model = parseGenericsToItemModel(object);
710                // 2) and if it's a material (typeof "resource" or "url") process its list of "contents"
711                JSONArray contents = null;
712                if (object.has("contents")) {
713                    contents = object.getJSONArray("contents");
714                    for (int i = 0; i < contents.length(); i++) { // (actually never in use, but possible)
715                        JSONObject resource = contents.getJSONObject(i);
716                        parseResourceToItemModel(model, resource);
717                        // ..) equip moodle item with timestamp of or resource (e.g. files)
718                        parseTimestampsToItemModel(model, resource);
719                    }
720                } else {
721                    // 3) equip any other item with default timestamps (or the timestamps of the object, not resource)
722                    parseTimestampsToItemModel(model, object);
723                }
724                // 4) equip moodle item with every hashtag set on the course
725                for (Topic hashtag : hashtags) {
726                    model.addRef(TAG_URI, hashtag.getId());
727                }
728                // 5) construct our internal uri for any moodl item
729                TopicModel item = new TopicModel(ISIS_ITEM_URI_PREFIX + itemId, MOODLE_ITEM_URI, model);
730                Topic result = dms.createTopic(item, null);
731                // 6) fix workspace assignment for shared editing (tagging) of moodle items via webclient
732                assignToMoodleWorkspace(result);
733                // sendTopicNotification("New Moodle Item", result);
734                tx.success();
735                return result;
736            } catch (JSONException ex) {
737                tx.failure();
738                log.warning("Moodle-Item could not be created (JSONException): " + ex.getCause().toString());
739            } finally {
740                tx.finish();
741            }
742            return null;
743        }
744    
745        private Topic updateMoodleItemTopic(Topic item, Topic courseTopic, JSONObject object) {
746            DeepaMehtaTransaction tx = dms.beginTx();
747            try {
748                // 0) parse new generic infos to new model
749                CompositeValueModel model = parseGenericsToItemModel(object);
750                // 1) parse infos specific to resources to model
751                JSONArray contents = null;
752                long last_modified_in_moodle = 0;
753                if (object.has("contents")) {
754                    contents = object.getJSONArray("contents");
755                    for (int i = 0; i < contents.length(); i++) { // (no observation that this is  >1, but possible)
756                        // if list of contents in a module is >1, currently the last one succeeds
757                        JSONObject resource = contents.getJSONObject(i);
758                        parseResourceToItemModel(model, resource);
759                        parseTimestampsToItemModel(model, resource);
760                    }
761                } else {
762                    // 2) equip ay other item with the new (moodle or our default) timestamps first
763                    parseTimestampsToItemModel(model, object);
764                }
765                // 2) start to perform update checks on contents of this (new) item
766                boolean update_this = false;
767                if (!item.getSimpleValue().toString().equals(model.getString(MOODLE_ITEM_NAME_URI))) { // by item-name
768                    // log.info("MoodleServiceClient: The name of the item has changed: ITEM is TO_BE_UPDATED");
769                    update_this = true;
770                } else if (item.getCompositeValue().getString(MOODLE_ITEM_TYPE_URI).equals("url")) { // by url-value
771                    // on "url"-items we perform a comparison by-value (cause timestamps are always missing)
772                    if (!item.getCompositeValue().getString(MOODLE_ITEM_REMOTE_URL_URI)
773                            .equals(model.getString(MOODLE_ITEM_REMOTE_URL_URI))) {
774                        // log.info("MoodleServiceClient: The url-value changed: ITEM is TO_BE_UPDATED");
775                        update_this = true;
776                    }
777                } else if (item.getCompositeValue().has(MOODLE_ITEM_MODIFIED_URI)) {
778                    // note: sometimes int sometimes long (webclient!) cause of neo4j and moodles timestamps/1000
779                    // check the last_modification timestamp from on our topic (in our current DB)
780                    long existing_timestamp = item.getCompositeValue().getLong(MOODLE_ITEM_MODIFIED_URI);
781                    if (existing_timestamp < last_modified_in_moodle) { // lets write new contents to our \"Moodle Item\"
782                        log.info("MoodleServiceClient: The remote Last-Modified-Timestamp indicates: "
783                                + "ITEM is TO_BE_UPDATED");
784                        update_this = true;
785                    }
786                }
787    
788                if (update_this) {
789                    // 3) write update
790                    dms.updateTopic(new TopicModel(item.getId(), model), null);
791                    // sendClientNotification("Changed Moodle Item in \""
792                       //     + courseTopic.getSimpleValue().toString() + "\"", item);
793                }
794                tx.success();
795                return item;
796            } catch (JSONException ex) {
797                tx.failure();
798                log.warning("Moodle-Item could not be updated (JSONException): " + ex.getCause().toString());
799            } finally {
800                tx.finish();
801            }
802            return null;
803        }
804    
805        private CompositeValueModel parseGenericsToItemModel(JSONObject object) throws JSONException {
806            // 0) parse name, icon and type
807            String name = object.getString("name");
808            String iconPath = object.getString("modicon");
809            String type = object.getString("modname");
810            String description = "", href = "";
811            // 1) check for an optional description (e.g. "choice", or "label")
812            if (object.has("description")) {
813                description = object.getString("description");
814            }
815            // 2) check for a moodle item url so we can link directly to the content (into the moodle installation)
816            if (object.has("url")) {
817                href = object.getString("url");
818            }
819            // 3) Initialize the values of each moodle item
820            CompositeValueModel model = new CompositeValueModel();
821            model.put(MOODLE_ITEM_NAME_URI, name);
822            model.put(MOODLE_ITEM_ICON_URI, iconPath);
823            model.put(MOODLE_ITEM_DESC_URI, description);
824            model.put(MOODLE_ITEM_HREF_URI, href); // exists always (if item is not of type "label")
825            model.put(MOODLE_ITEM_TYPE_URI, type);
826            // ..) with a neutral review-score
827            model.put(REVIEW_SCORE_URI, 0);
828            return model;
829        }
830    
831        private CompositeValueModel parseResourceToItemModel(CompositeValueModel model, JSONObject resource)
832                throws JSONException {
833            String resourceType = resource.getString("type");
834            String fileUrl = "", fileName = "", author = "", license = "";
835            // 1) Fill up either web resource or file-type
836            if (resourceType.equals("url")) { // Moodle Resource is an URL
837                fileUrl = resource.getString("fileurl");
838                // 1.1) parse youtube and replace /watch?v=id with /embed/id in the url
839                if (fileUrl.indexOf("youtube") != -1 || fileUrl.indexOf("youtu.be") != -1) {
840                    // turn youtube-share url into /embed/-url
841                    fileUrl = createYoutubeEmbedUrl(fileUrl);
842                }
843                model.put(MOODLE_ITEM_REMOTE_URL_URI, fileUrl);
844                // add it to the (to be created) Moodle Item
845                model.put(MOODLE_ITEM_TYPE_URI, resourceType);
846                // alternatively: model.put(MOODLE_ITEM_HREF_URI, fileurl);
847            } else if (resourceType.equals("file")) { // Moodle Resource is an URL
848                // pages _and_ documents are of type file
849                fileName = resource.getString("filename");
850                fileUrl = resource.getString("fileurl");
851                if (!resource.isNull("license")) {
852                    license = resource.getString("license");
853                    model.put(MOODLE_ITEM_LICENSE_URI, license);
854                }
855                if (!resource.isNull("author")) {
856                    author = resource.getString("author");
857                    model.put(MOODLE_ITEM_AUTHOR_URI, author);
858                }
859                long fileSize = resource.getLong("filesize");
860                model.put(MOODLE_ITEM_NAME_URI, fileName);
861                model.put(MOODLE_ITEM_REMOTE_URL_URI, fileUrl);
862                model.put(MOODLE_ITEM_SIZE_URI, fileSize);
863                String file_type = JavaUtils.getFileType(fileName);
864                // 1.1) Try to determine file media_type with the help of DM-Utils
865                if (file_type == null) file_type = "Unknown"; // Maybe null (e.g. for .ODT-Documents)
866                model.put(MOODLE_ITEM_MEDIA_TYPE_URI, file_type);
867                model.put(MOODLE_ITEM_TYPE_URI, resourceType);
868            }
869            // The new model will just be written to topic/db if our custom update-check (see caller) succeeds
870            return model;
871        }
872    
873        private CompositeValueModel parseTimestampsToItemModel(CompositeValueModel model, JSONObject item)
874                throws JSONException {
875            long last_modified = 0, time_created = 0;
876            // 1) Check for timestamp "Last Modified"
877            if (item.has("timemodified") && !item.isNull("timemodified")) {
878                last_modified = item.getLong("timemodified") * 1000; // addding three zeros
879            } else { // e.g. contents of type "url" NEVER have any timestamp set, setting it to NOW
880                last_modified = new Date().getTime();
881            }
882            // 2) Check for timestamp "Created"
883            if (item.has("timecreated") && !item.isNull("timecreated")) {
884                time_created = item.getLong("timecreated")  * 1000; // addding three zeros;
885            } else { // e.g. contents of type "url" NEVER have any timestamp set, setting it to NOW
886                time_created = new Date().getTime();
887            }
888            model.put(MOODLE_ITEM_MODIFIED_URI, last_modified);
889            model.put(MOODLE_ITEM_CREATED_URI, time_created);
890            return model;
891        }
892    
893        private String createYoutubeEmbedUrl(String fileUrl) {
894            String new_url = fileUrl;
895            // 0) Skip transformation if url already contains the "/embed/"-part
896            if (fileUrl.indexOf("/embed") != -1) return new_url;
897            // 1) Transform URL Scheme: http://www.youtube.com/watch?v=A9b9bxUyK0o
898            if (fileUrl.indexOf("/watch") != -1) {
899                String[] parts = fileUrl.split("v=");
900                // Strip of everything behind & e.g.: &feature=youtube_gdata
901                String new_url_part = "";
902                int indexOfParameters = parts[1].indexOf("&");
903                if (parts[1] != null && indexOfParameters != -1) {
904                    new_url_part = parts[1].substring(0, indexOfParameters);
905                    new_url = "//youtube.com/embed/" + new_url_part;
906                } else if (parts[1] != null) {
907                    new_url = "//youtube.com/embed/" + parts[1];
908                }
909            }
910            // 2) Transform URL Scheme: http://youtu.be/A9b9bxUyK0o
911            if (fileUrl.indexOf("youtu.be/") != -1) {
912                String[] parts = fileUrl.split(".be/");
913                            String new_url_part = "";
914                int indexOfParameters = parts[1].indexOf("&");
915                if (parts[1] != null && indexOfParameters != -1) {
916                    new_url_part = parts[1].substring(0, indexOfParameters);
917                    new_url = "//youtube.com/embed/" + new_url_part;
918                } else if (parts[1] != null) {
919                    new_url = "//youtube.com/embed/" + parts[1];
920                }
921            }
922            return new_url;
923        }
924    
925        private Topic getUserAccountTopic(String username) {
926            Topic accountTopic = null;
927            Topic userTopic = dms.getTopic(USER_NAME_TYPE_URI, new SimpleValue(username), true);
928            accountTopic = userTopic.getRelatedTopic(COMPOSITION_TYPE_URI, CHILD_ROLE_TYPE_URI, PARENT_ROLE_TYPE_URI,
929                    USER_ACCOUNT_TYPE_URI, true, false);
930            return accountTopic;
931        }
932    
933        private long getMoodleUserId(Topic userAccount) {
934            if (userAccount.hasProperty(MOODLE_USER_ID_URI)) {
935                String id = (String) userAccount.getProperty(MOODLE_USER_ID_URI);
936                long moodle_user_id = Long.parseLong(id);
937                return moodle_user_id;
938            }
939            return -1;
940        }
941    
942        private Topic getMoodleEndpointUrl() {
943            return dms.getTopic("uri", new SimpleValue(CLIENT_OPTION_ENDPOINT_URL_URI), true);
944        }
945    
946        private Topic getJavaKeyStorePath() {
947            return dms.getTopic("uri", new SimpleValue(CLIENT_OPTION_JAVA_KEY_STORE_PATH), true);
948        }
949    
950        private Topic getJavaKeyStorePass() {
951            return dms.getTopic("uri", new SimpleValue(CLIENT_OPTION_JAVA_KEY_STORE_PASS), true);
952        }
953    
954        private boolean isHTTPSConfigured() {
955            Topic string_opt = dms.getTopic("uri", new SimpleValue(CLIENT_OPTION_USE_HTTPS), true);
956            if (string_opt.getSimpleValue().toString().equals("true") || string_opt.getSimpleValue().toString().equals("True")) return true;
957            return false;
958        }
959    
960        private ResultList<RelatedTopic> getMoodleCoursesByUser(Topic user) {
961            ResultList<RelatedTopic> courses = user.getRelatedTopics(MOODLE_PARTICIPANT_EDGE, DEFAULT_ROLE_TYPE_URI,
962                    DEFAULT_ROLE_TYPE_URI, MOODLE_COURSE_URI, false, false, 0);
963            if (courses != null && courses.getSize() > 1) return courses;
964            return null;
965        }
966    
967        private String getMoodleSecurityKeyWithoutAuthCheck(Topic userAccount) {
968            if (userAccount.hasProperty(MOODLE_SECURITY_KEY_URI)) {
969                String token = (String) userAccount.getProperty(MOODLE_SECURITY_KEY_URI);
970                return token;
971            }
972            return null;
973        }
974    
975        private boolean hasParticipantEdge (Topic course, Topic user) {
976            boolean value = false;
977            Topic userAccount = course.getRelatedTopic(MOODLE_PARTICIPANT_EDGE, DEFAULT_ROLE_TYPE_URI,
978                    DEFAULT_ROLE_TYPE_URI, USER_ACCOUNT_TYPE_URI, true, true);
979            if (userAccount != null && user.getId() == userAccount.getId()) return value = true;
980            return value;
981        }
982    
983        private boolean hasAggregatingCourseParentEdge (Topic child, Topic parent) {
984            boolean value = false;
985            if (child == null) return true;
986            Topic topic = child.getRelatedTopic(AGGREGATION_TYPE_URI, CHILD_ROLE_TYPE_URI,
987                    PARENT_ROLE_TYPE_URI, MOODLE_COURSE_URI, true, true);
988            if (topic != null && parent.getId() == topic.getId()) return value = true;
989            return value;
990        }
991    
992        private boolean hasAggregatingSectionParentEdge (Topic child, Topic parent) {
993            boolean value = false;
994            if (child == null) return true;
995            Topic topic = child.getRelatedTopic(AGGREGATION_TYPE_URI, CHILD_ROLE_TYPE_URI,
996                    PARENT_ROLE_TYPE_URI, MOODLE_SECTION_URI, true, true);
997            if (topic != null && parent.getId() == topic.getId()) return value = true;
998            return value;
999        }
1000    
1001        private Topic getMoodleCourseTopic(long courseId) {
1002            return dms.getTopic("uri", new SimpleValue(ISIS_COURSE_URI_PREFIX + courseId), true);
1003        }
1004    
1005        private Topic getMoodleSectionTopic(long sectionId) {
1006            return dms.getTopic("uri", new SimpleValue(ISIS_SECTION_URI_PREFIX + sectionId), true);
1007        }
1008    
1009        private Topic getMoodleItemTopic(long itemId) {
1010            return dms.getTopic("uri", new SimpleValue(ISIS_ITEM_URI_PREFIX + itemId), true);
1011        }
1012    
1013        // === ACL Fix ===
1014    
1015        private DeepaMehtaObject setDefaultMoodleGroupACLEntries(DeepaMehtaObject item) {
1016            // Let's repair broken/missing ACL-Entries
1017            ACLEntry writeList = new ACLEntry(Operation.WRITE, UserRole.MEMBER, UserRole.CREATOR, UserRole.OWNER);
1018            aclService.setACL(item, new AccessControlList(writeList));
1019            aclService.setCreator(item, USERNAME_OF_SETTINGS_ADMINISTRATOR);
1020            aclService.setOwner(item, USERNAME_OF_SETTINGS_ADMINISTRATOR);
1021            return item;
1022        }
1023    
1024        private DeepaMehtaObject setDefaultMoodleAdminACLEntries(DeepaMehtaObject item) {
1025            // Let's repair broken/missing ACL-Entries
1026            ACLEntry writeEntry = new ACLEntry(Operation.WRITE, UserRole.CREATOR, UserRole.OWNER);
1027            aclService.setACL(item, new AccessControlList(writeEntry));
1028            aclService.setCreator(item, USERNAME_OF_SETTINGS_ADMINISTRATOR);
1029            aclService.setOwner(item, USERNAME_OF_SETTINGS_ADMINISTRATOR);
1030            return item;
1031        }
1032    
1033        // === Workspace Assignments ===
1034    
1035        private void assignToMoodleWorkspace(Topic topic) {
1036            if (hasAnyWorkspace(topic)) {
1037                log.warning("NOT assigning to Moodle Workspace since topic HAS some assigned already ..");
1038                return;
1039            }
1040            Topic moodleWorkspace = dms.getTopic("uri", new SimpleValue(WS_MOODLE_URI), false);
1041            dms.createAssociation(new AssociationModel(AGGREGATION_TYPE_URI,
1042                new TopicRoleModel(topic.getId(), PARENT_ROLE_TYPE_URI),
1043                new TopicRoleModel(moodleWorkspace.getId(),CHILD_ROLE_TYPE_URI)
1044            ), null);
1045        }
1046    
1047        private boolean hasAnyWorkspace(Topic topic) {
1048            return topic.getRelatedTopics(AGGREGATION_TYPE_URI, PARENT_ROLE_TYPE_URI, CHILD_ROLE_TYPE_URI,
1049                "dm4.workspaces.workspace", false, false, 0).getSize() > 0;
1050        }
1051    
1052        private Association assignDefaultAuthorship(Topic item) {
1053            try {
1054                // 0) Check if eduzen-tagging-notes plugin is available
1055                dms.getPlugin(PLUGIN_URI_TAGGING_NOTES);
1056                // 1) If eduzen-tagging-nodes plugin is available
1057                Topic author = dms.getTopic("dm4.accesscontrol.username",
1058                        new SimpleValue(USERNAME_OF_SETTINGS_ADMINISTRATOR), false);
1059                // org.deepamehta.tagging-resources - Constants
1060                String CREATOR_EDGE_URI = "org.deepamehta.resources.creator_edge";
1061                // String CONTRIBUTOR_EDGE_URI = "org.deepamehta.resources.contributor_edge";
1062                if (associationExists(CREATOR_EDGE_URI, item, author)) return null;
1063                return dms.createAssociation(new AssociationModel(CREATOR_EDGE_URI,
1064                        new TopicRoleModel(item.getId(), PARENT_URI),
1065                        new TopicRoleModel(author.getId(), CHILD_URI)), null);
1066            } catch (RuntimeException e) {
1067                log.fine("MoodleWebServiceClient assigns no \"Tagging Notes\" (Plugin) Default Authorship");
1068                return null;
1069            }
1070        }
1071    
1072        private boolean associationExists(String edge_type, Topic item, Topic user) {
1073            List<Association> results = dms.getAssociations(item.getId(), user.getId(), edge_type);
1074            return (results.size() > 0) ? true : false;
1075        }
1076    
1077    }