com.projectlaver.batch.FacebookListingPostingItemProcessor.java Source code

Java tutorial

Introduction

Here is the source code for com.projectlaver.batch.FacebookListingPostingItemProcessor.java

Source

/*******************************************************************************
 * The MIT License (MIT)
 * 
 * Copyright (c) 2015 PURPLE & GOLD, INC
 * 
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 * 
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 * 
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 ******************************************************************************/
package com.projectlaver.batch;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.net.URI;

import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.builder.ToStringBuilder;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.batch.item.ItemProcessor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.io.FileSystemResource;
import org.springframework.core.io.Resource;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.social.facebook.api.Facebook;
import org.springframework.social.facebook.api.PageOperations;
import org.springframework.social.facebook.api.impl.FacebookTemplate;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.client.RestOperations;

import com.amazonaws.auth.AWSCredentials;
import com.amazonaws.auth.BasicAWSCredentials;
import com.amazonaws.services.s3.AmazonS3;
import com.amazonaws.services.s3.AmazonS3Client;
import com.amazonaws.services.s3.model.S3Object;
import com.fasterxml.jackson.databind.JsonNode;
import com.projectlaver.batch.domain.ListingStateChangeCursorItem;

public class FacebookListingPostingItemProcessor extends ListingPostingItemProcessor
        implements ItemProcessor<ListingStateChangeCursorItem, ListingStateChangeCursorItem> {

    /**
     * Instance variables
     */

    private final Log logger = LogFactory.getLog(getClass());

    private String schema;

    @Value("${aws.s3.access.key}")
    private String s3accessKey;

    @Value("${aws.s3.secret.key}")
    private String s3secretKey;

    @Value("${aws.s3.public.bucket.name}")
    private String s3publicBucketName;

    /**
     * Static variables
     */

    private static String INSERT_REPLY_PAGING_STATUS_SQL = " INSERT INTO ReplyPagingStatuses "
            + " SET version = ?, " + "   status = ?, " + "   listing_message_id = ?, " + "   providerId = ?, "
            + "   providerContentId = ? ";

    private static final String PAGE_SIZE_LIMIT_KEY = "limit";
    private static final String PAGE_SIZE_LIMIT = "1";

    private static final String FILTER_TYPE_KEY = "filter";
    private static final String STREAM_FILTER = "stream";

    private static final String ACCESS_TOKEN_KEY = "access_token";

    /**
     * Setter injection of the schema system property.
     * @param schema
     */
    public void setSchema(String schema) {
        this.schema = schema;
    }

    @Override
    public ListingStateChangeCursorItem process(ListingStateChangeCursorItem item) throws Exception {

        if (this.logger.isDebugEnabled()) {

            // Output the item for debugging purposes, but suppress the access token value
            String itemRaw = ToStringBuilder.reflectionToString(item);
            String accessTokenSuppressed = StringUtils.replacePattern(itemRaw, "accessToken=[0-9a-zA-Z]*,",
                    "accessToken={protected},");
            this.logger.debug(String.format("Entered the process method with item: %s", accessTokenSuppressed));
        }

        // Create the caption
        String caption = super.formatFacebookListingMessageContent(item.getListingType(), item.getListingKeyword(),
                item.getListingAmount(), item.getAnnouncementPreamble(), item.getListingId(), item.getLocale());

        if (this.logger.isDebugEnabled()) {
            this.logger.debug("Formatted facebook listing message is: '" + caption + "'");
        }

        File tempFile = null;
        try {

            String filename = item.getImageFilename();

            tempFile = this.createTempFile(filename);

            // Get the image from AWS
            this.copyS3ObjectToTempFile(tempFile, filename);

            Resource photo = new FileSystemResource(tempFile);

            Facebook api = new FacebookTemplate(item.getAccessToken());
            PageOperations po = api.pageOperations();

            // Post the photo and get the ID. 
            // Note that the photo id is not the same as the facebook content id that we'll use to retrieve comments
            String photoId = po.postPhoto(item.getFacebookPageId(), item.getFacebookAlbumId(), photo, caption);
            if (this.logger.isDebugEnabled()) {
                this.logger.debug("Uploaded a photo to Facebook with id: " + photoId);
            }

            JsonNode facebookPostJson = this.getLatestFacebookPost(api.restOperations(), item.getFacebookPageId());

            // get the content id
            String contentId = facebookPostJson.findValue("id").toString().replaceAll("\"", "");

            // confirm the content id refers to our post
            Boolean isContentIdConfirmed = this.confirmContentIdIsListing(api.restOperations(), contentId, caption,
                    photoId);

            if (!isContentIdConfirmed) {

                this.logger.error("Unable to correlate the facebook content id: " + contentId
                        + " with the photoId: " + photoId + " in the context of our listing with id: "
                        + item.getListingId() + ". Taking compensating actions.");

                // this means we didn't get the right content id corresponding with the photo we just posted.
                // to compensate, delete the photo using the photoId
                this.deletePhoto(api, photoId);

                // throw an exception
                throw new RuntimeException(
                        "Could not correlate newly posted Facebook photo with the latest Page content id.");
            }

            // get the content href and remove the surrounding double quotes (")
            String facebookContentLink = facebookPostJson.findValue("link").toString();
            facebookContentLink = facebookContentLink.substring(1, facebookContentLink.length() - 1);

            if (this.logger.isDebugEnabled()) {
                this.logger.debug("The Facebook post content id is: " + contentId);
                this.logger.debug("The facebook contentLink is: " + facebookContentLink);
            }

            // insert the listing message and get the row's id
            Long listingMessageId = super.insertListingMessage(item, caption, contentId, facebookContentLink);

            if (this.logger.isDebugEnabled()) {
                this.logger.debug("Inserted the listing message with id: " + listingMessageId);
            }

            // insert a row into reply_paging_status to enable paged batch fetching of comments
            JdbcTemplate jdbcTemplate = new JdbcTemplate(super.getDataSource());
            int rows = jdbcTemplate.update(INSERT_REPLY_PAGING_STATUS_SQL, new Object[] { 0, // version
                    "IDLE", // status
                    listingMessageId, // listingMessageId
                    item.getProviderId(), // providerId
                    contentId // providerContentId
            });

            item.setDidPostingComplete((listingMessageId != null) && (rows == 1));

            // we have to set this field because the writer will set it to "null" otherwise.
            item.setNewListingStateChangeUpdatedStatus("PENDING");

        } catch (Exception e) {

            this.logger.error("Exception while trying to post listing: " + item.getListingId() + " to facebook.",
                    e);

            // Could not confirm the contentId. Mark this item as set aside
            // without a listing message.
            item.setDidPostingComplete(false);
            item.setNewListingStateChangeUpdatedStatus("POSTING-ERROR");

        } finally {

            // cleanup the temp file
            if (tempFile != null) {
                tempFile.delete();
            }
        }

        return item;
    }

    @Override
    protected String getSaleMessageKey() {
        return "saleForm.facebook.template.sale";
    }

    @Override
    protected String getDonationMessageKey() {
        return "saleForm.facebook.template.donation";
    }

    @Override
    protected String getCampaignMessageKey() {
        return "saleForm.facebook.template.opt-in-campaign";
    }

    Boolean confirmContentIdIsListing(RestOperations ro, String contentId, String caption, String photoId) {

        // Fetch the object using the contentId
        JsonNode contentNode = ro.getForObject(Facebook.GRAPH_API_URL + "{contentId}", JsonNode.class, contentId);
        String contentMessage = contentNode.findValue("message").asText();
        String contentObjectId = contentNode.findValue("object_id").asText();

        // Return true if the caption and photoId match
        return StringUtils.equals(caption, contentMessage) && StringUtils.equals(photoId, contentObjectId);
    }

    JsonNode getLatestFacebookPost(RestOperations ro, String pageId) throws InterruptedException {

        // Introduce a slight delay here so that when we query for the latest post the 
        // eventual consistency is likely to have caught up
        Thread.sleep(2000);

        // Get the content id with the expectation that it's the topmost post on this page
        JsonNode responseNode = ro.getForObject(
                Facebook.GRAPH_API_URL + "{facebookPageId}/posts?limit=1&filter=stream", JsonNode.class, pageId);
        JsonNode dataNode = responseNode.path("data");

        return dataNode;
    }

    void deletePhoto(Facebook api, String photoId) {
        this.logger.error("+deletePhoto() with photoId: " + photoId);

        api.delete(photoId);
        this.logger.error("-deletePhoto()");
    }

    File createTempFile(String filename) throws IOException {
        int dotSeparator = filename.indexOf(".");
        String filePrefix = filename.substring(0, dotSeparator);
        String fileSuffix = filename.substring(dotSeparator + 1);
        File tempFile = File.createTempFile(filePrefix, fileSuffix);

        if (this.logger.isDebugEnabled()) {
            this.logger.debug("File prefix: " + filePrefix + ", file suffix: " + fileSuffix);
        }
        return tempFile;
    }

    void copyS3ObjectToTempFile(File tempFile, String filename) throws IOException, FileNotFoundException {
        AWSCredentials myCredentials = new BasicAWSCredentials(this.s3accessKey, this.s3secretKey);
        AmazonS3 s3 = new AmazonS3Client(myCredentials);
        S3Object object = s3.getObject(this.s3publicBucketName, filename);

        IOUtils.copy(object.getObjectContent(), new FileOutputStream(tempFile));
    }

}