001    package org.deepamehta.plugins.twitter;
002    
003    import com.sun.jersey.core.util.Base64;
004    import de.deepamehta.core.RelatedTopic;
005    import de.deepamehta.core.Topic;
006    import de.deepamehta.core.model.CompositeValueModel;
007    import de.deepamehta.core.model.SimpleValue;
008    import de.deepamehta.core.model.TopicModel;
009    import de.deepamehta.core.osgi.PluginActivator;
010    import de.deepamehta.core.service.ClientState;
011    import de.deepamehta.core.service.Directives;
012    import de.deepamehta.core.service.PluginService;
013    import de.deepamehta.core.service.ResultList;
014    import de.deepamehta.core.service.annotation.ConsumesService;
015    import de.deepamehta.core.storage.spi.DeepaMehtaTransaction;
016    import de.deepamehta.plugins.accesscontrol.model.ACLEntry;
017    import de.deepamehta.plugins.accesscontrol.model.AccessControlList;
018    import de.deepamehta.plugins.accesscontrol.model.Operation;
019    import de.deepamehta.plugins.accesscontrol.model.UserRole;
020    import de.deepamehta.plugins.accesscontrol.service.AccessControlService;
021    import java.io.BufferedReader;
022    import java.io.IOException;
023    import java.io.InputStreamReader;
024    import java.io.OutputStreamWriter;
025    import java.net.HttpURLConnection;
026    import java.net.MalformedURLException;
027    import java.net.URL;
028    import java.net.URLEncoder;
029    import java.util.Date;
030    import java.util.Iterator;
031    import java.util.logging.Logger;
032    import javax.ws.rs.*;
033    import javax.ws.rs.core.Response.Status;
034    import org.codehaus.jettison.json.JSONArray;
035    import org.codehaus.jettison.json.JSONException;
036    import org.codehaus.jettison.json.JSONObject;
037    import org.deepamehta.plugins.twitter.service.TwitterService;
038    
039    /**
040     * A very basic client for researching with the public Twitter Search API v1.1 and DeepaMehta 4.1.2
041     *
042     * @author Malte Reißig (<malte@mikromedia.de>)
043     * @version 1.3.0-SNAPSHOT
044     * @website https://github.com/mukil/twitter-research
045     *
046     */
047    
048    @Path("/tweet")
049    @Consumes("application/json")
050    @Produces("application/json")
051    public class TwitterPlugin extends PluginActivator implements TwitterService {
052    
053        private Logger log = Logger.getLogger(getClass().getName());
054    
055        private final String DEEPAMEHTA_VERSION = "DeepaMehta 4.1.3-SNAPSHOT";
056        private final String TWITTER_RESEARCH_VERSION = "1.3.0-SNAPSHOT";
057        private final String CHARSET = "UTF-8";
058    
059        private final static String CHILD_URI = "dm4.core.child";
060        private final static String PARENT_URI = "dm4.core.parent";
061        private final static String AGGREGATION = "dm4.core.aggregation";
062        private final static String COMPOSITION = "dm4.core.composition";
063    
064        private final static String TWEET_URI = "org.deepamehta.twitter.tweet";
065        private final static String TWEET_ID_URI = "org.deepamehta.twitter.tweet_id";
066        private final static String TWEET_TIME_URI = "org.deepamehta.twitter.tweet_time";
067        private final static String TWEET_CONTENT_URI = "org.deepamehta.twitter.tweet_content";
068        private final static String TWEET_ENTITIES_URI = "org.deepamehta.twitter.tweet_entities";
069        private final static String TWEET_METADATA_URI = "org.deepamehta.twitter.tweet_metadata";
070        private final static String TWEET_SOURCE_BUTTON_URI = "org.deepamehta.twitter.tweet_source_button";
071        private final static String TWEET_LOCATION_URI = "org.deepamehta.twitter.tweet_location";
072        private final static String TWEET_FAVOURITE_COUNT_URI = "org.deepamehta.twitter.tweet_favourite_count";
073        private final static String TWEET_WITHHELD_DMCA_URI = "org.deepamehta.twitter.tweet_withheld_copyright";
074        private final static String TWEET_WITHHELD_IN_URI = "org.deepamehta.twitter.tweet_withheld_in";
075        private final static String TWEET_WITHHELD_SCOPE_URI = "org.deepamehta.twitter.tweet_withheld_scope";
076        private final static String TWEETED_TO_STATUS_ID = "org.deepamehta.twitter.tweeted_to_status_id";
077    
078        private final static String TWITTER_USER_URI = "org.deepamehta.twitter.user";
079        private final static String TWITTER_USER_ID_URI = "org.deepamehta.twitter.user_id";
080        private final static String TWITTER_USER_NAME_URI = "org.deepamehta.twitter.user_name";
081        private final static String TWITTER_USER_IMAGE_URI = "org.deepamehta.twitter.user_image_url";
082    
083        private final static String TWITTER_SEARCH_URI = "org.deepamehta.twitter.search";
084        private final static String TWITTER_SEARCH_LANG_URI = "org.deepamehta.twitter.search_language";
085        private final static String TWITTER_SEARCH_LOCATION_URI = "org.deepamehta.twitter.search_location";
086        private final static String TWITTER_SEARCH_TYPE_URI = "org.deepamehta.twitter.search_result_type";
087        private final static String TWITTER_SEARCH_NEXT_PAGE_URI = "org.deepamehta.twitter.search_next_page";
088        private final static String TWITTER_SEARCH_REFRESH_URL_URI = "org.deepamehta.twitter.search_refresh_url";
089        private final static String TWITTER_SEARCH_MAX_TWEET_URI = "org.deepamehta.twitter.search_last_tweet_id";
090        private final static String TWITTER_SEARCH_RESULT_SIZE_URI = "org.deepamehta.twitter.search_result_size";
091        private final static String TWITTER_SEARCH_TIME_URI = "org.deepamehta.twitter.last_search_time";
092    
093        private final static String TWITTER_AUTHENTICATION_URL = "https://api.twitter.com/oauth2/token";
094        private final static String TWITTER_SEARCH_BASE_URL = "https://api.twitter.com/1.1/search/tweets.json";
095    
096        private final String GEO_COORDINATE_TOPIC_URI = "dm4.geomaps.geo_coordinate";
097        private final String GEO_LONGITUDE_TYPE_URI = "dm4.geomaps.longitude";
098        private final String GEO_LATITUDE_TYPE_URI = "dm4.geomaps.latitude";
099    
100        private boolean isInitialized = false;
101        private boolean isAuthorized = false;
102        private String bearerToken = null;
103        private AccessControlService acService = null;
104    
105    
106    
107        /** Initialize the migrated soundsets ACL-Entries. */
108        public void init() {
109            isInitialized = true;
110            configureIfReady();
111        }
112    
113        private void configureIfReady() {
114            if (isInitialized) {
115                checkACLsOfMigration();
116            }
117        }
118    
119        private void authorizeSearchRequests () throws TwitterAPIException {
120            Topic applicationKey  = dms.getTopic("uri", new SimpleValue("org.deepamehta.twitter.application_key"), true);
121            Topic applicationSecret  = dms.getTopic("uri",
122                    new SimpleValue("org.deepamehta.twitter.application_secret"), true);
123            try {
124                StringBuilder resultBody = new StringBuilder();
125                URL requestUri = new URL(TWITTER_AUTHENTICATION_URL);
126                //
127                String key = URLEncoder.encode(applicationKey.getSimpleValue().toString(), CHARSET);
128                String secret = URLEncoder.encode(applicationSecret.getSimpleValue().toString(), CHARSET);
129                // get base64 encoded secrets
130                if (key.isEmpty() || secret.isEmpty()) {
131                    throw new TwitterAPIException("Bad Twitter secrets, please register your application.",
132                            Status.UNAUTHORIZED);
133                }
134                String values =  key + ":" + secret;
135                String credentials = new String(Base64.encode(values));
136                // initiate request
137                HttpURLConnection connection = (HttpURLConnection) requestUri.openConnection();
138                connection.setDoOutput(true);
139                connection.setDoInput(true);
140                connection.setRequestMethod("POST");
141                connection.setRequestProperty("User-Agent", "DeepaMehta "+DEEPAMEHTA_VERSION+" - "
142                        + "Twitter Research " + TWITTER_RESEARCH_VERSION);
143                connection.setRequestProperty("Content-Type", "application/x-www-form-urlencoded;charset=" + CHARSET);
144                connection.setRequestProperty("Authorization", "Basic " + credentials);
145                //
146                OutputStreamWriter writer = new OutputStreamWriter(connection.getOutputStream());
147                String parameters = "grant_type=client_credentials";
148                writer.write(parameters);
149                writer.flush();
150                //
151                int httpStatusCode = connection.getResponseCode();
152                if (httpStatusCode != HttpURLConnection.HTTP_OK) {
153                    throw new TwitterAPIException("Error with HTTPConnection.", Status.INTERNAL_SERVER_ERROR);
154                }
155                // read in the response
156                BufferedReader rd = new BufferedReader(new InputStreamReader(connection.getInputStream(), CHARSET));
157                for (String input; (input = rd.readLine()) != null;) {
158                    resultBody.append(input);
159                }
160                rd.close();
161                writer.close();
162                // TODO: Check if answer is something like "403: Too many requests"
163                if (resultBody.toString().isEmpty()) {
164                    throw new TwitterAPIException("Twitter just handed us an empty response ("+httpStatusCode+")",
165                            Status.NO_CONTENT);
166                }
167                //
168                JSONObject response = new JSONObject(resultBody.toString());
169                bearerToken = response.getString("access_token");
170                isAuthorized = true;
171            } catch (JSONException ex) {
172                throw new RuntimeException("Internal Server Error while parsing response " + ex.getMessage());
173            } catch (IOException ex) {
174                throw new RuntimeException("Internal Server Error HTTP I/O Error " + ex.getMessage());
175            }
176        }
177    
178        /**
179         *  This method executes an existing search-query to either:
180         *  (a) fetch more (older) tweets for the same query or
181         *  (b) fetch new tweets and assign them to the current search result
182         *
183         *  @param {searchId}   id of the "Twitter-Search"-Topic to operate on
184         *  @param {nextPage}   <code>true</code> for paging query to next page;
185         *                      <code>false</code> for fetching most recent tweets
186         */
187    
188        @GET
189        @Path("/search/public/{id}/{nextPage}")
190        @Produces("application/json")
191        public Topic searchMoreTweets(@PathParam("id") long searchId,
192                @PathParam("nextPage") boolean nextPage, @HeaderParam("Cookie") ClientState clientState) {
193    
194            Topic query = dms.getTopic(searchId, true);
195            StringBuffer resultBody = new StringBuffer();
196            URL requestUri = null;
197            DeepaMehtaTransaction tx = dms.beginTx();
198            try {
199                // 0) Authorize request
200                if (!isAuthorized) {
201                    authorizeSearchRequests();
202                    if (!isAuthorized) {  // check if authorization was sucessfull
203                        throw new WebApplicationException(new Throwable("Bad Twitter Secrets. "
204                            + "Consider to register your application at https://dev.twitter.com/apps/new."), 500);
205                    }
206                }
207                log.fine("Researching tweets for Twitter-Search (" +query.getId()+ ") next ? " + nextPage);
208                // 1) loading search configuration
209                if (nextPage) {
210                    // paging to next-page (query for older-tweets)
211                    String nextPageUrl = query.getCompositeValue().getString(TWITTER_SEARCH_NEXT_PAGE_URI);
212                    if (nextPageUrl.isEmpty()) throw new RuntimeException("There is no next page. (204)");
213                    log.fine("Loading next page of tweets => " + TWITTER_SEARCH_BASE_URL + nextPageUrl);
214                    requestUri = new URL(TWITTER_SEARCH_BASE_URL + nextPageUrl);
215                } else {
216                    // refreshing (query for new-tweets)
217                    String refreshPageUrl = query.getCompositeValue().getString(TWITTER_SEARCH_REFRESH_URL_URI);
218                    requestUri = new URL(TWITTER_SEARCH_BASE_URL + refreshPageUrl);
219                    log.fine("Loading more recent tweets => " + TWITTER_SEARCH_BASE_URL + refreshPageUrl);
220                }
221                log.fine("Requesting => " + requestUri.toString());
222                // 2) initiate request
223                HttpURLConnection connection = (HttpURLConnection) requestUri.openConnection();
224                connection.setRequestMethod("GET");
225                connection.setRequestProperty("User-Agent", "DeepaMehta "+DEEPAMEHTA_VERSION+" - "
226                        + "Twitter Research " + TWITTER_RESEARCH_VERSION);
227                connection.setRequestProperty("Authorization", "Bearer " + bearerToken);
228                // 3) check the response
229                int httpStatusCode = connection.getResponseCode();
230                if (httpStatusCode != HttpURLConnection.HTTP_OK) {
231                    throw new WebApplicationException(new Throwable("Error with HTTPConnection."),
232                            Status.INTERNAL_SERVER_ERROR);
233                }
234                // 4) read in the response
235                BufferedReader rd = new BufferedReader(new InputStreamReader(connection.getInputStream(), CHARSET));
236                for (String input; (input = rd.readLine()) != null;) {
237                    resultBody.append(input);
238                }
239                rd.close();
240                // 5) process response // TODO: Check if answer is something like "403: Too many requests"
241                if (resultBody.toString().isEmpty()) {
242                    throw new WebApplicationException(new RuntimeException("Twitter handed just us an empty response."),
243                            Status.NO_CONTENT);
244                } else {
245                    processTwitterSearchResponse(query, resultBody, clientState);
246                }
247                tx.success();
248                // update modification timestamp on parent (composite) topic to invalidate http caching
249                dms.updateTopic(new TopicModel(query.getId()), clientState);
250            } catch (TwitterAPIException ex) {
251                log.warning("TwitterApiException " + ex.getMessage());
252                throw new WebApplicationException(new Throwable(ex.getMessage()), ex.getStatus());
253            } catch (MalformedURLException e) {
254                throw new RuntimeException("Could not trigger existing search-topic.", e);
255            } catch (IOException ioe) {
256                throw new WebApplicationException(new Throwable("Most probably we made a mistake in constructing the query. "
257                        + "We're sorry, please try again."), Status.BAD_REQUEST);
258            } finally {
259                tx.finish();
260            }
261            return query;
262        }
263    
264    
265    
266        /**
267         * Fetches public tweets matching the given <code>query</code>, maintains a search-query topic and
268         * references existing tweets and users, as it should be.
269         *
270         * @param {id}          Twitter Search Topic Id
271         * @param {resultType}  "mixed", "recent", "popular"
272         * @param {lang}        ISO-639-1 Code (2 chars) (optional)
273         * @param {location}    "lat,lng,radiuskm" (optional)
274         */
275    
276        @GET
277        @Path("/search/public/{id}/{query}/{resultType}/{lang}/{location}")
278        @Produces("application/json")
279        public Topic searchPublicTweets(@PathParam("id") long searchId, @PathParam("query") String query,
280                @PathParam("resultType") String resultType, @PathParam("lang") String lang,
281                @PathParam("location") String location, @HeaderParam("Cookie") ClientState clientState) {
282    
283            StringBuffer resultBody = new StringBuffer();
284            DeepaMehtaTransaction tx = dms.beginTx();
285            try {
286                // 0) Authorize request
287                if (!isAuthorized) {
288                    authorizeSearchRequests();
289                    if (!isAuthorized) {  // check if authorization was sucessfull
290                        throw new WebApplicationException(new Throwable("Bad Twitter Secrets. "
291                            + "Consider to register your application at https://dev.twitter.com/apps/new."), 500);
292                    }
293                }
294                // 1) setup search container
295                Topic twitterSearch = dms.getTopic(searchId, true);
296                log.fine("Resarching Public Tweets " +query+ " (" +resultType+ ") "
297                        + "in language: " + lang + " near loc: " + location);
298                // 2) construct search query
299                String queryUrl = TWITTER_SEARCH_BASE_URL + "?q=" + URLEncoder.encode(query.toString(), CHARSET)
300                        + ";&include_entities=true;&result_type=" + resultType + ";"; // ;&rpp=" + querySize + "
301                if (lang == null) lang = "";
302                if (location == null) location = "";
303                if (!lang.isEmpty() && !lang.equals("unspecified")) queryUrl += "&lang="+lang+";";
304                if (!location.isEmpty() && !location.equals("none")) queryUrl += "&geocode="+location+";";
305                URL requestUri = new URL(queryUrl);
306                // 3) initiate request
307                HttpURLConnection connection = (HttpURLConnection) requestUri.openConnection();
308                connection.setRequestMethod("GET");
309                connection.setRequestProperty("User-Agent", "DeepaMehta "+DEEPAMEHTA_VERSION+" - "
310                        + "Twitter Research " + TWITTER_RESEARCH_VERSION);
311                connection.setRequestProperty("Authorization", "Bearer " + bearerToken);
312                // 4) check the response
313                int httpStatusCode = connection.getResponseCode();
314                if (httpStatusCode != HttpURLConnection.HTTP_OK) {
315                    throw new WebApplicationException(new Throwable("Error with HTTPConnection."),
316                            Status.INTERNAL_SERVER_ERROR);
317                }
318                // 5) process response
319                            BufferedReader rd = new BufferedReader(new InputStreamReader(connection.getInputStream(), CHARSET));
320                for (String input; (input = rd.readLine()) != null;) {
321                    resultBody.append(input);
322                }
323                rd.close();
324                if (resultBody.toString().isEmpty()) {
325                    throw new WebApplicationException(new RuntimeException("Twitter just handed us an empty response."),
326                            Status.NO_CONTENT);
327                } else {
328                    processTwitterSearchResponse(twitterSearch, resultBody, clientState);
329                }
330                tx.success();
331                return twitterSearch;
332            } catch (TwitterAPIException ex) {
333                throw new WebApplicationException(new Throwable(ex.getMessage()), ex.getStatus());
334            } catch (IOException e) {
335                throw new WebApplicationException(new RuntimeException("HTTP I/O Error", e));
336            } finally {
337                tx.finish();
338            }
339        }
340    
341    
342    
343        /** Private Helper Methods */
344    
345        private void processTwitterSearchResponse(Topic twitterSearch, StringBuffer resultBody, ClientState clientState) {
346            DeepaMehtaTransaction tx = dms.beginTx();
347            try {
348                //
349                JSONObject results = new JSONObject(resultBody.toString());
350                // add or reference all new tweets and new twitter user accounts
351                for (int i=0; i < results.getJSONArray("statuses").length(); i++) {
352                    JSONObject item = results.getJSONArray("statuses").getJSONObject(i);
353                    // gets an existing or creates a new "Twitter User"-Topic
354                    JSONObject user = item.getJSONObject("user");
355                    String userName = "", twitterUserId = "", profileImageUrl = "";
356                    if (user.has("name")) userName = user.getString("name");
357                    if (user.has("id_str")) twitterUserId = user.getString("id_str");
358                    if (user.has("profile_image_url")) profileImageUrl = user.getString("profile_image_url");
359    
360                    Topic twitterUser = getTwitterUser(twitterUserId, userName, profileImageUrl, clientState);
361                    // gets an existing or creates a new "Tweet"-Topic
362                    Topic tweet = getTweet(item, twitterUser.getId(), clientState);
363                    // associate "Tweet" with "Twitter User" fixme: check if there's already an association
364                    twitterSearch.setCompositeValue(new CompositeValueModel().addRef(TWEET_URI, tweet.getId()),
365                            clientState, new Directives());
366                    // old style association
367                    /* dms.createAssociation(new AssociationModel(SEARCH_RESULT,
368                            new TopicRoleModel(searchId, DEFAULT_URI),
369                            new TopicRoleModel(tweet.getId(), DEFAULT_URI)), clientState); **/
370                }
371    
372                // get current (overall) result size
373                int size = twitterSearch.getRelatedTopics(AGGREGATION, PARENT_URI, CHILD_URI, TWEET_URI, false, false,
374                        0).getTotalCount();
375                // update our "Twitter Search"-Topic to reflect results after latest query
376                String nextPage = "", maxTweetId = "", refreshUrl = "";
377                JSONObject search_metadata;
378                if (results.has("search_metadata")) {
379                    search_metadata = results.getJSONObject("search_metadata");
380                    //
381                    if (search_metadata.has("max_id_str")) maxTweetId = search_metadata.getString("max_id_str");
382                    if (search_metadata.has("next_results")) nextPage = search_metadata.getString("next_results");
383                    if (search_metadata.has("refresh_url")) refreshUrl = search_metadata.getString("refresh_url");
384                }
385                // update search cointainer
386                twitterSearch.getCompositeValue().set(TWITTER_SEARCH_NEXT_PAGE_URI,
387                        nextPage, clientState, new Directives());
388                twitterSearch.getCompositeValue().set(TWITTER_SEARCH_RESULT_SIZE_URI, size,
389                        clientState, new Directives());
390                twitterSearch.getCompositeValue().set(TWITTER_SEARCH_TIME_URI, new Date().getTime(), clientState,
391                        new Directives());
392                twitterSearch.getCompositeValue().set(TWITTER_SEARCH_MAX_TWEET_URI,
393                        maxTweetId, clientState, new Directives());
394                twitterSearch.getCompositeValue().set(TWITTER_SEARCH_REFRESH_URL_URI,
395                        refreshUrl, clientState, new Directives());
396                tx.success();
397            } catch (JSONException e) {
398                log.warning("ERROR: We could not parse the response properly " + e.getMessage());
399                throw new RuntimeException("We could not parse the response properly." + e.getMessage());
400            } finally {
401                tx.finish();
402            }
403        }
404    
405        private Topic getTweet(JSONObject item, long userTopicId, ClientState clientState) {
406            Topic tweet = null;
407            DeepaMehtaTransaction tx = dms.beginTx();
408            try {
409                Topic tweetId = dms.getTopic(TWEET_ID_URI, new SimpleValue(item.getString("id_str")), true);
410                if (tweetId != null) {
411                    tweet = tweetId.getRelatedTopic(COMPOSITION, CHILD_URI, PARENT_URI, TWEET_URI, true, false);
412                } else {
413                    tweet = createTweet(item, userTopicId, clientState);
414                }
415                tx.success();
416            } catch (Exception e) {
417                throw new RuntimeException("We could neither fetch nor create this \"Tweet\".");
418            } finally {
419                tx.finish();
420            }
421            return tweet;
422        }
423    
424        private Topic createTweet(JSONObject item, long userTopicId, ClientState clientState) {
425            Topic tweet = null;
426            DeepaMehtaTransaction tx = dms.beginTx();
427            try {
428                // find twitter's doc on a tweets fields (https://dev.twitter.com/docs/platform-objects/tweets)
429                TopicModel topic = new TopicModel(TWEET_URI);
430                TopicModel coordinate = null;
431                String inReplyToStatus = "", withheldCopyright = "",
432                        withheldInCountries = "", withheldScope = "", metadata = "", entities = "";
433                int favourite_count = 0;
434                if (item.has("place") && !item.isNull("place")) {
435                    JSONObject place = item.getJSONObject("place");
436                    if (place.has("bounding_box")) {
437                        JSONObject box = place.getJSONObject("bounding_box");
438                        JSONArray tudes = box.getJSONArray("coordinates");
439                        // first value in array is always longitude
440                        JSONArray container = tudes.getJSONArray(0);
441                        JSONArray lower_left = container.getJSONArray(0);
442                        JSONArray lower_right = container.getJSONArray(1);
443                        JSONArray upper_right = container.getJSONArray(2);
444                        JSONArray upper_left = container.getJSONArray(3);
445                        double lng = (upper_left.getDouble(0) + upper_right.getDouble(0) + lower_left.getDouble(0) + lower_right.getDouble(0)) / 4;
446                        double lat = (upper_left.getDouble(1) + upper_right.getDouble(1) + lower_left.getDouble(1) + lower_right.getDouble(1)) / 4;
447                        coordinate = createGeoCoordinateTopicModel(lng, lat);
448                    }
449                }
450                // check for and create coordinates (simply overwrites place if given)
451                if (item.has("coordinates") && !item.isNull("coordinates")) {
452                    JSONObject coordinates = item.getJSONObject("coordinates");
453                    if (coordinates.has("coordinates")) {
454                        JSONArray tudes = coordinates.getJSONArray("coordinates");
455                        log.fine("Writing coordinates OVER place..");
456                        coordinate = createGeoCoordinateTopicModel(tudes.getDouble(0), tudes.getDouble(1));
457                    }
458                }
459                if (item.has("in_reply_to_status_id_str")) inReplyToStatus = item.getString("in_reply_to_status_id_str");
460                if (item.has("withheld_copyright")) withheldCopyright = item.getString("withheld_copyright");
461                if (item.has("withheld_in_countries")) withheldInCountries = item.getJSONArray("withheld_in_countries")
462                        .toString();
463                if (item.has("withheld_scope")) withheldScope = item.getString("withheld_scope");
464                if (item.has("favorite_count")) favourite_count = item.getInt("favorite_count");
465                if (item.has("place") && !item.isNull("place")) metadata = item.getJSONObject("place").toString();
466                if (item.has("entities") && !item.isNull("entities")) entities = item.getJSONObject("entities").toString();
467                CompositeValueModel content = new CompositeValueModel()
468                    .put(TWEET_CONTENT_URI, item.getString("text"))
469                    .put(TWEET_TIME_URI, item.getString("created_at")) // is utc-time
470                    .put(TWEET_ID_URI, item.getString("id_str"))
471                    .put(TWEET_ENTITIES_URI, entities) // is application-json/text
472                    .put(TWEET_METADATA_URI, metadata) // is application-json/text
473                    .put(TWEET_LOCATION_URI, "") // unused, to be removed (?)
474                    .put(TWEET_FAVOURITE_COUNT_URI, favourite_count)
475                    .put(TWEETED_TO_STATUS_ID, inReplyToStatus)
476                    .put(TWEET_WITHHELD_DMCA_URI, withheldCopyright) // a boolean indicating dmca-request
477                    .put(TWEET_WITHHELD_IN_URI, withheldInCountries) // is application-json/text (array)
478                    .put(TWEET_WITHHELD_SCOPE_URI, withheldScope) // is application-json/text (array)
479                    .put(TWEET_SOURCE_BUTTON_URI, item.getString("source"))
480                    .putRef(TWITTER_USER_URI, userTopicId);
481                topic.setCompositeValue(content);
482                if (coordinate != null) {
483                    content.put(GEO_COORDINATE_TOPIC_URI, coordinate.getCompositeValueModel());
484                }
485                tweet = dms.createTopic(topic, clientState);
486                tx.success();
487            } catch (JSONException jex) {
488                throw new RuntimeException(jex.getMessage());
489            } finally {
490                tx.finish();
491            }
492            return tweet;
493        }
494    
495        private Topic getTwitterUser(String userId, String userName, String userImageUrl, ClientState clientState) {
496            Topic identity = null;
497            DeepaMehtaTransaction tx = dms.beginTx();
498            try {
499                if (userId == null) throw new RuntimeException("Search Result is invalid. Missing Twitter-User-Id");
500                Topic twitterId = dms.getTopic(TWITTER_USER_ID_URI, new SimpleValue(userId), true);
501                if (twitterId != null) {
502                    identity = twitterId.getRelatedTopic(COMPOSITION, CHILD_URI, PARENT_URI, TWITTER_USER_URI, true, false);
503                } else {
504                    identity = createTwitterUser(userId, userName, userImageUrl, clientState);
505                }
506                tx.success();
507            } catch (Exception ex) {
508                log.info("Crashed query for twitter-id topic, trying to create new Twitter User Topic");
509                throw new RuntimeException(ex);
510            } finally {
511                tx.finish();
512            }
513            return identity;
514        }
515    
516        private Topic createTwitterUser(String userId, String userName, String userImageUrl, ClientState clientState) {
517            TopicModel twitterUser = new TopicModel(TWITTER_USER_URI);
518            twitterUser.setCompositeValue(new CompositeValueModel()
519                    .put(TWITTER_USER_ID_URI, userId)
520                    .put(TWITTER_USER_NAME_URI, userName)
521                    .put(TWITTER_USER_IMAGE_URI, userImageUrl));
522            return dms.createTopic(twitterUser, clientState);
523        }
524    
525        private TopicModel createGeoCoordinateTopicModel(double lng, double lat) {
526            TopicModel coordinates = new TopicModel(GEO_COORDINATE_TOPIC_URI);
527            CompositeValueModel model = new CompositeValueModel();
528            model.put(GEO_LONGITUDE_TYPE_URI, lng);
529            model.put(GEO_LATITUDE_TYPE_URI, lat);
530            coordinates.setCompositeValue(model);
531            return coordinates;
532        }
533    
534        /** Code running once, after plugin initialization. */
535    
536        private void checkACLsOfMigration() {
537            // secrets
538            ResultList<RelatedTopic> secrets = dms.getTopics("org.deepamehta.twitter.secret", false, 0);
539            Iterator<RelatedTopic> secs = secrets.iterator();
540            while (secs.hasNext()) {
541                RelatedTopic secret = secs.next();
542                DeepaMehtaTransaction dmx = dms.beginTx();
543                try {
544                    if (acService.getCreator(secret) == null) {
545                        log.fine("Running initial ACL update of twitter secret topics " + secret.getSimpleValue().toString());
546                        Topic admin = acService.getUsername("admin");
547                        String adminName = admin.getSimpleValue().toString();
548                        acService.setCreator(secret, adminName);
549                        acService.setOwner(secret, adminName);
550                        acService.setACL(secret, new AccessControlList( //
551                                new ACLEntry(Operation.WRITE, UserRole.OWNER)));
552                    }
553                    dmx.success();
554               } catch (Exception ex) {
555                    dmx.failure();
556                    log.warning(ex.getMessage());
557                    throw new RuntimeException(ex);
558                } finally {
559                    dmx.finish();
560                }
561            }
562            // keys
563            ResultList<RelatedTopic> keys = dms.getTopics("org.deepamehta.twitter.key", false, 0);
564            Iterator<RelatedTopic> ks = keys.iterator();
565            while (ks.hasNext()) {
566                RelatedTopic key = ks.next();
567                DeepaMehtaTransaction dmx = dms.beginTx();
568                try {
569                    if (acService.getCreator(key) == null) {
570                        log.fine("Running initial ACL update of twitter key topics " + key.getSimpleValue().toString());
571                        Topic admin = acService.getUsername("admin");
572                        String adminName = admin.getSimpleValue().toString();
573                        acService.setCreator(key, adminName);
574                        acService.setOwner(key, adminName);
575                        acService.setACL(key, new AccessControlList( //
576                                new ACLEntry(Operation.WRITE, UserRole.OWNER)));
577                    }
578                    dmx.success();
579               } catch (Exception ex) {
580                    dmx.failure();
581                    log.warning(ex.getMessage());
582                    throw new RuntimeException(ex);
583                } finally {
584                    dmx.finish();
585                }
586            }
587        }
588    
589        /** --- Implementing PluginService Interfaces to consume AccessControlService --- */
590    
591        @Override
592        @ConsumesService({
593            "de.deepamehta.plugins.accesscontrol.service.AccessControlService"
594        })
595        public void serviceArrived(PluginService service) {
596            if (service instanceof AccessControlService) {
597                acService = (AccessControlService) service;
598            }
599        }
600    
601        @Override
602        @ConsumesService({
603            "de.deepamehta.plugins.accesscontrol.service.AccessControlService"
604        })
605        public void serviceGone(PluginService service) {
606            if (service == acService) {
607                acService = null;
608            }
609        }
610    
611    }