Java tutorial
/******************************************************************************* * 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.service; import java.awt.image.BufferedImage; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.net.URLConnection; import java.security.Security; import java.util.ArrayList; import java.util.Calendar; import java.util.Date; import java.util.HashMap; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Set; import javax.imageio.IIOImage; import javax.imageio.ImageIO; import javax.imageio.ImageWriteParam; import javax.imageio.ImageWriter; import javax.imageio.stream.FileImageOutputStream; import javax.persistence.EntityManager; import javax.persistence.PersistenceContext; import javax.servlet.http.HttpServletResponse; import org.apache.commons.lang3.RandomStringUtils; 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.jets3t.service.CloudFrontService; import org.jets3t.service.utils.ServiceUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.cache.annotation.Cacheable; import org.springframework.context.MessageSource; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.security.access.AccessDeniedException; import org.springframework.social.connect.Connection; import org.springframework.social.connect.ConnectionRepository; import org.springframework.social.connect.NotConnectedException; import org.springframework.social.connect.UsersConnectionRepository; import org.springframework.social.facebook.api.Facebook; import org.springframework.social.support.URIBuilder; import org.springframework.social.twitter.api.Twitter; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.util.FileCopyUtils; import org.springframework.web.multipart.MultipartFile; 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.amazonaws.services.s3.transfer.TransferManager; import com.amazonaws.services.s3.transfer.Upload; import com.braintreegateway.BraintreeGateway; import com.braintreegateway.MerchantAccount; import com.braintreegateway.MerchantAccountRequest; import com.braintreegateway.Result; import com.mortennobel.imagescaling.AdvancedResizeOp; import com.mortennobel.imagescaling.DimensionConstrain; import com.mortennobel.imagescaling.ResampleFilters; import com.mortennobel.imagescaling.ResampleOp; import com.projectlaver.domain.Address; import com.projectlaver.domain.ContentFile; import com.projectlaver.domain.EffectiveVoucherStatus; import com.projectlaver.domain.Inventory; import com.projectlaver.domain.Listing; import com.projectlaver.domain.Message; import com.projectlaver.domain.Payment; import com.projectlaver.domain.User; import com.projectlaver.domain.Voucher; import com.projectlaver.integration.SocialProviders; import com.projectlaver.repository.BulkOperationsRepository; import com.projectlaver.repository.ListingRepository; import com.projectlaver.repository.UserRepository; import com.projectlaver.util.CheckoutAlreadyCompletedException; import com.projectlaver.util.FileList; import com.projectlaver.util.FileMetadata; import com.projectlaver.util.GoodsType; import com.projectlaver.util.InitiatedPaymentDTO; import com.projectlaver.util.ListingActionButton; import com.projectlaver.util.ListingDetailDTO; import com.projectlaver.util.ListingType; import com.projectlaver.util.MessageStatus; import com.projectlaver.util.StartCheckoutFailedException; import com.projectlaver.util.VoucherStatus; @Service @Transactional(readOnly = false) public class ListingService { @PersistenceContext private EntityManager em; @Autowired private BulkOperationsRepository bulkMessageOperationsRepository; @Autowired private ListingRepository listingRepository; @Autowired private MessageService messageService; @Autowired private PaymentProviderService paymentProviderService; @Autowired private PaymentService paymentService; @Autowired private UserService userService; @Autowired private MessageSource messageSource; @Autowired private UsersConnectionRepository usersConnectionRepository; @Autowired private UserRepository userRepository; @Autowired private SocialService socialService; @Autowired private org.springframework.context.ApplicationContext springContext; @Autowired BraintreeGateway braintreeGateway; private final Log logger = LogFactory.getLog(getClass()); private byte[] derPrivateKey = new byte[0]; // properties @Value("${aws.s3.access.key}") private String s3accessKey; @Value("${aws.s3.secret.key}") private String s3secretKey; @Value("${aws.s3.private.bucket.name}") private String s3privateBucketName; @Value("${aws.s3.public.bucket.name}") private String s3publicBucketName; @Value("${aws.cloudfront.distributionDomain}") private String cloudfrontDistributionDomain; @Value("${aws.cloudfront.privateKeyFilePath}") private String cloudfrontPrivateKeyFilePath; @Value("${aws.cloudfront.keyPairId}") private String cloudfrontKeyPairId; @Value("${aws.cloudfront.signedUrlValidMinutes}") private Integer cloudfrontSignedUrlValidMinutes; @Value("${paypalPayment.checkoutUrl}") private String checkoutUrl; @Value("${braintree.masterMerchantAccountId}") private String masterMerchantAccountId; /** * Static variables */ private static Map<String, String> countryList = new HashMap<String, String>(); private static final Integer MAX_FILES_PER_LISTING = 25; public static final String IN_PROGRESS_PAYMENT_KEY = "inProgressPayment"; /** * Constructor */ public ListingService() { // Add the bouncy castle provider so that it exists when we need to sign URL's for cloudfront Security.addProvider(new org.bouncycastle.jce.provider.BouncyCastleProvider()); } /** * Public methods */ @Transactional(readOnly = false) public Listing create(Listing listing) throws Exception { // set the expiration for one year from now if unset if (listing.getExpires() == null) { listing.setExpires(this.addDays(new Date(), 365)); } // merge the user (reattach to DB) User user = listing.getSeller(); User mergedUser = this.em.merge(user); listing.setSeller(mergedUser); if (listing.getImageAsFile() != null) { // upload the image preview to the public S3 bucket AWSCredentials myCredentials = new BasicAWSCredentials(this.s3accessKey, this.s3secretKey); TransferManager tx = new TransferManager(myCredentials); Upload myUpload = tx.upload(this.s3publicBucketName, listing.getImageFilename(), listing.getImageAsFile()); myUpload.waitForCompletion(); } if (listing.getContentFiles() != null && listing.getContentFiles().size() > 0) { Set<ContentFile> contentFiles = listing.getContentFiles(); AWSCredentials myCredentials = new BasicAWSCredentials(this.s3accessKey, this.s3secretKey); TransferManager tx = new TransferManager(myCredentials); for (ContentFile file : contentFiles) { // upload the digital content to the private S3 bucket Upload myUpload = tx.upload(this.s3privateBucketName, file.getContentFilename(), file.getDigitalContentAsFile()); myUpload.waitForCompletion(); } } return this.listingRepository.save(listing); } @Transactional(readOnly = true) @Cacheable(value = "listings") public Listing findById(Long id) { Listing listing = this.listingRepository.findByListingIdEagerlyFetchContentAndMessage(id); if (this.logger.isDebugEnabled()) { this.logger.debug("findById for id: " + id + " returned this listing: " + listing); } return listing; } @Transactional(readOnly = true) public Listing findByIdBypassCache(Long id) { Listing listing = this.listingRepository.findByListingIdEagerlyFetchContentAndMessage(id); if (this.logger.isDebugEnabled() && listing != null) { String logInventory = null; List<Inventory> inventoryList = listing.getInventories(); if (inventoryList == null) { logInventory = "unlimited"; } else { for (Inventory inventory : inventoryList) { logInventory = logInventory + String.format( " productCode: %s remainingQuantity: %d totalQuantity: %d", inventory.getProductCode(), inventory.getRemainingQuantity(), inventory.getQuantity()); } } this.logger.debug(String.format( "findByIdBypassCache for id: %d returned listing type: %s, subType: %s with inventory: %s", listing.getId(), listing.getType(), listing.getSubType(), logInventory)); } return listing; } /** * * Authorizes that the requestor is entitled to the content by checking to see if the user has bought the item * or is the item's seller. * * Writes the requested digital content to the OutputStream parameter if the requestor is authorized. * * @param requestor * @param contentFilename * @param outputStream * @throws Exception */ @Transactional(readOnly = true) public void streamPurchasedContent(User requestor, String contentFilename, HttpServletResponse response, OutputStream outputStream) throws Exception { Boolean isAuthorized = false; Payment payment = this.paymentService.findByStatusAndBuyerAndContentFilename(requestor.getId(), contentFilename, this.paymentService.getValidPaymentStatuses()); Listing listing = null; if (payment.getListing() == null) { listing = this.listingRepository.findBySellerByUsernameAndContentFilename(requestor.getUsername(), contentFilename); if (listing != null) { // The requestor is the seller isAuthorized = true; } } else { // The requestor is a buyer listing = payment.getListing(); isAuthorized = true; } ContentFile theFile = null; if (isAuthorized) { // This is an authorized download, so stream the content Set<ContentFile> files = listing.getContentFiles(); for (ContentFile file : files) { if (file.getContentFilename().equals(contentFilename)) { theFile = file; break; } } } if (theFile == null) { if (this.logger.isDebugEnabled()) { this.logger.debug("Request for contentFilename: " + contentFilename + " was rejected because requestor " + requestor.getId() + " could not be authorized."); } response.setStatus(HttpServletResponse.SC_NOT_FOUND); } else { this.setDownloadHeaders(theFile, contentFilename, response); try { this.streamAwsContentToResponse(contentFilename, this.s3privateBucketName, outputStream); // Persist the download instance so we can count / calculate metrics on it this.bulkMessageOperationsRepository.insertDownloadInstance(requestor.getId(), listing.getId(), theFile.getId(), (payment == null ? null : payment.getId())); } catch (IOException e) { this.logger.error("Caught an IOException while trying to stream: " + contentFilename + " with requesting user: " + requestor.getUsername(), e); } } } @Transactional(readOnly = true) public void streamImage(String contentFilename, OutputStream outputStream) throws Exception { this.streamAwsContentToResponse(contentFilename, this.s3publicBucketName, outputStream); } /** * Looks for listings that this user has expressed an interest in (i.e., has mentioned the hashtag) but * where the message is stuck in "PENDING_MEANS_OF_PAYMENT" status * * @param userId * @return */ @Transactional(readOnly = true) public List<Long> findUserPendingMeansOfPaymentListingIds(Long userId) { return this.bulkMessageOperationsRepository.findUserPendingMeansOfPaymentListingIds(userId); } @Transactional(readOnly = true) public FileList handleDigitalContentUpload(FileList fileList, LinkedList<MultipartFile> multipartFiles) { List<FileMetadata> files = fileList.getFiles(); for (MultipartFile mpf : multipartFiles) { if (this.logger.isDebugEnabled()) { this.logger.debug(mpf.getOriginalFilename() + " uploaded! " + files.size()); } // limit max files per listing if (files.size() >= MAX_FILES_PER_LISTING) { throw new RuntimeException("Exceeded max files per listing with size: " + files.size()); } // create new fileMeta FileMetadata fileMeta = new FileMetadata(); fileMeta.setName(mpf.getOriginalFilename()); fileMeta.setAwsFilename(this.createFilename(mpf.getOriginalFilename())); fileMeta.setId(Long.valueOf(RandomStringUtils.randomNumeric(2))); fileMeta.setContentType(mpf.getContentType()); fileMeta.setSize(mpf.getSize()); fileMeta.setType(mpf.getContentType()); if (mpf.getName().equals("image")) { fileMeta.setIsCampaignImage(true); // there can't be two campaign images for a listing -- if another one exists, error for (int i = 0; i < files.size(); i++) { FileMetadata otherMeta = files.get(i); if (otherMeta.getIsCampaignImage()) { throw new RuntimeException("Multiple listing images are not allowed."); } } } String tempDir = System.getProperty("java.io.tmpdir"); String transientPath = tempDir + fileMeta.getAwsFilename(); File thisFile = new File(transientPath); fileMeta.setTransientPath(transientPath); try { // copy file to local disk mpf.transferTo(thisFile); // create a thumbnail version Boolean isImage = fileMeta.getContentType().indexOf("image") != -1; if (isImage) { BufferedImage sourceImage = ImageIO.read(thisFile); ResampleOp resampleOp = new ResampleOp(DimensionConstrain.createMaxDimension(100, 100, true)); resampleOp.setFilter(ResampleFilters.getLanczos3Filter()); resampleOp.setUnsharpenMask(AdvancedResizeOp.UnsharpenMask.Normal); BufferedImage destImage = resampleOp.filter(sourceImage, null); ImageWriter writer = null; FileImageOutputStream output = null; try { writer = ImageIO.getImageWritersByFormatName("png").next(); ImageWriteParam param = writer.getDefaultWriteParam(); String thumbnailFilename = this.getThumbnailFilename(fileMeta.getAwsFilename()); output = new FileImageOutputStream(new File(tempDir + thumbnailFilename)); writer.setOutput(output); IIOImage iioImage = new IIOImage(destImage, null, null); writer.write(null, iioImage, param); } catch (IOException ex) { throw ex; } finally { if (writer != null) { writer.dispose(); } if (output != null) { output.close(); } } fileMeta.setThumbnailUrl("newListingThumbnail/" + fileMeta.getId()); fileMeta.setDeleteType("DELETE"); fileMeta.setDeleteUrl("newListingContentDelete/" + fileMeta.getId()); } fileMeta.setUrl("newListingContent/" + fileMeta.getId()); } catch (IOException e) { throw new RuntimeException("Error occured while handling the multipart file.", e); } // 2.4 add to files at the head files.add(0, fileMeta); } return fileList; } @Transactional(readOnly = true) public void getNewListingContent(FileList fileList, Long id, HttpServletResponse response) { if (fileList != null) { List<FileMetadata> files = fileList.getFiles(); for (FileMetadata meta : files) { if (meta.getId().equals(id)) { String contentFilename = meta.getAwsFilename(); this.streamFileToResponse(contentFilename, meta.getContentType(), meta.getSize().intValue(), response); break; } } } } @Transactional(readOnly = true) public void getNewListingContentThumbnail(FileList fileList, Long id, HttpServletResponse response) { if (fileList != null) { List<FileMetadata> files = fileList.getFiles(); for (FileMetadata meta : files) { if (meta.getId().equals(id)) { String thumbnailFilename = this.getThumbnailFilename(meta.getAwsFilename()); this.streamFileToResponse(thumbnailFilename, meta.getContentType(), meta.getSize().intValue(), response); break; } } } } @Transactional(readOnly = true) public Map<String, List<Map<String, Object>>> deleteUploadedDigitalContent(Long id, FileList fileList) { if (this.logger.isDebugEnabled()) { this.logger.debug("Delete uploaded file: " + id + " called."); } List<Map<String, Object>> result = new ArrayList<Map<String, Object>>(); Map<String, List<Map<String, Object>>> results = new HashMap<String, List<Map<String, Object>>>(); results.put("files", result); List<FileMetadata> files = fileList.getFiles(); for (int i = 0; i < files.size(); i++) { FileMetadata fileMeta = files.get(i); if (fileMeta.getId().equals(id)) { // Delete the corresponding temp files String tempDir = System.getProperty("java.io.tmpdir"); String thumbnailFilename = this.getThumbnailFilename(fileMeta.getAwsFilename()); File thumbnail = new File(tempDir + thumbnailFilename); thumbnail.delete(); File file = new File(tempDir + fileMeta.getAwsFilename()); file.delete(); // remove the deleted file from the fileList files.remove(i); Map<String, Object> success = new HashMap<String, Object>(); success.put(fileMeta.getName(), true); result.add(success); break; } } return results; } @Transactional(readOnly = true) public Page<Listing> findListingsBySellerId(Long userId, Pageable p) { return this.listingRepository.findListingsBySellerId(userId, p); } @Transactional(readOnly = true) public Map<String, Object> doListingDetail(ListingDetailDTO dto) { if (this.logger.isDebugEnabled()) { this.logger.debug(String.format("Listing Service doListingDetail for id: %d", dto.getListingId())); } Map<String, Object> result = new HashMap<String, Object>(); // find the listing by id and add to model Listing listing = null; if (dto.getIsCheckoutMode()) { // if we're in checkout mode, load directly from the database so we don't get a stale status / quantity listing = this.findByIdBypassCache(dto.getListingId()); } else { // if we're not in checkout mode, a slightly stale status / quantity isn't a show stopper listing = this.findById(dto.getListingId()); } if (listing != null) { result.put("saleInfo", listing); if (listing.getType().equals(ListingType.CAMPAIGN)) { result.put("isDigitalGiveaway", listing.getGoodsType().equals(GoodsType.DIGITAL)); } result.putAll(this.addSellerProfileAttributesToModel(listing)); // get the listing detail action button back here ListingActionButton button = this.createListingActionButton(listing, dto); result.put("listingActionButton", button); result.put("actionButtonText", button.getText()); result.put("isActionButtonActive", button.isEnabled()); // if the button is active, setup the action for one of: login / comment / download String buttonActionMode = button.getActionMode(); // make the current referrer available to the listingDetail page String referrer = this.identifyListingDetailReferer(dto.getReferrerHttpHeader()); result.put("listingDetailReferer", referrer); if (this.logger.isDebugEnabled()) { this.logger.debug("Setting listingDetailReferer: " + referrer); } result.put("buttonActionMode", buttonActionMode); // Add the days, hours, minutes, and seconds remaining result.putAll(this.addTimeRemainingAttributes(listing)); } return result; } /** * Checks to see if the (presumed) seller has the right configuration to list on * various sites. For Twitter, just need a connection. For Facebook, need to * have the various album / page id's as well as a page url and a valid * token. * * @return */ @Transactional(readOnly = true) public Map<String, Object> getSellerEnabledListingProviders() { User seller = this.userService.getCurrentUser(); Boolean enableTwitterListings = false; String twitterDisplayName = null; Boolean enableFacebookListings = false; // if the user doesn't have an email, cannot create a listing if (StringUtils.isNotBlank(seller.getEmailAddress())) { // check to see if this seller is in the current initialized twitter stream enableTwitterListings = this.bulkMessageOperationsRepository.isInCurrentTwitterStream(seller); if (enableTwitterListings) { Connection<Twitter> twitterConnection = this.socialService .getCurrentUserSocialConnection(Twitter.class); twitterDisplayName = twitterConnection.getDisplayName(); } // first ensure the user has their facebook metadata configured if (StringUtils.isNoneBlank(seller.getFacebookAlbumId(), seller.getFacebookPageId(), seller.getFacebookPageUrl())) { Connection<Facebook> facebookConnection = this.socialService .getCurrentUserSocialConnection(Facebook.class); if (facebookConnection != null) { // Confirm with facebook that this user has granted the manage_pages permissions enableFacebookListings = this.socialService .confirmCurrentUserFacebookPermissions("manage_pages", "publish_actions"); } } } HashMap<String, Object> result = new HashMap<String, Object>(); result.put("doEnableTwitter", true); result.put("twitterDisplayName", twitterDisplayName); result.put("doEnableFacebook", enableFacebookListings); return result; } @Transactional(readOnly = true) public Map<String, Object> getTransactionDetails(Long transactionId) { Map<String, Object> result = new HashMap<String, Object>(); Long requestorId = this.userService.getCurrentUserId(); // only allow viewing the Payment details if the requestor is the payer or the payee Payment payment = this.paymentService.securedFindPaymentFetchEagerly(transactionId, requestorId); if (payment != null) { Message buyingMessage = payment.getMessage(); result.put("paymentInfo", payment); Boolean userIsBuyer = requestorId.equals(payment.getPayer().getId()); result.put("userIsBuyer", userIsBuyer); if (userIsBuyer) { // If this is the buyer, add the links to Follow / Like User seller = payment.getListing().getSeller(); String sellerUsername = seller.getUsername(); ConnectionRepository connectionRepository = this.usersConnectionRepository .createConnectionRepository(sellerUsername); Connection<Twitter> twitterConnection = null; try { twitterConnection = connectionRepository.getPrimaryConnection(Twitter.class); if (twitterConnection != null) { result.put("sellerTwitterProfileUrl", twitterConnection.getProfileUrl()); } } catch (NotConnectedException e) { // not fatal, just carry on without the twitter profile url link } String sellerFacebookPageUrl = seller.getFacebookPageUrl(); if (sellerFacebookPageUrl != null) { result.put("sellerFacebookPageUrl", sellerFacebookPageUrl); } } result.put("userIsSeller", requestorId.equals(payment.getPayee().getId())); // logic depending on whether it's a digital or physical campaign GoodsType goodsType = payment.getListing().getGoodsType(); //if( this.doesListingHavePhysicalGoods( payment, listingType ) ) { if (goodsType.equals(GoodsType.PHYSICAL)) { // Do stuff related to physical campaigns result.put("listingIsPhysical", true); result.put("listingIsDigital", false); result.put("listingIsVoucher", false); if (payment.getShippingAddress() == null) { // no address set Set<Address> addresses = payment.getPayer().getAddresses(); if (addresses != null && !addresses.isEmpty()) { this.logger.debug("Payment with id: " + transactionId + " does not have an associated address, but this user has existing addresses."); // add the buyer's addresses to the result so they can be displayed result.put("buyerAddresses", addresses); } else { this.logger.debug( "Payment does not have an address, nor does the buyer have any existing addresses."); } result.put("doesPaymentHaveShippingAddress", false); result.put("isPendingShipment", false); result.put("hasBeenShipped", false); } else { // this payment has an address set this.logger.debug("Payment with id: " + transactionId + " is already associated with address: " + payment.getShippingAddress().getId()); result.put("doesPaymentHaveShippingAddress", true); result.put("isPendingShipment", buyingMessage.getStatus().equals(MessageStatus.PENDING_SHIPMENT)); result.put("hasBeenShipped", (payment.getHasBeenShipped() != null ? payment.getHasBeenShipped() : false)); } // add Country typecode list result.put("countryList", this.getCountryList()); } else if (goodsType.equals(GoodsType.DIGITAL)) { // Do stuff related to digital campaigns result.put("listingIsDigital", true); result.put("listingIsPhysical", false); result.put("listingIsVoucher", false); Set<ContentFile> contentFiles = payment.getListing().getContentFiles(); for (ContentFile file : contentFiles) { // this is the URL for viewing the content inline file.setCloudFrontUrl(this.createSignedUrl(file.getContentFilename(), null)); // this is the URL for downloading the content file.setCloudFrontDownloadableUrl(this.createSignedUrl(file.getContentFilename(), file.getDigitalContentOriginalFilename())); } } else { // voucher listing result.put("listingIsVoucher", true); result.put("listingIsPhysical", false); result.put("listingIsDigital", false); List<Voucher> vouchers = payment.getVouchers(); if (vouchers != null && userIsBuyer) { for (Voucher voucher : vouchers) { EffectiveVoucherStatus evs = voucher.getCurrentEffectiveVoucherStatus(); if (evs.getStatus().equals(VoucherStatus.UNREDEEMED)) { /** * The voucher barcode image is stored in the private S3 bucket. Create a signed * URL for the user to access it. * * Note that even though the content itself is not heavy in this case, it's still * preferable to route them through Cloudfront rather than having them go through * our secured ListingController method for private content, since that re-authorizes * the user every time (by going to the DB). */ voucher.setCloudFrontUrl(this.createSignedUrl(voucher.getFilename(), null)); } } } } } else { throw new AccessDeniedException(String.format( "User with id: %d requested transaction details for a transaction to which this user is not a party. Transaction id: %d", requestorId, transactionId)); } return result; } @SuppressWarnings("unchecked") @Transactional(readOnly = true) public Map<String, String> getCountryList() { synchronized (ListingService.countryList) { if (ListingService.countryList.isEmpty()) { ListingService.countryList = (Map<String, String>) this.springContext.getBean("countryList"); } return ListingService.countryList; } } @Transactional(readOnly = false) public Boolean markPaymentAsShipped(Long paymentId) { Boolean result = null; Long sellerId = this.userService.getCurrentUserId(); Payment payment = this.paymentService.securedFindPaymentFetchEagerly(paymentId, sellerId); if (payment != null && payment.getPayee().getId().equals(sellerId)) { // Make sure it's not already marked "has been shipped" if (payment.getMessage().getStatus().equals(MessageStatus.PENDING_SHIPMENT) && (payment.getHasBeenShipped() == null) || !payment.getHasBeenShipped()) { payment.setHasBeenShipped(true); // persist the updated payment this.paymentService.save(payment); if (this.logger.isDebugEnabled()) { this.logger.debug("Persisted the payment: " + paymentId + " hasBeenShipped value to true (aka this item has been shipped)"); } // reset the message's batch processor to null so the next batch job run will pick it up again Message message = this.messageService.findByPaymentId(payment.getId()); message.setBatchProcessor(null); this.messageService.update(message); result = true; } else { // this is the seller but it's either not pending shipment or it's already been shipped result = false; } } else { // throw Exception - unexpected condition throw new RuntimeException("Unexpected condition in markPaymentAsShipped. payment: " + payment + ", caller userId: " + sellerId); } return result; } @Transactional(readOnly = false) public Map<String, Object> redeemVoucher(String serialNumber) { Long sellerId = this.userService.getCurrentUserId(); return this.paymentService.redeemVoucher(sellerId, serialNumber); } @Transactional(readOnly = false) public InitiatedPaymentDTO startCheckout(User buyer, Long listingId, String productCode, Integer quantity, InitiatedPaymentDTO existingInitiatedPayment) { if (this.logger.isDebugEnabled()) { this.logger.debug(String.format( "+++startCheckout for buyer id: %d, listing id: %d, and existingInProgressPayment: %s", buyer.getId(), listingId, ToStringBuilder.reflectionToString(existingInitiatedPayment))); } Listing listing = this.findByIdBypassCache(listingId); User seller = listing.getSeller(); InitiatedPaymentDTO initiatedPayment = null; if (existingInitiatedPayment != null && existingInitiatedPayment.hasRemainingValidity()) { if (this.logger.isDebugEnabled()) { this.logger.debug("Re-using existing initiatedPayment object."); } // use the existingInitiatedPayment initiatedPayment = existingInitiatedPayment; } else { if (this.logger.isDebugEnabled()) { this.logger.debug("Creating a new initiatedPayment object."); } // Either the existingInitiatedPayment is null or it has no remaining validity. // Use the payapalService to start a new checkout. initiatedPayment = this.paymentProviderService.startCheckout(buyer, seller, listing, productCode, quantity); // add a few extra parameters initiatedPayment.setProductDescription(listing.getTitle()); initiatedPayment.setAmountCents(listing.getAmountCents()); initiatedPayment.setBuyerEmail(buyer.getEmailAddress()); } if (initiatedPayment == null || !initiatedPayment.getDidStartCheckoutSucceed() || initiatedPayment.getStartCheckoutErrorReason() != null) { // error reason indicates that there was an insufficient quantity errors // could not initiate payment throw new StartCheckoutFailedException( String.format("Could not start checkout for buyer id: %d and listing id: %d.", buyer.getId(), listing.getId())); } return initiatedPayment; } @Transactional(readOnly = true) public Boolean doesListingExist(Long id) { return this.bulkMessageOperationsRepository.doesListingExist(id); } @Transactional(readOnly = true) public Set<Listing> findActiveFacebookListings() { return this.listingRepository.findActiveFacebookListings(); } @Transactional(readOnly = true) public Boolean createBraintreeSubMerchant(Long sellerId) { if (this.logger.isDebugEnabled()) { this.logger.debug( String.format("Attempting to create a braintree sub merchant for seller id: %d", sellerId)); } // Lookup the user and fetch addresses User seller = this.userRepository.findUserEagerlyFetchAddresses(sellerId); Set<Address> addresses = seller.getAddresses(); Address address = addresses.iterator().next(); if (this.logger.isDebugEnabled()) { this.logger.debug( String.format("Registering the new seller with address: %s", address.getCompactFormat())); } // append address line 1 to line 2, if exists String streetAddress = address.getSecondLine(); if (StringUtils.isNotBlank(address.getFirstLine())) { streetAddress = streetAddress.concat(address.getFirstLine()); } MerchantAccountRequest request = new MerchantAccountRequest(); request.individual().firstName(seller.getFirstName()).lastName(seller.getLastName()) .email(seller.getEmailAddress()).phone(seller.getMobileNumber()).dateOfBirth(seller.getDob()) .address().streetAddress(streetAddress).locality(address.getCity()).region(address.getState()) .postalCode(address.getZip()).done().done().funding() .destination(MerchantAccount.FundingDestination.EMAIL).email(seller.getEmailAddress()).done() .tosAccepted(true).masterMerchantAccountId(this.masterMerchantAccountId) .id(seller.getId().toString()); // Attempt to create the sub merchant but trap any exceptions try { Result<MerchantAccount> result = this.braintreeGateway.merchantAccount().create(request); if (result.isSuccess()) { if (this.logger.isDebugEnabled()) { this.logger.debug("Braintree sub merchant registration succeeded."); } return true; } else { this.logger.error(String.format( "Unable to register the user as a sub merchant with Braintree. Error message: %s", result.getMessage())); return false; } } catch (RuntimeException e) { this.logger.error("Error while attempting to create the Braintree sub merchant.", e); return false; } } /** * Internal methods */ void streamFileToResponse(String filename, String contentType, int contentLength, HttpServletResponse response) { String tempDir = System.getProperty("java.io.tmpdir"); File image = new File(tempDir + filename); // this wasn't working right, so I killed it. // see this response if it become necessary to fix: http://stackoverflow.com/a/8797893/1325237 // response.setContentLength( contentLength ); response.setContentType(contentType); try { InputStream is = new FileInputStream(image); FileCopyUtils.copy(is, response.getOutputStream()); } catch (IOException e) { this.logger.error("Could not write thumbnail: " + filename, e); } } void streamAwsContentToResponse(String contentFilename, String bucketName, OutputStream outputStream) throws IOException { AWSCredentials myCredentials = new BasicAWSCredentials(this.s3accessKey, this.s3secretKey); AmazonS3 s3 = new AmazonS3Client(myCredentials); S3Object object = s3.getObject(bucketName, contentFilename); FileCopyUtils.copy(object.getObjectContent(), outputStream); } void setDownloadHeaders(ContentFile file, String contentFilename, HttpServletResponse response) { // Determine the content type. Default to the saved type, but guess if necessary. String contentType = file.getDigitalContentType(); if (contentType == null) { contentType = URLConnection.guessContentTypeFromName(contentFilename); } if (this.logger.isDebugEnabled()) { this.logger.debug("For the requested filename: " + contentFilename + ", setting the response header content type: " + contentType); } response.setContentType(contentType); response.setHeader("Content-disposition", "attachment; filename=" + file.getDigitalContentOriginalFilename()); } Date addDays(Date date, int days) { Calendar cal = Calendar.getInstance(); cal.setTime(date); cal.add(Calendar.DATE, days); //a negative number goes to a past date return cal.getTime(); } String createFilename(String originalFilename) { String contentFileExtension = originalFilename.substring(originalFilename.lastIndexOf(".") + 1); return String.format("%s.%s", RandomStringUtils.randomAlphanumeric(20), contentFileExtension); } String getThumbnailFilename(String awsFilename) { return awsFilename.substring(0, awsFilename.lastIndexOf(".")) + "-thumbnail.png"; } Map<String, Object> addSellerProfileAttributesToModel(Listing listing) { Map<String, Object> result = new HashMap<String, Object>(); // set seller attributes ConnectionRepository sellerConnectionRepository = this.usersConnectionRepository .createConnectionRepository(listing.getSeller().getUsername()); Connection sellerConnection = null; if (listing.getDoPostToTwitter()) { sellerConnection = sellerConnectionRepository.findPrimaryConnection(Twitter.class); } else { // must be Facebook sellerConnection = sellerConnectionRepository.findPrimaryConnection(Facebook.class); } result.put("sellerProfileImageUrl", sellerConnection.getImageUrl()); result.put("sellerProfileUrl", sellerConnection.getProfileUrl()); result.put("sellerFacebookPageUrl", listing.getSeller().getFacebookPageUrl()); if (StringUtils.isNotBlank(listing.getBackgroundImageUrl())) { result.put("backgroundImageUrl", listing.getBackgroundImageUrl()); } return result; } String identifyListingDetailReferer(String referrer) { String referrerProviderId = null; Boolean referredByTwitter = StringUtils.contains(referrer, "https://t.co"); if (referredByTwitter) { referrerProviderId = SocialProviders.TWITTER; } Boolean referredByFacebook = StringUtils.contains(referrer, "https://www.facebook.com"); if (referredByFacebook) { referrerProviderId = SocialProviders.FACEBOOK; } Boolean referredByPaypal = StringUtils.contains(referrer, "https://ic.paypal.com"); if (referredByPaypal) { referrerProviderId = "paypal"; } if (referrerProviderId == null) { referrerProviderId = "unknown"; } return referrerProviderId; } /** * Calculate the days, hours, minutes, and seconds before a listing expires. */ Map<String, Object> addTimeRemainingAttributes(Listing listing) { Map<String, Object> result = new HashMap<String, Object>(); Date now = new Date(); Date end = listing.getExpires(); long remainingMillis = end.getTime() - now.getTime(); long secondsPart = (remainingMillis / 1000) % 60; long minutesPart = (remainingMillis / (60 * 1000)) % 60; long hoursPart = (remainingMillis / (60 * 60 * 1000)) % 24; long daysPart = remainingMillis / (24 * 60 * 60 * 1000); result.put("remainingDays", (daysPart >= 0 ? daysPart : 0)); result.put("remainingHours", (hoursPart >= 0 ? hoursPart : 0)); result.put("remainingMinutes", (minutesPart >= 0 ? minutesPart : 0)); result.put("remainingSeconds", (secondsPart >= 0 ? secondsPart : 0)); return result; } ListingActionButton createListingActionButton(Listing listing, ListingDetailDTO dto) { Long currentUserId = this.userService.getCurrentUserId(); Boolean isUserLoggedIn = currentUserId != null; Boolean isUserLister = isUserLoggedIn && currentUserId.equals(listing.getSeller().getId()); // Setup the action button text and action ListingActionButton button = new ListingActionButton(); button.setListing(listing); button.setIsUserLoggedIn(isUserLoggedIn); button.setIsUserLister(isUserLister); button.setMessageSource(this.messageSource); button.setLocale(dto.getLocale()); // Add extended information for logged in users who are not the item's lister if (isUserLoggedIn && !isUserLister) { // has the user paid for this item? Payment payment = this.paymentService.findValidPaymentByListingIdAndBuyerId(listing.getId(), currentUserId); Boolean hasPaidForThisItem = payment != null; button.setHasUserPaidForThisItem(hasPaidForThisItem); // halt a user who has bought an item & then goes into checkout mode if (hasPaidForThisItem && dto.getIsCheckoutMode()) { throw new CheckoutAlreadyCompletedException(String.format( "User with id: %d has already purchased listing with id: %d, so halting checkout.", currentUserId, listing.getId()), payment.getId()); } else if (hasPaidForThisItem) { button.setPaymentId(payment.getId()); } else { // has not (yet) paid for this item // if this is a 'selling' listing, does the user have a "pending means of payment" or "failed payment" message? if (listing.getType().equals(ListingType.SELLING)) { Message pendingMeansOfPaymentMessage = this.messageService .findPendingMeansOfPaymentMessage(listing.getId(), currentUserId); button.setDoesUserHavePendingPurchase(pendingMeansOfPaymentMessage != null); } } } return button; } Boolean shouldStartCheckout(ListingDetailDTO dto, Listing listing, ListingActionButton button) { if (!dto.getIsCheckoutMode()) { return false; } if (!listing.isActive()) { return false; } if (button.getHasUserPaidForThisItem()) { return false; } if (listing.getInventories() != null && listing.getInventories().size() > 0) { return false; } // If result has not been set to false by the if logic, return true return true; } /** * Created a signed URL for a private cloudfront distribution. These URL's will be valid for the number * of minutes configured into the system. * * Method from: http://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/CFPrivateDistJavaDevelopment.html * * @return * @throws Exception */ String createSignedUrl(String contentFilename, String downloadFilename) { String result = null; try { // One time conversion of the DER file into a byte array. synchronized (this.derPrivateKey) { if (this.derPrivateKey.length == 0) { this.derPrivateKey = ServiceUtils .readInputStreamToBytes(new FileInputStream(this.cloudfrontPrivateKeyFilePath)); } } // Create an expiry date and set its time for a number of minutes from now Date expires = new Date(); expires.setTime(System.currentTimeMillis() + (this.cloudfrontSignedUrlValidMinutes * 60 * 1000)); // Generate a "canned" signed URL to allow access to a // specific distribution and object URIBuilder builder = URIBuilder .fromUri(String.format("https://%s/%s", this.cloudfrontDistributionDomain, contentFilename)); if (downloadFilename != null) { builder.queryParam("response-content-disposition", "attachment;filename=\"" + downloadFilename + "\""); builder.queryParam("response-content-type", URLConnection.guessContentTypeFromName(contentFilename)); } result = CloudFrontService.signUrlCanned(builder.build().toString(), // Resource URL or Path this.cloudfrontKeyPairId, // Certificate identifier, an active trusted signer for the distribution this.derPrivateKey, // DER Private key data expires // DateLessThan ); if (this.logger.isDebugEnabled()) { this.logger.debug(result); } /** * Alternate approach for signing the URL using a generated policy */ // Build a policy document to define custom restrictions for a signed URL. // String policy = CloudFrontService.buildPolicyForSignedUrl( // // Resource path (optional, may include '*' and '?' wildcards) // policyResourcePath, // // DateLessThan // ServiceUtils.parseIso8601Date("2014-11-14T22:20:00.000Z"), // // CIDR IP address restriction (optional, 0.0.0.0/0 means everyone) // "0.0.0.0/0", // // DateGreaterThan (optional) // ServiceUtils.parseIso8601Date("2011-10-16T06:31:56.000Z") // ); // // // Generate a signed URL using a custom policy document. // String signedUrl = CloudFrontService.signUrl( // // Resource URL or Path // "https://" + distributionDomain + "/" + s3ObjectKey, // // Certificate identifier, an active trusted signer for the distribution // keyPairId, // // DER Private key data // derPrivateKey, // // Access control policy // policy // ); // // if( this.logger.isDebugEnabled() ) { // this.logger.debug( signedUrl ); // } } catch (Exception e) { throw new RuntimeException("Exception trying to create signed cloudfront url.", e); } return result; } }