diff --git a/spring-social-facebook/src/main/java/org/springframework/social/facebook/api/FacebookErrors.java b/spring-social-facebook/src/main/java/org/springframework/social/facebook/api/FacebookErrors.java
index 3e69a2340..7736e4c86 100644
--- a/spring-social-facebook/src/main/java/org/springframework/social/facebook/api/FacebookErrors.java
+++ b/spring-social-facebook/src/main/java/org/springframework/social/facebook/api/FacebookErrors.java
@@ -194,7 +194,10 @@ public class FacebookErrors {
public static final int POKE_OUTSTANDING = 511;
public static final int POKE_RATE = 512;
public static final int POKE_USER_BLOCKED = 513;
-
+
+ // Rate limiting errors
+ public static final int USER_APP_TOO_MANY_CALLS = 613;
+
// Ref errors
public static final int REF_SET_FAILED = 700;
@@ -304,6 +307,9 @@ public class FacebookErrors {
public static final int TEST_ACCOUNTS_INVALID_ID = 2901;
public static final int TEST_ACCOUNTS_CANT_REMOVE_APP = 2902;
public static final int TEST_ACCOUNTS_CANT_DELETE = 2903;
+
+ // Ad Level Rate Limit error
+ public static final int AD_CREATION_LIMIT_EXCEEDED = 1487225;
public static boolean isGeneralError(int code) {
return code < 100;
diff --git a/spring-social-facebook/src/main/java/org/springframework/social/facebook/api/GraphApi.java b/spring-social-facebook/src/main/java/org/springframework/social/facebook/api/GraphApi.java
index 7f717849a..8a723d443 100644
--- a/spring-social-facebook/src/main/java/org/springframework/social/facebook/api/GraphApi.java
+++ b/spring-social-facebook/src/main/java/org/springframework/social/facebook/api/GraphApi.java
@@ -131,7 +131,16 @@ public interface GraphApi {
* @param data the data to publish to the connection.
*/
void post(String objectId, String connectionName, MultiValueMap data);
-
+
+ /**
+ * Updates data of an object.
+ * Requires appropriate permission to update to the object connection.
+ * @param objectId the object ID to update.
+ * @param data the data to update in the object.
+ * @return true if update was successful
+ */
+ boolean update(String objectId, MultiValueMap data);
+
/**
* Deletes an object.
* Requires appropriate permission to delete the object.
diff --git a/spring-social-facebook/src/main/java/org/springframework/social/facebook/api/InvalidParameterException.java b/spring-social-facebook/src/main/java/org/springframework/social/facebook/api/InvalidParameterException.java
new file mode 100644
index 000000000..86d3dc3a4
--- /dev/null
+++ b/spring-social-facebook/src/main/java/org/springframework/social/facebook/api/InvalidParameterException.java
@@ -0,0 +1,15 @@
+package org.springframework.social.facebook.api;
+
+import org.springframework.social.ApiException;
+
+/**
+ * Exception thrown when one of the supplied parameters is invalid.
+ *
+ * @author Sebastian Górecki
+ */
+public class InvalidParameterException extends ApiException {
+
+ public InvalidParameterException(String providerId, String message) {
+ super(providerId, message);
+ }
+}
diff --git a/spring-social-facebook/src/main/java/org/springframework/social/facebook/api/ads/AccountOperations.java b/spring-social-facebook/src/main/java/org/springframework/social/facebook/api/ads/AccountOperations.java
new file mode 100644
index 000000000..fb80e4379
--- /dev/null
+++ b/spring-social-facebook/src/main/java/org/springframework/social/facebook/api/ads/AccountOperations.java
@@ -0,0 +1,123 @@
+package org.springframework.social.facebook.api.ads;
+
+import org.springframework.social.ApiException;
+import org.springframework.social.InsufficientPermissionException;
+import org.springframework.social.MissingAuthorizationException;
+import org.springframework.social.facebook.api.PagedList;
+import org.springframework.social.facebook.api.ads.AdUser.AdUserRole;
+
+/**
+ * Defines operations for working with Facebook Marketing API Ad Account object.
+ *
+ * @author Sebastian Górecki
+ */
+public interface AccountOperations {
+
+ static final String[] AD_ACCOUNT_FIELDS = {
+ "id", "account_id", "account_status", "age", "amount_spent", "balance", "business_city", "business_country_code",
+ "business_name", "business_state", "business_street", "business_street2", "business_zip", "capabilities",
+ "created_time", "currency", "daily_spend_limit", "end_advertiser", "funding_source", "funding_source_details",
+ "is_personal", "media_agency", "name", "offsite_pixels_tos_accepted", "partner", "spend_cap", "timezone_id",
+ "timezone_name", "timezone_offset_hours_utc", "users", "tax_id_status"
+ };
+
+ static final String[] AD_ACCOUNT_INSIGHT_FIELDS = {
+ "account_id", "account_name", "date_start", "date_stop", "actions_per_impression", "clicks", "unique_clicks",
+ "cost_per_result", "cost_per_total_action", "cpc", "cost_per_unique_click", "cpm", "cpp", "ctr", "unique_ctr",
+ "frequency", "impressions", "unique_impressions", "objective", "reach", "result_rate", "results", "roas",
+ "social_clicks", "unique_social_clicks", "social_impressions", "unique_social_impressions", "social_reach",
+ "spend", "today_spend", "total_action_value", "total_actions", "total_unique_actions", "actions",
+ "unique_actions", "cost_per_action_type", "video_start_actions"
+ };
+
+ /**
+ * Get all ad accounts for given user.
+ *
+ * @param userId the id of an user
+ * @return the list of {@link AdAccount} objects
+ * @throws ApiException if there is an error while communicating with Facebook.
+ * @throws InsufficientPermissionException if the user has not granted "ads_read" or "ads_management" permission.
+ * @throws MissingAuthorizationException if FacebookAdsTemplate was not created with an access token.
+ */
+ PagedList getAdAccounts(String userId);
+
+ /**
+ * Get the ad account by given id.
+ *
+ * @param accountId the ID of the ad account (account_id)
+ * @return the {@link AdAccount} object
+ * @throws ApiException if there is an error while communicating with Facebook.
+ * @throws InsufficientPermissionException if the user has not granted "ads_read" or "ads_management" permission.
+ * @throws MissingAuthorizationException if FacebookAdsTemplate was not created with an access token.
+ */
+ AdAccount getAdAccount(String accountId);
+
+ /**
+ * Get all ad campaigns of an ad account.
+ *
+ * @param accountId the ID of the ad account (account_id)
+ * @return the list of {@link AdCampaign} objects
+ * @throws ApiException if there is an error while communicating with Facebook.
+ * @throws InsufficientPermissionException if the user has not granted "ads_read" or "ads_management" permission.
+ * @throws MissingAuthorizationException if FacebookAdsTemplate was not created with an access token.
+ */
+ PagedList getAdAccountCampaigns(String accountId);
+
+ /**
+ * Get all users of the ad account.
+ *
+ * @param accountId the ID of the ad account, the string act_{ad_account_id}
+ * @return the list of {@link AdUser} objects
+ * @throws ApiException if there is an error while communicating with Facebook.
+ * @throws InsufficientPermissionException if the user has not granted "ads_read" or "ads_management" permission.
+ * @throws MissingAuthorizationException if FacebookAdsTemplate was not created with an access token.
+ */
+ PagedList getAdAccountUsers(String accountId);
+
+ /**
+ * Add the user to the ad account.
+ *
+ * @param accountId the ID of the ad account (account_id)
+ * @param userId the id of an user (App Scoped User ID)
+ * @param role the role for the new user in ad account
+ * @throws ApiException if there is an error while communicating with Facebook.
+ * @throws InsufficientPermissionException if the user has not granted "ads_management" permission.
+ * @throws MissingAuthorizationException if FacebookAdsTemplate was not created with an access token.
+ */
+ void addUserToAdAccount(String accountId, String userId, AdUserRole role);
+
+ /**
+ * Remove user's access to an ad account.
+ *
+ * @param accountId the ID of the ad account (account_id)
+ * @param userId the id of an user (App Scoped User ID)
+ * @throws ApiException if there is an error while communicating with Facebook.
+ * @throws InsufficientPermissionException if the user has not granted "ads_management" permission.
+ * @throws MissingAuthorizationException if FacebookAdsTemplate was not created with an access token.
+ */
+ void deleteUserFromAdAccount(String accountId, String userId);
+
+ /**
+ * Get the insight for the ad account in aggregate.
+ *
+ * @param accountId the ID of the ad account (account_id)
+ * @return the {@link AdInsight} object
+ * @throws ApiException if there is an error while communicating with Facebook.
+ * @throws MissingAuthorizationException if FacebookAdsTemplate was not created with an access token.
+ * @throws InsufficientPermissionException if the user has not granted "ads_read" or "ads_management" permission.
+ */
+ AdInsight getAdAccountInsight(String accountId);
+
+ /**
+ * Updates the ad account with information given in adAccount object.
+ * Currently only updates on ad account name and spend_cap are supported.
+ *
+ * @param accountId the ID of the ad account (account_id)
+ * @param adAccount the ad account object containing updated account information
+ * @return true if the update succeeded
+ * @throws ApiException if there is an error while communicating with Facebook.
+ * @throws MissingAuthorizationException if FacebookAdsTemplate was not created with an access token.
+ * @throws InsufficientPermissionException if the user has not granted "ads_read" or "ads_management" permission.
+ */
+ boolean updateAdAccount(String accountId, AdAccount adAccount);
+}
diff --git a/spring-social-facebook/src/main/java/org/springframework/social/facebook/api/ads/Ad.java b/spring-social-facebook/src/main/java/org/springframework/social/facebook/api/ads/Ad.java
new file mode 100644
index 000000000..a171dc089
--- /dev/null
+++ b/spring-social-facebook/src/main/java/org/springframework/social/facebook/api/ads/Ad.java
@@ -0,0 +1,109 @@
+package org.springframework.social.facebook.api.ads;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+
+import java.util.Date;
+
+/**
+ * @author Sebastian Górecki
+ */
+public class Ad {
+ private String id;
+ private AdStatus status;
+ private String name;
+ private BidType bidType;
+ private BidInfo bidInfo;
+
+ private String accountId;
+ private String adSetId;
+ private String campaignId;
+
+ private String creativeId;
+ private Targeting targeting;
+
+ private Date createdTime;
+ private Date updatedTime;
+
+ public void setStatus(AdStatus status) {
+ this.status = status;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ public void setBidInfo(BidInfo bidInfo) {
+ this.bidInfo = bidInfo;
+ }
+
+ public void setAdSetId(String adSetId) {
+ this.adSetId = adSetId;
+ }
+
+ public void setCreativeId(String creativeId) {
+ this.creativeId = creativeId;
+ }
+
+ public String getId() {
+ return id;
+ }
+
+ public AdStatus getStatus() {
+ return status;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public BidType getBidType() {
+ return bidType;
+ }
+
+ public BidInfo getBidInfo() {
+ return bidInfo;
+ }
+
+ public String getAccountId() {
+ return accountId;
+ }
+
+ public String getAdSetId() {
+ return adSetId;
+ }
+
+ public String getCampaignId() {
+ return campaignId;
+ }
+
+ public String getCreativeId() {
+ return creativeId;
+ }
+
+ public Targeting getTargeting() {
+ return targeting;
+ }
+
+ public Date getCreatedTime() {
+ return createdTime;
+ }
+
+ public Date getUpdatedTime() {
+ return updatedTime;
+ }
+
+ public enum AdStatus {
+ ACTIVE, PAUSED, CAMPAIGN_PAUSED, CAMPAIGN_GROUP_PAUSED, CREDIT_CARD_NEEDED, DISABLED, DISAPPROVED, PENDING_REVIEW,
+ PREAPPROVED, PENDING_BILLING_INFO, ARCHIVED, DELETED, UNKNOWN;
+
+ @JsonCreator
+ public static AdStatus fromValue(String value) {
+ for (AdStatus status : AdStatus.values()) {
+ if (status.name().equals(value)) {
+ return status;
+ }
+ }
+ return UNKNOWN;
+ }
+ }
+}
diff --git a/spring-social-facebook/src/main/java/org/springframework/social/facebook/api/ads/AdAccount.java b/spring-social-facebook/src/main/java/org/springframework/social/facebook/api/ads/AdAccount.java
new file mode 100644
index 000000000..d67517c50
--- /dev/null
+++ b/spring-social-facebook/src/main/java/org/springframework/social/facebook/api/ads/AdAccount.java
@@ -0,0 +1,330 @@
+package org.springframework.social.facebook.api.ads;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonGetter;
+import org.springframework.social.facebook.api.FacebookObject;
+
+import java.util.Date;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Model class representing an ad account.
+ *
+ * @author Sebastian Górecki
+ */
+public class AdAccount extends FacebookObject {
+ private String id;
+ private List accountGroups;
+ private long accountId;
+ private AccountStatus status;
+ private int age;
+ private AgencyClientDeclaration agencyClientDeclaration;
+ private String amountSpent;
+ private String balance;
+ private String businessCity;
+ private String businessCountryCode;
+ private String businessName;
+ private String businessState;
+ private String businessStreet;
+ private String businessStreet2;
+ private String businessZip;
+ private List capabilities;
+ private Date createdTime;
+ private String currency;
+ private String dailySpendLimit;
+ private long endAdvertiser;
+ private String fundingSource;
+ private Map fundingSourceDetails;
+ private int isPersonal;
+ private long mediaAgency;
+ private String name;
+ private boolean offsitePixelsTOSAccepted;
+ private long partner;
+ private String spendCap;
+ private int timezoneId;
+ private String timezoneName;
+ private int timezoneOffsetHoursUTC;
+ private Map tosAccepted;
+ private List users;
+ private TaxStatus taxStatus;
+
+ public String getId() {
+ return id;
+ }
+
+ public List getAccountGroups() {
+ return accountGroups;
+ }
+
+ public long getAccountId() {
+ return accountId;
+ }
+
+ public AccountStatus getStatus() {
+ return status;
+ }
+
+ public int getAge() {
+ return age;
+ }
+
+ public AgencyClientDeclaration getAgencyClientDeclaration() {
+ return agencyClientDeclaration;
+ }
+
+ public String getAmountSpent() {
+ return amountSpent;
+ }
+
+ public String getBalance() {
+ return balance;
+ }
+
+ public String getBusinessCity() {
+ return businessCity;
+ }
+
+ public String getBusinessCountryCode() {
+ return businessCountryCode;
+ }
+
+ public String getBusinessName() {
+ return businessName;
+ }
+
+ public String getBusinessState() {
+ return businessState;
+ }
+
+ public String getBusinessStreet() {
+ return businessStreet;
+ }
+
+ public String getBusinessStreet2() {
+ return businessStreet2;
+ }
+
+ public String getBusinessZip() {
+ return businessZip;
+ }
+
+ public List getCapabilities() {
+ return capabilities;
+ }
+
+ public Date getCreatedTime() {
+ return createdTime;
+ }
+
+ public String getCurrency() {
+ return currency;
+ }
+
+ public String getDailySpendLimit() {
+ return dailySpendLimit;
+ }
+
+ public long getEndAdvertiser() {
+ return endAdvertiser;
+ }
+
+ public String getFundingSource() {
+ return fundingSource;
+ }
+
+ public Map getFundingSourceDetails() {
+ return fundingSourceDetails;
+ }
+
+ public int getIsPersonal() {
+ return isPersonal;
+ }
+
+ public long getMediaAgency() {
+ return mediaAgency;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ public boolean isOffsitePixelsTOSAccepted() {
+ return offsitePixelsTOSAccepted;
+ }
+
+ public long getPartner() {
+ return partner;
+ }
+
+ public String getSpendCap() {
+ return spendCap;
+ }
+
+ public void setSpendCap(String spendCap) {
+ this.spendCap = spendCap;
+ }
+
+ public int getTimezoneId() {
+ return timezoneId;
+ }
+
+ public String getTimezoneName() {
+ return timezoneName;
+ }
+
+ public int getTimezoneOffsetHoursUTC() {
+ return timezoneOffsetHoursUTC;
+ }
+
+ public Map getTosAccepted() {
+ return tosAccepted;
+ }
+
+ public List getUsers() {
+ return users;
+ }
+
+ public TaxStatus getTaxStatus() {
+ return taxStatus;
+ }
+
+ public enum AccountStatus {
+ ACTIVE(1), DISABLED(2), UNSETTLED(3), PENDING_REVIEW(7), IN_GRACE_PERIOD(9), TEMPORARILY_UNAVAILABLE(101),
+ PENDING_CLOSURE(100), UNKNOWN(0);
+
+ private final int value;
+
+ AccountStatus(int value) {
+ this.value = value;
+ }
+
+ @JsonCreator
+ public static AccountStatus fromValue(int value) {
+ for (AccountStatus status : AccountStatus.values()) {
+ if (status.getValue() == value) {
+ return status;
+ }
+ }
+ return UNKNOWN;
+ }
+
+ @JsonGetter
+ public int getValue() {
+ return value;
+ }
+ }
+
+ public enum Capability {
+ BULK_ACCOUNT, CAN_CREATE_LOOKALIKES_WITH_CUSTOM_RATIO, CAN_USE_CONVERSION_LOOKALIKES, CAN_USE_MOBILE_EXTERNAL_PAGE_TYPE_FOR_LPP,
+ CAN_USE_REACH_AND_FREQUENCY, CUSTOM_CLUSTER_SHARING, DIRECT_SALES, HAS_AD_SET_TARGETING, HAS_AVAILABLE_PAYMENT_METHODS,
+ HOLDOUT_VIEW_TAGS, MOBILE_ADVERTISER_ID_UPLOAD, MOBILE_APP_REENGAGEMENT_ADS, MOBILE_APP_VIDEO_ADS,
+ NEKO_DESKTOP_CANVAS_APP_ADS, NEW_CAMPAIGN_STRUCTURE, PREMIUM, VIEW_TAGS, PRORATED_BUDGET, OFFSITE_CONVERSION_HIGH_BID,
+ CAN_USE_MOBILE_EXTERNAL_PAGE_TYPE, CAN_USE_OLD_AD_TYPES, CAN_USE_VIDEO_METRICS_BREAKDOWN, ADS_CF_INSTORE_DAILY_BUDGET,
+ AD_SET_PROMOTED_OBJECT_APP, AD_SET_PROMOTED_OBJECT_OFFER, AD_SET_PROMOTED_OBJECT_PAGE, AD_SET_PROMOTED_OBJECT_PIXEL,
+ CONNECTIONS_UI_V2, LOOKALIKE_AUDIENCE, CUSTOM_AUDIENCES_OPT_OUT_LINK, CUSTOM_AUDIENCES_FOLDERS, UNKNOWN;
+
+ @JsonCreator
+ public static Capability fromValue(String value) {
+ for (Capability capability : Capability.values()) {
+ if (capability.name().equals(value)) {
+ return capability;
+ }
+ }
+ return UNKNOWN;
+ }
+ }
+
+ public enum TaxStatus {
+ VAT_NOT_REQUIRED_US_CA(1), VAT_INFORMATION_REQUIRED(2), VAT_INFORMATION_SUBMITTED(3), OFFLINE_VAT_VALIDATION_FAILED(4),
+ ACCOUNT_IS_PERSONAL_ACCOUNT(5), UNKNOWN(0);
+
+ private final int value;
+
+ TaxStatus(int value) {
+ this.value = value;
+ }
+
+ @JsonCreator
+ public static TaxStatus fromValue(int value) {
+ for (TaxStatus status : TaxStatus.values()) {
+ if (status.getValue() == value) {
+ return status;
+ }
+ }
+ return UNKNOWN;
+ }
+
+ @JsonGetter
+ public int getValue() {
+ return value;
+ }
+ }
+
+ public class AgencyClientDeclaration {
+ private int agencyRepresentingClient;
+ private int clientBasedInFrance;
+ private String clientCity;
+ private String clientCountryCode;
+ private String clientEmailAddress;
+ private String clientName;
+ private String clientPostalCode;
+ private String clientProvince;
+ private String clientStreet;
+ private String clientStreet2;
+ private int hasWrittenMandateFromAdvertiser;
+ private int isClientPayingInvoices;
+
+ public int getAgencyRepresentingClient() {
+ return agencyRepresentingClient;
+ }
+
+ public int getClientBasedInFrance() {
+ return clientBasedInFrance;
+ }
+
+ public String getClientCity() {
+ return clientCity;
+ }
+
+ public String getClientCountryCode() {
+ return clientCountryCode;
+ }
+
+ public String getClientEmailAddress() {
+ return clientEmailAddress;
+ }
+
+ public String getClientName() {
+ return clientName;
+ }
+
+ public String getClientPostalCode() {
+ return clientPostalCode;
+ }
+
+ public String getClientProvince() {
+ return clientProvince;
+ }
+
+ public String getClientStreet() {
+ return clientStreet;
+ }
+
+ public String getClientStreet2() {
+ return clientStreet2;
+ }
+
+ public int getHasWrittenMandateFromAdvertiser() {
+ return hasWrittenMandateFromAdvertiser;
+ }
+
+ public int getIsClientPayingInvoices() {
+ return isClientPayingInvoices;
+ }
+ }
+}
diff --git a/spring-social-facebook/src/main/java/org/springframework/social/facebook/api/ads/AdAccountGroup.java b/spring-social-facebook/src/main/java/org/springframework/social/facebook/api/ads/AdAccountGroup.java
new file mode 100644
index 000000000..6b958f935
--- /dev/null
+++ b/spring-social-facebook/src/main/java/org/springframework/social/facebook/api/ads/AdAccountGroup.java
@@ -0,0 +1,26 @@
+package org.springframework.social.facebook.api.ads;
+
+import org.springframework.social.facebook.api.FacebookObject;
+
+/**
+ * Model class representing an ad account group.
+ *
+ * @author Sebastian Górecki
+ */
+public class AdAccountGroup extends FacebookObject {
+ private String id;
+ private String name;
+ private int status;
+
+ public String getId() {
+ return id;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public int getStatus() {
+ return status;
+ }
+}
diff --git a/spring-social-facebook/src/main/java/org/springframework/social/facebook/api/ads/AdCampaign.java b/spring-social-facebook/src/main/java/org/springframework/social/facebook/api/ads/AdCampaign.java
new file mode 100644
index 000000000..4f2eef78c
--- /dev/null
+++ b/spring-social-facebook/src/main/java/org/springframework/social/facebook/api/ads/AdCampaign.java
@@ -0,0 +1,111 @@
+package org.springframework.social.facebook.api.ads;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import org.springframework.social.facebook.api.FacebookObject;
+
+/**
+ * Model class representing an ad campaign.
+ *
+ * @author Sebastian Górecki
+ */
+public class AdCampaign extends FacebookObject {
+ private String id;
+ private String accountId;
+ private BuyingType buyingType;
+ private CampaignStatus status;
+ private String name;
+ private CampaignObjective objective;
+ private int spendCap;
+
+ public CampaignStatus getStatus() {
+ return status;
+ }
+
+ public void setStatus(CampaignStatus status) {
+ this.status = status;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ public String getId() {
+ return id;
+ }
+
+ public String getAccountId() {
+ return accountId;
+ }
+
+ public BuyingType getBuyingType() {
+ return buyingType;
+ }
+
+ public void setBuyingType(BuyingType buyingType) {
+ this.buyingType = buyingType;
+ }
+
+ public CampaignObjective getObjective() {
+ return objective;
+ }
+
+ public void setObjective(CampaignObjective objective) {
+ this.objective = objective;
+ }
+
+ public int getSpendCap() {
+ return spendCap;
+ }
+
+ public void setSpendCap(int spendCap) {
+ this.spendCap = spendCap;
+ }
+
+ public enum BuyingType {
+ AUCTION, FIXED_CPM, RESERVED, MIXED, UNKNOWN;
+
+ @JsonCreator
+ public static BuyingType fromValue(String value) {
+ for (BuyingType type : BuyingType.values()) {
+ if (type.name().equals(value)) {
+ return type;
+ }
+ }
+ return UNKNOWN;
+ }
+ }
+
+ public enum CampaignStatus {
+ ACTIVE, PAUSED, ARCHIVED, DELETED, UNKNOWN;
+
+ @JsonCreator
+ public static CampaignStatus fromValue(String value) {
+ for (CampaignStatus status : CampaignStatus.values()) {
+ if (status.name().equals(value)) {
+ return status;
+ }
+ }
+ return UNKNOWN;
+ }
+ }
+
+ public enum CampaignObjective {
+ CANVAS_APP_ENGAGEMENT, CANVAS_APP_INSTALLS, EVENT_RESPONSES, LOCAL_AWARENESS, MOBILE_APP_ENGAGEMENT,
+ MOBILE_APP_INSTALLS, NONE, OFFER_CLAIMS, PAGE_LIKES, POST_ENGAGEMENT, VIDEO_VIEWS, WEBSITE_CLICKS,
+ WEBSITE_CONVERSIONS, UNKNOWN;
+
+ @JsonCreator
+ public static CampaignObjective fromValue(String value) {
+ for (CampaignObjective objective : CampaignObjective.values()) {
+ if (objective.name().equals(value)) {
+ return objective;
+ }
+ }
+ return UNKNOWN;
+ }
+ }
+}
diff --git a/spring-social-facebook/src/main/java/org/springframework/social/facebook/api/ads/AdCreative.java b/spring-social-facebook/src/main/java/org/springframework/social/facebook/api/ads/AdCreative.java
new file mode 100644
index 000000000..759691904
--- /dev/null
+++ b/spring-social-facebook/src/main/java/org/springframework/social/facebook/api/ads/AdCreative.java
@@ -0,0 +1,161 @@
+package org.springframework.social.facebook.api.ads;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+
+/**
+ * @author Sebastian Górecki
+ */
+public class AdCreative {
+ private String id;
+ private AdCreativeType type;
+ private String name;
+ private String title;
+ private AdCreativeStatus status;
+
+ private String body;
+ private String objectId;
+ private String imageHash;
+ private String imageUrl;
+ private String linkUrl;
+ private String objectStoryId;
+ private String objectUrl;
+ private String urlTags;
+ private String thumbnailUrl;
+
+ public AdCreativeType getType() {
+ return type;
+ }
+
+ public void setType(AdCreativeType type) {
+ this.type = type;
+ }
+
+ public String getThumbnailUrl() {
+ return thumbnailUrl;
+ }
+
+ public void setThumbnailUrl(String thumbnailUrl) {
+ this.thumbnailUrl = thumbnailUrl;
+ }
+
+ public String getId() {
+ return id;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ public String getTitle() {
+ return title;
+ }
+
+ public void setTitle(String title) {
+ this.title = title;
+ }
+
+ public AdCreativeStatus getStatus() {
+ return status;
+ }
+
+ public void setStatus(AdCreativeStatus status) {
+ this.status = status;
+ }
+
+ public String getBody() {
+ return body;
+ }
+
+ public void setBody(String body) {
+ this.body = body;
+ }
+
+ public String getObjectId() {
+ return objectId;
+ }
+
+ public void setObjectId(String objectId) {
+ this.objectId = objectId;
+ }
+
+ public String getImageHash() {
+ return imageHash;
+ }
+
+ public void setImageHash(String imageHash) {
+ this.imageHash = imageHash;
+ }
+
+ public String getImageUrl() {
+ return imageUrl;
+ }
+
+ public void setImageUrl(String imageUrl) {
+ this.imageUrl = imageUrl;
+ }
+
+ public String getLinkUrl() {
+ return linkUrl;
+ }
+
+ public void setLinkUrl(String linkUrl) {
+ this.linkUrl = linkUrl;
+ }
+
+ public String getObjectStoryId() {
+ return objectStoryId;
+ }
+
+ public void setObjectStoryId(String objectStoryId) {
+ this.objectStoryId = objectStoryId;
+ }
+
+ public String getObjectUrl() {
+ return objectUrl;
+ }
+
+ public void setObjectUrl(String objectUrl) {
+ this.objectUrl = objectUrl;
+ }
+
+ public String getUrlTags() {
+ return urlTags;
+ }
+
+ public void setUrlTags(String urlTags) {
+ this.urlTags = urlTags;
+ }
+
+ public enum AdCreativeType {
+ PAGE, DOMAIN, EVENT, STORE_ITEM, OFFER, SHARE, PHOTO, STATUS, VIDEO, APPLICATION, INVALID, UNKNOWN;
+
+ @JsonCreator
+ public static AdCreativeType fromValue(String value) {
+ for (AdCreativeType type : AdCreativeType.values()) {
+ if (type.name().equals(value)) {
+ return type;
+ }
+ }
+ return UNKNOWN;
+ }
+ }
+
+ public enum AdCreativeStatus {
+ PENDING, ACTIVE, PAUSED, DELETED, PENDING_REVIEW, DISAPPROVED, PREAPPROVED, PENDING_BILLING_INFO,
+ CAMPAIGN_PAUSED, ADGROUP_PAUSED, CAMPAIGN_GROUP_PAUSED, ARCHIVED, UNKNOWN;
+
+ @JsonCreator
+ public static AdCreativeStatus fromValue(String value) {
+ for (AdCreativeStatus status : AdCreativeStatus.values()) {
+ if (status.name().equals(value)) {
+ return status;
+ }
+ }
+ return UNKNOWN;
+ }
+ }
+}
diff --git a/spring-social-facebook/src/main/java/org/springframework/social/facebook/api/ads/AdInsight.java b/spring-social-facebook/src/main/java/org/springframework/social/facebook/api/ads/AdInsight.java
new file mode 100644
index 000000000..8828a3f66
--- /dev/null
+++ b/spring-social-facebook/src/main/java/org/springframework/social/facebook/api/ads/AdInsight.java
@@ -0,0 +1,252 @@
+package org.springframework.social.facebook.api.ads;
+
+import java.util.Date;
+import java.util.List;
+
+/**
+ * Class representing response object given by Ad Insights API for request
+ * about AdAccount, AdCampaign, AdSet or Ad.
+ *
+ * @author Sebastian Górecki
+ */
+public class AdInsight {
+ // id fields
+ private String accountId;
+ private String adGroupId;
+ private String campaignId;
+ private String campaignGroupId;
+
+ // name fields
+ private String accountName;
+ private String adGroupName;
+ private String campaignGroupName;
+ private String camapignName;
+
+ // date fields
+ private Date dateStart;
+ private Date dateStop;
+ private Date campaignStart;
+ private Date campaignEnd;
+ private Date campaignGroupEnd;
+
+ // general fields
+ private double actionsPerImpression;
+ private int clicks;
+ private int uniqueClicks;
+ private double costPerResult;
+ private double costPerTotalAction;
+ private double costPerClick;
+ private double costPerUniqueClick;
+ private double cpm;
+ private double cpp;
+ private double ctr;
+ private double uniqueCtr;
+ private double frequency;
+ private int impressions;
+ private int uniqueImpressions;
+ private String objective;
+ private int reach;
+ private double resultRate;
+ private int results;
+ private int roas;
+ private int socialClicks;
+ private int uniqueSocialClicks;
+ private int socialImpressions;
+ private int uniqueSocialImpressions;
+ private int socialReach;
+ private int spend;
+ private int todaySpend;
+ private int totalActionValue;
+ private int totalActions;
+ private int totalUniqueActions;
+
+ // action and video fields
+ private List actions;
+ private List uniqueActions;
+ private List costPerActionType;
+ private List videoStartActions;
+
+ public String getAccountId() {
+ return accountId;
+ }
+
+ public String getAdGroupId() {
+ return adGroupId;
+ }
+
+ public String getCampaignId() {
+ return campaignId;
+ }
+
+ public String getCampaignGroupId() {
+ return campaignGroupId;
+ }
+
+ public String getAccountName() {
+ return accountName;
+ }
+
+ public String getAdGroupName() {
+ return adGroupName;
+ }
+
+ public String getCampaignGroupName() {
+ return campaignGroupName;
+ }
+
+ public String getCamapignName() {
+ return camapignName;
+ }
+
+ public Date getDateStart() {
+ return dateStart;
+ }
+
+ public Date getDateStop() {
+ return dateStop;
+ }
+
+ public Date getCampaignStart() {
+ return campaignStart;
+ }
+
+ public Date getCampaignEnd() {
+ return campaignEnd;
+ }
+
+ public Date getCampaignGroupEnd() {
+ return campaignGroupEnd;
+ }
+
+ public double getActionsPerImpression() {
+ return actionsPerImpression;
+ }
+
+ public int getClicks() {
+ return clicks;
+ }
+
+ public int getUniqueClicks() {
+ return uniqueClicks;
+ }
+
+ public double getCostPerResult() {
+ return costPerResult;
+ }
+
+ public double getCostPerTotalAction() {
+ return costPerTotalAction;
+ }
+
+ public double getCostPerClick() {
+ return costPerClick;
+ }
+
+ public double getCostPerUniqueClick() {
+ return costPerUniqueClick;
+ }
+
+ public double getCpm() {
+ return cpm;
+ }
+
+ public double getCpp() {
+ return cpp;
+ }
+
+ public double getCtr() {
+ return ctr;
+ }
+
+ public double getUniqueCtr() {
+ return uniqueCtr;
+ }
+
+ public double getFrequency() {
+ return frequency;
+ }
+
+ public int getImpressions() {
+ return impressions;
+ }
+
+ public int getUniqueImpressions() {
+ return uniqueImpressions;
+ }
+
+ public String getObjective() {
+ return objective;
+ }
+
+ public int getReach() {
+ return reach;
+ }
+
+ public double getResultRate() {
+ return resultRate;
+ }
+
+ public int getResults() {
+ return results;
+ }
+
+ public int getRoas() {
+ return roas;
+ }
+
+ public int getSocialClicks() {
+ return socialClicks;
+ }
+
+ public int getUniqueSocialClicks() {
+ return uniqueSocialClicks;
+ }
+
+ public int getSocialImpressions() {
+ return socialImpressions;
+ }
+
+ public int getUniqueSocialImpressions() {
+ return uniqueSocialImpressions;
+ }
+
+ public int getSocialReach() {
+ return socialReach;
+ }
+
+ public int getSpend() {
+ return spend;
+ }
+
+ public int getTodaySpend() {
+ return todaySpend;
+ }
+
+ public int getTotalActionValue() {
+ return totalActionValue;
+ }
+
+ public int getTotalActions() {
+ return totalActions;
+ }
+
+ public int getTotalUniqueActions() {
+ return totalUniqueActions;
+ }
+
+ public List getActions() {
+ return actions;
+ }
+
+ public List getUniqueActions() {
+ return uniqueActions;
+ }
+
+ public List getCostPerActionType() {
+ return costPerActionType;
+ }
+
+ public List getVideoStartActions() {
+ return videoStartActions;
+ }
+}
diff --git a/spring-social-facebook/src/main/java/org/springframework/social/facebook/api/ads/AdInsightAction.java b/spring-social-facebook/src/main/java/org/springframework/social/facebook/api/ads/AdInsightAction.java
new file mode 100644
index 000000000..ce2908799
--- /dev/null
+++ b/spring-social-facebook/src/main/java/org/springframework/social/facebook/api/ads/AdInsightAction.java
@@ -0,0 +1,19 @@
+package org.springframework.social.facebook.api.ads;
+
+/**
+ * Model class representing an ad insight action.
+ *
+ * @author Sebastian Górecki
+ */
+public class AdInsightAction {
+ private String actionType;
+ private double value;
+
+ public String getActionType() {
+ return actionType;
+ }
+
+ public double getValue() {
+ return value;
+ }
+}
diff --git a/spring-social-facebook/src/main/java/org/springframework/social/facebook/api/ads/AdOperations.java b/spring-social-facebook/src/main/java/org/springframework/social/facebook/api/ads/AdOperations.java
new file mode 100644
index 000000000..6b30ef701
--- /dev/null
+++ b/spring-social-facebook/src/main/java/org/springframework/social/facebook/api/ads/AdOperations.java
@@ -0,0 +1,111 @@
+package org.springframework.social.facebook.api.ads;
+
+import org.springframework.social.ApiException;
+import org.springframework.social.InsufficientPermissionException;
+import org.springframework.social.MissingAuthorizationException;
+import org.springframework.social.facebook.api.PagedList;
+
+/**
+ * @author Sebastian Górecki
+ */
+public interface AdOperations {
+ static final String[] AD_FIELDS = {
+ "id", "account_id", "adgroup_status", "bid_type", "bid_info", "campaign_id", "campaign_group_id", "created_time",
+ "creative", "name", "targeting", "updated_time"
+ };
+
+ static final String[] AD_INSIGHT_FIELDS = {
+ "account_id", "account_name", "date_start", "date_stop", "actions_per_impression", "clicks", "unique_clicks",
+ "cost_per_result", "cost_per_total_action", "cpc", "cost_per_unique_click", "cpm", "cpp", "ctr", "unique_ctr",
+ "frequency", "impressions", "unique_impressions", "objective", "reach", "result_rate", "results", "roas",
+ "social_clicks", "unique_social_clicks", "social_impressions", "unique_social_impressions", "social_reach",
+ "spend", "today_spend", "total_action_value", "total_actions", "total_unique_actions", "actions",
+ "unique_actions", "cost_per_action_type", "video_start_actions"
+ };
+
+ /**
+ * Get all ads from one ad account.
+ *
+ * @param accountId the ID of the ad account
+ * @return the list of the {@link Ad} objects.
+ * @throws ApiException if there is an error while communicating with Facebook.
+ * @throws InsufficientPermissionException if the user has not granted "ads_read" or "ads_management" permission.
+ * @throws MissingAuthorizationException if FacebookAdsTemplate was not created with an access token.
+ */
+ PagedList getAccountAds(String accountId);
+
+ /**
+ * Get all ads from one ad campaign.
+ *
+ * @param campaignId the id of the ad campaign.
+ * @return the list of the {@link Ad} objects.
+ * @throws ApiException if there is an error while communicating with Facebook.
+ * @throws InsufficientPermissionException if the user has not granted "ads_read" or "ads_management" permission.
+ * @throws MissingAuthorizationException if FacebookAdsTemplate was not created with an access token.
+ */
+ PagedList getCampaignAds(String campaignId);
+
+ /**
+ * Get all ads from one ad set.
+ *
+ * @param adSetId the of of the ad set.
+ * @return the list of the {@link Ad} objects.
+ * @throws ApiException if there is an error while communicating with Facebook.
+ * @throws InsufficientPermissionException if the user has not granted "ads_read" or "ads_management" permission.
+ * @throws MissingAuthorizationException if FacebookAdsTemplate was not created with an access token.
+ */
+ PagedList getAdSetAds(String adSetId);
+
+ /**
+ * Get details about an ad.
+ *
+ * @param adId the id of an ad.
+ * @return the {@link Ad} object.
+ * @throws ApiException if there is an error while communicating with Facebook.
+ * @throws InsufficientPermissionException if the user has not granted "ads_read" or "ads_management" permission.
+ * @throws MissingAuthorizationException if FacebookAdsTemplate was not created with an access token.
+ */
+ Ad getAd(String adId);
+
+ /**
+ * Get the insights from ad object.
+ *
+ * @param adId the id of an ad.
+ * @return the {@link AdInsight} object.
+ * @throws ApiException if there is an error while communicating with Facebook.
+ * @throws InsufficientPermissionException if the user has not granted "ads_read" or "ads_management" permission.
+ * @throws MissingAuthorizationException if FacebookAdsTemplate was not created with an access token.
+ */
+ AdInsight getAdInsight(String adId);
+
+ /**
+ * Synchronously creates one ad.
+ *
+ * @param accountId the ID of the ad account
+ * @param ad the {@link Ad} object.
+ * @return the id of created ad.
+ * @throws ApiException if there is an error while communicating with Facebook.
+ * @throws InsufficientPermissionException if the user has not granted "ads_read" or "ads_management" permission.
+ * @throws MissingAuthorizationException if FacebookAdsTemplate was not created with an access token.
+ */
+ String createAd(String accountId, Ad ad);
+
+ /**
+ * Updates certain fields in an ad.
+ *
+ * @param adId the if of an ad.
+ * @param ad the {@link Ad} object.
+ * @return true if successful.
+ */
+ boolean updateAd(String adId, Ad ad);
+
+ /**
+ * Deletes an ad object.
+ *
+ * @param adId the if of an ad.
+ * @throws ApiException if there is an error while communicating with Facebook.
+ * @throws InsufficientPermissionException if the user has not granted "ads_read" or "ads_management" permission.
+ * @throws MissingAuthorizationException if FacebookAdsTemplate was not created with an access token.
+ */
+ void deleteAd(String adId);
+}
diff --git a/spring-social-facebook/src/main/java/org/springframework/social/facebook/api/ads/AdSet.java b/spring-social-facebook/src/main/java/org/springframework/social/facebook/api/ads/AdSet.java
new file mode 100644
index 000000000..bb046bb17
--- /dev/null
+++ b/spring-social-facebook/src/main/java/org/springframework/social/facebook/api/ads/AdSet.java
@@ -0,0 +1,171 @@
+package org.springframework.social.facebook.api.ads;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import org.springframework.social.facebook.api.FacebookObject;
+
+import java.util.Date;
+import java.util.List;
+
+/**
+ * Model class representing an ad set.
+ *
+ * @author Sebastian Górecki
+ */
+public class AdSet extends FacebookObject {
+ private String id;
+ private String accountId;
+ private String campaignId;
+ private String name;
+ private AdSetStatus status;
+
+ private boolean autobid;
+ private BidInfo bidInfo;
+ private BidType bidType;
+
+ private int budgetRemaining;
+ private int dailyBudget;
+ private int lifetimeBudget;
+
+ private List creativeSequence;
+ private PromotedObject promotedObject;
+ private Targeting targeting;
+
+ private Date startTime;
+ private Date endTime;
+ private Date createdTime;
+ private Date updatedTime;
+
+ public String getId() {
+ return id;
+ }
+
+ public String getAccountId() {
+ return accountId;
+ }
+
+ public String getCampaignId() {
+ return campaignId;
+ }
+
+ public void setCampaignId(String campaignId) {
+ this.campaignId = campaignId;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ public AdSetStatus getStatus() {
+ return status;
+ }
+
+ public void setStatus(AdSetStatus status) {
+ this.status = status;
+ }
+
+ public boolean isAutobid() {
+ return autobid;
+ }
+
+ public void setAutobid(boolean autobid) {
+ this.autobid = autobid;
+ }
+
+ public BidInfo getBidInfo() {
+ return bidInfo;
+ }
+
+ public void setBidInfo(BidInfo bidInfo) {
+ this.bidInfo = bidInfo;
+ }
+
+ public BidType getBidType() {
+ return bidType;
+ }
+
+ public void setBidType(BidType bidType) {
+ this.bidType = bidType;
+ }
+
+ public int getBudgetRemaining() {
+ return budgetRemaining;
+ }
+
+ public int getDailyBudget() {
+ return dailyBudget;
+ }
+
+ public void setDailyBudget(int dailyBudget) {
+ this.dailyBudget = dailyBudget;
+ }
+
+ public int getLifetimeBudget() {
+ return lifetimeBudget;
+ }
+
+ public void setLifetimeBudget(int lifetimeBudget) {
+ this.lifetimeBudget = lifetimeBudget;
+ }
+
+ public List getCreativeSequence() {
+ return creativeSequence;
+ }
+
+ public PromotedObject getPromotedObject() {
+ return promotedObject;
+ }
+
+ public void setPromotedObject(PromotedObject promotedObject) {
+ this.promotedObject = promotedObject;
+ }
+
+ public Targeting getTargeting() {
+ return targeting;
+ }
+
+ public void setTargeting(Targeting targeting) {
+ this.targeting = targeting;
+ }
+
+ public Date getStartTime() {
+ return startTime;
+ }
+
+ public void setStartTime(Date startTime) {
+ this.startTime = startTime;
+ }
+
+ public Date getEndTime() {
+ return endTime;
+ }
+
+ public void setEndTime(Date endTime) {
+ this.endTime = endTime;
+ }
+
+ public Date getCreatedTime() {
+ return createdTime;
+ }
+
+ public Date getUpdatedTime() {
+ return updatedTime;
+ }
+
+ public enum AdSetStatus {
+ ACTIVE, PAUSED, ARCHIVED, DELETED, CAMPAIGN_GROUP_PAUSED, UNKNOWN;
+
+ @JsonCreator
+ public static AdSetStatus fromValue(String value) {
+ for (AdSetStatus status : AdSetStatus.values()) {
+ if (status.name().equals(value)) {
+ return status;
+ }
+ }
+ return UNKNOWN;
+ }
+ }
+}
diff --git a/spring-social-facebook/src/main/java/org/springframework/social/facebook/api/ads/AdSetOperations.java b/spring-social-facebook/src/main/java/org/springframework/social/facebook/api/ads/AdSetOperations.java
new file mode 100644
index 000000000..c72d00894
--- /dev/null
+++ b/spring-social-facebook/src/main/java/org/springframework/social/facebook/api/ads/AdSetOperations.java
@@ -0,0 +1,106 @@
+package org.springframework.social.facebook.api.ads;
+
+import org.springframework.social.ApiException;
+import org.springframework.social.InsufficientPermissionException;
+import org.springframework.social.MissingAuthorizationException;
+import org.springframework.social.facebook.api.PagedList;
+
+/**
+ * Defines operations for working with Facebook Ad set object.
+ *
+ * @author Sebastian Górecki
+ */
+public interface AdSetOperations {
+ static final String[] AD_SET_FIELDS = {
+ "account_id", "bid_info", "bid_type", "budget_remaining", "campaign_group_id", "campaign_status", "created_time",
+ "creative_sequence", "daily_budget", "end_time", "id", "is_autobid", "lifetime_budget", "name", "promoted_object",
+ "start_time", "targeting", "updated_time"
+ };
+
+ static final String[] AD_SET_INSIGHT_FIELDS = {
+ "account_id", "account_name", "date_start", "date_stop", "actions_per_impression", "clicks", "unique_clicks",
+ "cost_per_result", "cost_per_total_action", "cpc", "cost_per_unique_click", "cpm", "cpp", "ctr", "unique_ctr",
+ "frequency", "impressions", "unique_impressions", "objective", "reach", "result_rate", "results", "roas",
+ "social_clicks", "unique_social_clicks", "social_impressions", "unique_social_impressions", "social_reach",
+ "spend", "today_spend", "total_action_value", "total_actions", "total_unique_actions", "actions",
+ "unique_actions", "cost_per_action_type", "video_start_actions"
+ };
+
+ /**
+ * Gets all ad sets from ad account given by account id.
+ *
+ * @param accountId the ID of an ad account
+ * @return the list of {@link AdSet} objects
+ * @throws ApiException if there is an error while communicating with Facebook.
+ * @throws InsufficientPermissionException if the user has not granted "ads_read" or "ads_management" permission.
+ * @throws MissingAuthorizationException if FacebookAdsTemplate was not created with an access token.
+ */
+ PagedList getAccountAdSets(String accountId);
+
+ /**
+ * Get all ad sets for the given campaign
+ *
+ * @param campaignId the id of the ad campaing
+ * @return the list of {@link AdSet} objects.
+ * @throws ApiException if there is an error while communicating with Facebook.
+ * @throws InsufficientPermissionException if the user has not granted "ads_read" or "ads_management" permission.
+ * @throws MissingAuthorizationException if FacebookAdsTemplate was not created with an access token.
+ */
+ PagedList getCampaignAdSets(String campaignId);
+
+ /**
+ * Gets ad set by given id.
+ *
+ * @param id the id of the ad set
+ * @return the {@link AdSet} object
+ * @throws ApiException if there is an error while communicating with Facebook.
+ * @throws InsufficientPermissionException if the user has not granted "ads_read" or "ads_management" permission.
+ * @throws MissingAuthorizationException if FacebookAdsTemplate was not created with an access token.
+ */
+ AdSet getAdSet(String id);
+
+ /**
+ * Get the insight for the ad set.
+ *
+ * @param adSetId the id of the ad set
+ * @return the {@link AdInsight} object
+ * @throws ApiException if there is an error while communicating with Facebook.
+ * @throws InsufficientPermissionException if the user has not granted "ads_read" or "ads_management" permission.
+ * @throws MissingAuthorizationException if FacebookAdsTemplate was not created with an access token.
+ */
+ AdInsight getAdSetInsight(String adSetId);
+
+ /**
+ * Creates an ad set in the given account
+ *
+ * @param accountId the account id
+ * @param adSet the ad set object
+ * @return the id of the new ad set.
+ * @throws ApiException if there is an error while communicating with Facebook.
+ * @throws InsufficientPermissionException if the user has not granted "ads_read" or "ads_management" permission.
+ * @throws MissingAuthorizationException if FacebookAdsTemplate was not created with an access token.
+ */
+ String createAdSet(String accountId, AdSet adSet);
+
+ /**
+ * Updates the ad set.
+ *
+ * @param adSetId the id of the ad set
+ * @param adSet the ad set object
+ * @return true if update was successful
+ * @throws ApiException if there is an error while communicating with Facebook.
+ * @throws InsufficientPermissionException if the user has not granted "ads_read" or "ads_management" permission.
+ * @throws MissingAuthorizationException if FacebookAdsTemplate was not created with an access token.
+ */
+ boolean updateAdSet(String adSetId, AdSet adSet);
+
+ /**
+ * Deletes ad set given by id.
+ *
+ * @param adSetId the id of the ad set
+ * @throws ApiException if there is an error while communicating with Facebook.
+ * @throws InsufficientPermissionException if the user has not granted "ads_read" or "ads_management" permission.
+ * @throws MissingAuthorizationException if FacebookAdsTemplate was not created with an access token.
+ */
+ void deleteAdSet(String adSetId);
+}
diff --git a/spring-social-facebook/src/main/java/org/springframework/social/facebook/api/ads/AdUser.java b/spring-social-facebook/src/main/java/org/springframework/social/facebook/api/ads/AdUser.java
new file mode 100644
index 000000000..9e4da7279
--- /dev/null
+++ b/spring-social-facebook/src/main/java/org/springframework/social/facebook/api/ads/AdUser.java
@@ -0,0 +1,85 @@
+package org.springframework.social.facebook.api.ads;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonGetter;
+import org.springframework.social.facebook.api.FacebookObject;
+
+import java.util.List;
+
+/**
+ * Model class representing an ad user.
+ *
+ * @author Sebastian Górecki
+ */
+public class AdUser extends FacebookObject {
+ private String id;
+ private String name;
+ private List permissions;
+ private AdUserRole role;
+
+ public String getId() {
+ return id;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public List getPermissions() {
+ return permissions;
+ }
+
+ public AdUserRole getRole() {
+ return role;
+ }
+
+ public enum AdUserPermission {
+ ACCOUNT_ADMIN(1), ADMANAGER_READ(2), ADMANAGER_WRITE(3), BILLING_READ(4), BILLING_WRITE(5), REPORTS(7), UNKNOWN(0);
+
+ private final int value;
+
+ AdUserPermission(int value) {
+ this.value = value;
+ }
+
+ @JsonCreator
+ public static AdUserPermission forValue(int value) {
+ for (AdUserPermission permission : AdUserPermission.values()) {
+ if (permission.getValue() == value) {
+ return permission;
+ }
+ }
+ return UNKNOWN;
+ }
+
+ @JsonGetter
+ public int getValue() {
+ return value;
+ }
+ }
+
+ public enum AdUserRole {
+ ADMINISTRATOR(1001), ADVERTISER(1002), ANALYST(1003), SALES(1004), UNKNOWN(0);
+
+ private final int value;
+
+ AdUserRole(int value) {
+ this.value = value;
+ }
+
+ @JsonCreator
+ public static AdUserRole forValue(int value) {
+ for (AdUserRole role : AdUserRole.values()) {
+ if (role.getValue() == value) {
+ return role;
+ }
+ }
+ return UNKNOWN;
+ }
+
+ @JsonGetter
+ public int getValue() {
+ return value;
+ }
+ }
+}
diff --git a/spring-social-facebook/src/main/java/org/springframework/social/facebook/api/ads/BidInfo.java b/spring-social-facebook/src/main/java/org/springframework/social/facebook/api/ads/BidInfo.java
new file mode 100644
index 000000000..d6f1ca73f
--- /dev/null
+++ b/spring-social-facebook/src/main/java/org/springframework/social/facebook/api/ads/BidInfo.java
@@ -0,0 +1,9 @@
+package org.springframework.social.facebook.api.ads;
+
+import java.util.HashMap;
+
+/**
+ * @author Sebastian Górecki
+ */
+public class BidInfo extends HashMap {
+}
diff --git a/spring-social-facebook/src/main/java/org/springframework/social/facebook/api/ads/BidType.java b/spring-social-facebook/src/main/java/org/springframework/social/facebook/api/ads/BidType.java
new file mode 100644
index 000000000..2f1934b37
--- /dev/null
+++ b/spring-social-facebook/src/main/java/org/springframework/social/facebook/api/ads/BidType.java
@@ -0,0 +1,22 @@
+package org.springframework.social.facebook.api.ads;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+
+/**
+ * Enum used in Ad and AdSet objects.
+ *
+ * @author Sebastian Górecki
+ */
+public enum BidType {
+ CPM, CPC, MULTI_PREMIUM, ABSOLUTE_OCPM, CPA, UNKNOWN;
+
+ @JsonCreator
+ public static BidType fromValue(String value) {
+ for (BidType type : BidType.values()) {
+ if (type.name().equals(value)) {
+ return type;
+ }
+ }
+ return UNKNOWN;
+ }
+}
diff --git a/spring-social-facebook/src/main/java/org/springframework/social/facebook/api/ads/CampaignOperations.java b/spring-social-facebook/src/main/java/org/springframework/social/facebook/api/ads/CampaignOperations.java
new file mode 100644
index 000000000..e2501eb32
--- /dev/null
+++ b/spring-social-facebook/src/main/java/org/springframework/social/facebook/api/ads/CampaignOperations.java
@@ -0,0 +1,106 @@
+package org.springframework.social.facebook.api.ads;
+
+import org.springframework.social.ApiException;
+import org.springframework.social.InsufficientPermissionException;
+import org.springframework.social.MissingAuthorizationException;
+import org.springframework.social.facebook.api.PagedList;
+
+/**
+ * Defines operations for working with Facebook Ad Campaign object.
+ *
+ * @author Sebastian Górecki
+ */
+public interface CampaignOperations {
+
+ static final String[] AD_CAMPAIGN_FIELDS = {
+ "id", "account_id", "buying_type", "campaign_group_status", "name", "objective", "spend_cap"
+ };
+
+ static final String[] AD_CAMPAIGN_INSIGHT_FIELDS = {
+ "account_id", "account_name", "date_start", "date_stop", "actions_per_impression", "clicks", "unique_clicks",
+ "cost_per_result", "cost_per_total_action", "cpc", "cost_per_unique_click", "cpm", "cpp", "ctr", "unique_ctr",
+ "frequency", "impressions", "unique_impressions", "objective", "reach", "result_rate", "results", "roas",
+ "social_clicks", "unique_social_clicks", "social_impressions", "unique_social_impressions", "social_reach",
+ "spend", "today_spend", "total_action_value", "total_actions", "total_unique_actions", "actions",
+ "unique_actions", "cost_per_action_type", "video_start_actions"
+ };
+
+ /**
+ * Gets all ad campaigns belonging to user.
+ *
+ * @param accountId the ID of the ad account (account_id)
+ * @return the list of {@link AdCampaign} objects
+ * @throws ApiException if there is an error while communicating with Facebook.
+ * @throws InsufficientPermissionException if the user has not granted "ads_read" or "ads_management" permission.
+ * @throws MissingAuthorizationException if FacebookAdsTemplate was not created with an access token.
+ */
+ PagedList getAdCampaigns(String accountId);
+
+ /**
+ * Get the campaign by given id.
+ *
+ * @param id the id of the campaign
+ * @return the {@link AdCampaign} object.
+ * @throws ApiException if there is an error while communicating with Facebook.
+ * @throws InsufficientPermissionException if the user has not granted "ads_read" or "ads_management" permission.
+ * @throws MissingAuthorizationException if FacebookAdsTemplate was not created with an access token.
+ */
+ AdCampaign getAdCampaign(String id);
+
+ /**
+ * Get all ad sets from one ad campaign.
+ *
+ * @param campaignId the id of the campaign
+ * @return the list of {@link AdSet} objects
+ * @throws ApiException if there is an error while communicating with Facebook.
+ * @throws InsufficientPermissionException if the user has not granted "ads_read" or "ads_management" permission.
+ * @throws MissingAuthorizationException if FacebookAdsTemplate was not created with an access token.
+ */
+ PagedList getAdCampaignSets(String campaignId);
+
+ /**
+ * Get the insight of the ad campaign.
+ *
+ * @param campaignId the id of the campaign
+ * @return the {@link AdInsight} object
+ * @throws ApiException if there is an error while communicating with Facebook.
+ * @throws InsufficientPermissionException if the user has not granted "ads_read" or "ads_management" permission.
+ * @throws MissingAuthorizationException if FacebookAdsTemplate was not created with an access token.
+ */
+ AdInsight getAdCampaignInsight(String campaignId);
+
+ /**
+ * Creates new campaign based on adCampaign object.
+ *
+ * @param accountId the ID of the ad account (account_id)
+ * @param adCampaign the ad campaign object
+ * @return the id of the created ad campaign
+ * @throws ApiException if there is an error while communicating with Facebook.
+ * @throws InsufficientPermissionException if the user has not granted "ads_read" or "ads_management" permission.
+ * @throws MissingAuthorizationException if FacebookAdsTemplate was not created with an access token.
+ * @throws InvalidCampaignStatusException if you provided wrong status for the new campaign
+ */
+ String createAdCampaign(String accountId, AdCampaign adCampaign);
+
+ /**
+ * Updates the ad campaign with information in adCampaign object.
+ *
+ * @param campaignId the ID of the ad campaign to update
+ * @param adCampaign the ad campaign object
+ * @return true if update was successful
+ * @throws ApiException if there is an error while communicating with Facebook.
+ * @throws InsufficientPermissionException if the user has not granted "ads_read" or "ads_management" permission.
+ * @throws MissingAuthorizationException if FacebookAdsTemplate was not created with an access token.
+ */
+ boolean updateAdCampaign(String campaignId, AdCampaign adCampaign);
+
+ /**
+ * Deletes the ad campaign given by id.
+ *
+ * @param campaignId the id of the campaign to delete
+ * @throws ApiException if there is an error while communicating with Facebook.
+ * @throws InsufficientPermissionException if the user has not granted "ads_read" or "ads_management" permission.
+ * @throws MissingAuthorizationException if FacebookAdsTemplate was not created with an access token.
+ */
+ void deleteAdCampaign(String campaignId);
+}
diff --git a/spring-social-facebook/src/main/java/org/springframework/social/facebook/api/ads/CreativeOperations.java b/spring-social-facebook/src/main/java/org/springframework/social/facebook/api/ads/CreativeOperations.java
new file mode 100644
index 000000000..753045f96
--- /dev/null
+++ b/spring-social-facebook/src/main/java/org/springframework/social/facebook/api/ads/CreativeOperations.java
@@ -0,0 +1,86 @@
+package org.springframework.social.facebook.api.ads;
+
+import org.springframework.social.ApiException;
+import org.springframework.social.InsufficientPermissionException;
+import org.springframework.social.MissingAuthorizationException;
+import org.springframework.social.facebook.api.PagedList;
+
+/**
+ * Defines operations for working with Facebook Creative object.
+ *
+ * @author Sebastian Górecki
+ */
+public interface CreativeOperations {
+
+ static final String[] AD_CREATIVE_FIELDS = {
+ "actor_id", "body", "image_hash", "image_url", "link_url", "name", "object_id", "object_story_id",
+ "object_url", "run_status", "title", "url_tags", "thumbnail_url", "object_type", "id"
+ };
+
+ /**
+ * Retrieve an account's ad creatives.
+ *
+ * @param accountId the ID of the ad account.
+ * @return the list of {@link AdCreative} objects.
+ * @throws ApiException if there is an error while communicating with Facebook.
+ * @throws InsufficientPermissionException if the user has not granted "ads_read" or "ads_management" permission.
+ * @throws MissingAuthorizationException if FacebookAdsTemplate was not created with an access token.
+ */
+ PagedList getAccountCreatives(String accountId);
+
+ /**
+ * Retrieve an ad set's ad creatives.
+ *
+ * @param adSetId the id of the ad set.
+ * @return the list of {@link AdCreative} objects.
+ * @throws ApiException if there is an error while communicating with Facebook.
+ * @throws InsufficientPermissionException if the user has not granted "ads_read" or "ads_management" permission.
+ * @throws MissingAuthorizationException if FacebookAdsTemplate was not created with an access token.
+ */
+ PagedList getAdSetCreatives(String adSetId);
+
+ /**
+ * Get information about an ad creative.
+ *
+ * @param creativeId the id of the ad creative
+ * @return the {@link AdCreative} object.
+ * @throws ApiException if there is an error while communicating with Facebook.
+ * @throws InsufficientPermissionException if the user has not granted "ads_read" or "ads_management" permission.
+ * @throws MissingAuthorizationException if FacebookAdsTemplate was not created with an access token.
+ */
+ AdCreative getAdCreative(String creativeId);
+
+ /**
+ * Create an ad creative in the given account.
+ *
+ * @param accountId the ID of the ad account.
+ * @param creative the {@link AdInsight} object.
+ * @return the id of the ad creative
+ * @throws ApiException if there is an error while communicating with Facebook.
+ * @throws InsufficientPermissionException if the user has not granted "ads_read" or "ads_management" permission.
+ * @throws MissingAuthorizationException if FacebookAdsTemplate was not created with an access token.
+ */
+ String createAdCreative(String accountId, AdCreative creative);
+
+ /**
+ * Rename an ad creative in creative library.
+ *
+ * @param creativeId the id of the ad creative.
+ * @param name new name of the ad creative
+ * @return true if rename was successful
+ * @throws ApiException if there is an error while communicating with Facebook.
+ * @throws InsufficientPermissionException if the user has not granted "ads_read" or "ads_management" permission.
+ * @throws MissingAuthorizationException if FacebookAdsTemplate was not created with an access token.
+ */
+ boolean renameAdCreative(String creativeId, String name);
+
+ /**
+ * Delete given ad creative.
+ *
+ * @param creativeId the id of the ad creative
+ * @throws ApiException if there is an error while communicating with Facebook.
+ * @throws InsufficientPermissionException if the user has not granted "ads_read" or "ads_management" permission.
+ * @throws MissingAuthorizationException if FacebookAdsTemplate was not created with an access token.
+ */
+ void deleteAdCreative(String creativeId);
+}
diff --git a/spring-social-facebook/src/main/java/org/springframework/social/facebook/api/ads/FacebookAds.java b/spring-social-facebook/src/main/java/org/springframework/social/facebook/api/ads/FacebookAds.java
new file mode 100644
index 000000000..ba3ff951f
--- /dev/null
+++ b/spring-social-facebook/src/main/java/org/springframework/social/facebook/api/ads/FacebookAds.java
@@ -0,0 +1,46 @@
+package org.springframework.social.facebook.api.ads;
+
+import org.springframework.social.facebook.api.ads.impl.FacebookAdsTemplate;
+
+/**
+ * Interface specifying a basic set of operations for interacting with Facebook Marketing API.
+ * Implemented by {@link FacebookAdsTemplate}.
+ *
+ * @author Sebastian Górecki
+ */
+public interface FacebookAds {
+ /**
+ * API for working with Facebook Ad account.
+ *
+ * @return {@link AccountOperations}
+ */
+ AccountOperations accountOperations();
+
+ /**
+ * API for working with Facebook Ad campaign.
+ *
+ * @return {@link CampaignOperations}
+ */
+ CampaignOperations campaignOperations();
+
+ /**
+ * API for working with Facebook AdSet.
+ *
+ * @return {@link AdSetOperations}
+ */
+ AdSetOperations adSetOperations();
+
+ /**
+ * API for working with Facebook creative.
+ *
+ * @return {@link CreativeOperations}
+ */
+ CreativeOperations creativeOperations();
+
+ /**
+ * API for working with Facebook ad.
+ *
+ * @return {@link AdOperations}
+ */
+ AdOperations adOperations();
+}
diff --git a/spring-social-facebook/src/main/java/org/springframework/social/facebook/api/ads/InvalidCampaignStatusException.java b/spring-social-facebook/src/main/java/org/springframework/social/facebook/api/ads/InvalidCampaignStatusException.java
new file mode 100644
index 000000000..87548c1e7
--- /dev/null
+++ b/spring-social-facebook/src/main/java/org/springframework/social/facebook/api/ads/InvalidCampaignStatusException.java
@@ -0,0 +1,14 @@
+package org.springframework.social.facebook.api.ads;
+
+import org.springframework.social.facebook.api.InvalidParameterException;
+
+/**
+ * Exception is thrown when a new ad campaign is created with status other that ACTIVE or PAUSED.
+ *
+ * @author Sebastian Górecki
+ */
+public class InvalidCampaignStatusException extends InvalidParameterException {
+ public InvalidCampaignStatusException(String providerId, String message) {
+ super(providerId, message);
+ }
+}
diff --git a/spring-social-facebook/src/main/java/org/springframework/social/facebook/api/ads/PromotedObject.java b/spring-social-facebook/src/main/java/org/springframework/social/facebook/api/ads/PromotedObject.java
new file mode 100644
index 000000000..f9df1dbd4
--- /dev/null
+++ b/spring-social-facebook/src/main/java/org/springframework/social/facebook/api/ads/PromotedObject.java
@@ -0,0 +1,9 @@
+package org.springframework.social.facebook.api.ads;
+
+import java.util.HashMap;
+
+/**
+ * @author Sebastian Górecki
+ */
+public class PromotedObject extends HashMap {
+}
diff --git a/spring-social-facebook/src/main/java/org/springframework/social/facebook/api/ads/Targeting.java b/spring-social-facebook/src/main/java/org/springframework/social/facebook/api/ads/Targeting.java
new file mode 100644
index 000000000..1bf968cb7
--- /dev/null
+++ b/spring-social-facebook/src/main/java/org/springframework/social/facebook/api/ads/Targeting.java
@@ -0,0 +1,319 @@
+package org.springframework.social.facebook.api.ads;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonValue;
+
+import java.util.List;
+
+/**
+ * @author Sebastian Górecki
+ */
+public class Targeting {
+
+ // demographics
+ private List genders;
+ private Integer ageMin;
+ private Integer ageMax;
+ private List relationshipStatuses;
+ private List interestedIn;
+
+ // location
+ private TargetingLocation geoLocations;
+ private TargetingLocation excludedGeoLocations;
+
+ // placement
+ private List pageTypes;
+
+ // connections
+ private List connections;
+ private List excludedConnections;
+ private List friendsOfConnections;
+
+ // interests
+ private List interests;
+
+ // behaviors
+ private List behaviors;
+
+ // education and workplace
+ private List educationSchools;
+ private List educationStatuses;
+ private List collegeYears;
+ private List educationMajors;
+ private List workEmployers;
+ private List workPositions;
+
+ public List getGenders() {
+ return genders;
+ }
+
+ public void setGenders(List genders) {
+ this.genders = genders;
+ }
+
+ public Integer getAgeMin() {
+ return ageMin;
+ }
+
+ public void setAgeMin(Integer ageMin) {
+ this.ageMin = ageMin;
+ }
+
+ public Integer getAgeMax() {
+ return ageMax;
+ }
+
+ public void setAgeMax(Integer ageMax) {
+ this.ageMax = ageMax;
+ }
+
+ public List getRelationshipStatuses() {
+ return relationshipStatuses;
+ }
+
+ public void setRelationshipStatuses(List relationshipStatuses) {
+ this.relationshipStatuses = relationshipStatuses;
+ }
+
+ public List getInterestedIn() {
+ return interestedIn;
+ }
+
+ public void setInterestedIn(List interestedIn) {
+ this.interestedIn = interestedIn;
+ }
+
+ public TargetingLocation getGeoLocations() {
+ return geoLocations;
+ }
+
+ public void setGeoLocations(TargetingLocation geoLocations) {
+ this.geoLocations = geoLocations;
+ }
+
+ public TargetingLocation getExcludedGeoLocations() {
+ return excludedGeoLocations;
+ }
+
+ public void setExcludedGeoLocations(TargetingLocation excludedGeoLocations) {
+ this.excludedGeoLocations = excludedGeoLocations;
+ }
+
+ public List getPageTypes() {
+ return pageTypes;
+ }
+
+ public void setPageTypes(List pageTypes) {
+ this.pageTypes = pageTypes;
+ }
+
+ public List getConnections() {
+ return connections;
+ }
+
+ public void setConnections(List connections) {
+ this.connections = connections;
+ }
+
+ public List getExcludedConnections() {
+ return excludedConnections;
+ }
+
+ public void setExcludedConnections(List excludedConnections) {
+ this.excludedConnections = excludedConnections;
+ }
+
+ public List getFriendsOfConnections() {
+ return friendsOfConnections;
+ }
+
+ public void setFriendsOfConnections(List friendsOfConnections) {
+ this.friendsOfConnections = friendsOfConnections;
+ }
+
+ public List getInterests() {
+ return interests;
+ }
+
+ public void setInterests(List interests) {
+ this.interests = interests;
+ }
+
+ public List getBehaviors() {
+ return behaviors;
+ }
+
+ public void setBehaviors(List behaviors) {
+ this.behaviors = behaviors;
+ }
+
+ public List getEducationSchools() {
+ return educationSchools;
+ }
+
+ public void setEducationSchools(List educationSchools) {
+ this.educationSchools = educationSchools;
+ }
+
+ public List getEducationStatuses() {
+ return educationStatuses;
+ }
+
+ public void setEducationStatuses(List educationStatuses) {
+ this.educationStatuses = educationStatuses;
+ }
+
+ public List getCollegeYears() {
+ return collegeYears;
+ }
+
+ public void setCollegeYears(List collegeYears) {
+ this.collegeYears = collegeYears;
+ }
+
+ public List getEducationMajors() {
+ return educationMajors;
+ }
+
+ public void setEducationMajors(List educationMajors) {
+ this.educationMajors = educationMajors;
+ }
+
+ public List getWorkEmployers() {
+ return workEmployers;
+ }
+
+ public void setWorkEmployers(List workEmployers) {
+ this.workEmployers = workEmployers;
+ }
+
+ public List getWorkPositions() {
+ return workPositions;
+ }
+
+ public void setWorkPositions(List workPositions) {
+ this.workPositions = workPositions;
+ }
+
+ public enum Gender {
+ UNKNOWN(0), MALE(1), FEMALE(2);
+
+ private final int value;
+
+ Gender(int value) {
+ this.value = value;
+ }
+
+ @JsonCreator
+ public static Gender fromValue(int value) {
+ for (Gender gender : Gender.values()) {
+ if (gender.getValue() == value) return gender;
+ }
+ return UNKNOWN;
+ }
+
+ @JsonValue
+ public int getValue() {
+ return value;
+ }
+ }
+
+ public enum RelationshipStatus {
+ UNKNOWN(0), SINGLE(1), IN_RELATIONSHIP(2), MARRIED(3), ENGAGED(4), NOT_SPECIFIED(6), IN_CIVIL_UNION(7),
+ IN_DOMESTIC_PARTNERSHIP(8), IN_OPEN_RELATIONSHIP(9), ITS_COMPLICATED(10), SEPARATED(11), DIVORCED(12),
+ WIDOWED(13);
+
+ private final int value;
+
+ RelationshipStatus(int value) {
+ this.value = value;
+ }
+
+ @JsonCreator
+ public static RelationshipStatus fromValue(int value) {
+ for (RelationshipStatus status : RelationshipStatus.values()) {
+ if (status.getValue() == value) return status;
+ }
+ return UNKNOWN;
+ }
+
+ @JsonValue
+ public int getValue() {
+ return value;
+ }
+ }
+
+ public enum InterestedIn {
+ UNKNOWN(0), MEN(1), WOMEN(2), MAN_AND_WOMAN(3), NOT_SPECIFIED(4);
+
+ private final int value;
+
+ InterestedIn(int value) {
+ this.value = value;
+ }
+
+ @JsonCreator
+ public static InterestedIn fromValue(int value) {
+ for (InterestedIn interestedIn : InterestedIn.values()) {
+ if (interestedIn.getValue() == value) return interestedIn;
+ }
+ return UNKNOWN;
+ }
+
+ @JsonValue
+ public int getValue() {
+ return value;
+ }
+ }
+
+ public enum PageType {
+ UNKNOWN("unknown"), DESKTOP("desktop"), FEED("feed"), DESKTOP_FEED("desktopfeed"), MOBILE("mobile"),
+ MOBILE_FEED_AND_EXTERNAL("mobilefeed-and-external"), MOBILE_FEED("mobilefeed"), RIGHTCOLUMN("rightcolumn"),
+ RIGHTCOLUMN_AND_MOBILE("rightcolumn-and-mobile"), HOME("home"), DESKTOP_AND_MOBILE_AND_EXTERNAL("desktop-and-mobile-and-external"),
+ FEED_AND_EXTERNAL("feed-and-external"), RIGHTCOLUMN_AND_MOBILE_AND_EXTERNAL("rightcolumn-and-mobile-and-external");
+
+ private final String value;
+
+ PageType(String value) {
+ this.value = value;
+ }
+
+ @JsonCreator
+ public static PageType fromValue(String value) {
+ for (PageType type : PageType.values()) {
+ if (type.getValue().equals(value)) return type;
+ }
+ return UNKNOWN;
+ }
+
+ @JsonValue
+ public String getValue() {
+ return value;
+ }
+ }
+
+ public enum EducationStatus {
+ UNKNOWN(0), HIGH_SCHOOL(1), UNDERGRAD(2), ALUM(3), HIGH_SCHOOL_GRAD(4), SOME_COLLEGE(5), ASSOCIATE_DEGREE(6),
+ IN_GRAD_SCHOOL(7), SOME_GRAD_SCHOOL(8), MASTER_DEGREE(9), PROFESSIONAL_DEGREE(10), DOCTORATE_DEGREE(11),
+ UNSPECIFIED(12), SOME_HIGH_SCHOOL(13);
+
+ private final int value;
+
+ EducationStatus(int value) {
+ this.value = value;
+ }
+
+ @JsonCreator
+ public static EducationStatus fromValue(int value) {
+ for (EducationStatus status : EducationStatus.values()) {
+ if (status.getValue() == value) return status;
+ }
+ return UNKNOWN;
+ }
+
+ @JsonValue
+ public int getValue() {
+ return value;
+ }
+ }
+}
diff --git a/spring-social-facebook/src/main/java/org/springframework/social/facebook/api/ads/TargetingCityEntry.java b/spring-social-facebook/src/main/java/org/springframework/social/facebook/api/ads/TargetingCityEntry.java
new file mode 100644
index 000000000..1ab1de3b2
--- /dev/null
+++ b/spring-social-facebook/src/main/java/org/springframework/social/facebook/api/ads/TargetingCityEntry.java
@@ -0,0 +1,31 @@
+package org.springframework.social.facebook.api.ads;
+
+/**
+ * @author Sebastian Górecki
+ */
+public class TargetingCityEntry {
+ private String key;
+ private int radius;
+ private String distanceUnit;
+
+ TargetingCityEntry() {
+ }
+
+ public TargetingCityEntry(String key, int radius, String distanceUnit) {
+ this.key = key;
+ this.radius = radius;
+ this.distanceUnit = distanceUnit;
+ }
+
+ public String getKey() {
+ return key;
+ }
+
+ public int getRadius() {
+ return radius;
+ }
+
+ public String getDistanceUnit() {
+ return distanceUnit;
+ }
+}
diff --git a/spring-social-facebook/src/main/java/org/springframework/social/facebook/api/ads/TargetingEntry.java b/spring-social-facebook/src/main/java/org/springframework/social/facebook/api/ads/TargetingEntry.java
new file mode 100644
index 000000000..51314fee7
--- /dev/null
+++ b/spring-social-facebook/src/main/java/org/springframework/social/facebook/api/ads/TargetingEntry.java
@@ -0,0 +1,25 @@
+package org.springframework.social.facebook.api.ads;
+
+/**
+ * @author Sebastian Górecki
+ */
+public class TargetingEntry {
+ private long id;
+ private String name;
+
+ public TargetingEntry() {
+ }
+
+ public TargetingEntry(long id, String name) {
+ this.id = id;
+ this.name = name;
+ }
+
+ public long getId() {
+ return id;
+ }
+
+ public String getName() {
+ return name;
+ }
+}
diff --git a/spring-social-facebook/src/main/java/org/springframework/social/facebook/api/ads/TargetingLocation.java b/spring-social-facebook/src/main/java/org/springframework/social/facebook/api/ads/TargetingLocation.java
new file mode 100644
index 000000000..9ed228033
--- /dev/null
+++ b/spring-social-facebook/src/main/java/org/springframework/social/facebook/api/ads/TargetingLocation.java
@@ -0,0 +1,80 @@
+package org.springframework.social.facebook.api.ads;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonValue;
+
+import java.util.List;
+
+/**
+ * @author Sebastian Górecki
+ */
+public class TargetingLocation {
+ private List countries;
+ private List regions;
+ private List cities;
+ private List zips;
+ private List locationTypes;
+
+ public List getLocationTypes() {
+ return locationTypes;
+ }
+
+ public void setLocationTypes(List locationTypes) {
+ this.locationTypes = locationTypes;
+ }
+
+ public List getCountries() {
+ return countries;
+ }
+
+ public void setCountries(List countries) {
+ this.countries = countries;
+ }
+
+ public List getRegions() {
+ return regions;
+ }
+
+ public void setRegions(List regions) {
+ this.regions = regions;
+ }
+
+ public List getCities() {
+ return cities;
+ }
+
+ public void setCities(List cities) {
+ this.cities = cities;
+ }
+
+ public List getZips() {
+ return zips;
+ }
+
+ public void setZips(List zips) {
+ this.zips = zips;
+ }
+
+ public enum LocationType {
+ UNKNOWN("unknown"), RECENT("recent"), HOME("home"), TRAVEL_IN("travel_in");
+
+ private final String value;
+
+ LocationType(String value) {
+ this.value = value;
+ }
+
+ @JsonCreator
+ public static LocationType fromValue(String value) {
+ for (LocationType type : LocationType.values()) {
+ if (type.getValue().equals(value)) return type;
+ }
+ return UNKNOWN;
+ }
+
+ @JsonValue
+ public String getValue() {
+ return value;
+ }
+ }
+}
diff --git a/spring-social-facebook/src/main/java/org/springframework/social/facebook/api/ads/impl/AccountTemplate.java b/spring-social-facebook/src/main/java/org/springframework/social/facebook/api/ads/impl/AccountTemplate.java
new file mode 100644
index 000000000..31fc40f60
--- /dev/null
+++ b/spring-social-facebook/src/main/java/org/springframework/social/facebook/api/ads/impl/AccountTemplate.java
@@ -0,0 +1,72 @@
+package org.springframework.social.facebook.api.ads.impl;
+
+import org.springframework.social.facebook.api.GraphApi;
+import org.springframework.social.facebook.api.PagedList;
+import org.springframework.social.facebook.api.ads.*;
+import org.springframework.social.facebook.api.ads.AdUser.AdUserRole;
+import org.springframework.social.facebook.api.impl.AbstractFacebookOperations;
+import org.springframework.util.LinkedMultiValueMap;
+import org.springframework.util.MultiValueMap;
+
+/**
+ * @author Sebastian Górecki
+ */
+public class AccountTemplate extends AbstractFacebookOperations implements AccountOperations {
+
+ private final GraphApi graphApi;
+
+ public AccountTemplate(GraphApi graphApi, boolean isAuthorizedForUser) {
+ super(isAuthorizedForUser);
+ this.graphApi = graphApi;
+ }
+
+ public PagedList getAdAccounts(String userId) {
+ requireAuthorization();
+ return graphApi.fetchConnections(userId, "adaccounts", AdAccount.class, AD_ACCOUNT_FIELDS);
+ }
+
+ public AdAccount getAdAccount(String accountId) {
+ requireAuthorization();
+ return graphApi.fetchObject(getAdAccountId(accountId), AdAccount.class, AD_ACCOUNT_FIELDS);
+ }
+
+ public PagedList getAdAccountCampaigns(String accountId) {
+ return graphApi.fetchConnections(getAdAccountId(accountId), "adcampaign_groups", AdCampaign.class, CampaignOperations.AD_CAMPAIGN_FIELDS);
+ }
+
+ public PagedList getAdAccountUsers(String accountId) {
+ requireAuthorization();
+ return graphApi.fetchConnections(getAdAccountId(accountId), "users", AdUser.class);
+ }
+
+ public void addUserToAdAccount(String accountId, String userId, AdUserRole role) {
+ requireAuthorization();
+ MultiValueMap map = new LinkedMultiValueMap();
+ map.set("uid", userId);
+ map.set("role", String.valueOf(role.getValue()));
+ graphApi.post(getAdAccountId(accountId) + "/users", "", map);
+ }
+
+ public void deleteUserFromAdAccount(String accountId, String userId) {
+ requireAuthorization();
+ graphApi.delete(getAdAccountId(accountId) + "/users/" + userId);
+ }
+
+ public AdInsight getAdAccountInsight(String accountId) {
+ requireAuthorization();
+ PagedList insights = graphApi.fetchConnections(getAdAccountId(accountId), "insights", AdInsight.class, AD_ACCOUNT_INSIGHT_FIELDS);
+ return insights.get(0);
+ }
+
+ public boolean updateAdAccount(String accountId, AdAccount adAccount) {
+ requireAuthorization();
+ MultiValueMap map = new LinkedMultiValueMap();
+ if (adAccount.getName() != null) {
+ map.set("name", adAccount.getName());
+ }
+ if (adAccount.getSpendCap() != null) {
+ map.set("spend_cap", adAccount.getSpendCap());
+ }
+ return graphApi.update(getAdAccountId(accountId), map);
+ }
+}
diff --git a/spring-social-facebook/src/main/java/org/springframework/social/facebook/api/ads/impl/AdSetTemplate.java b/spring-social-facebook/src/main/java/org/springframework/social/facebook/api/ads/impl/AdSetTemplate.java
new file mode 100644
index 000000000..8d46e4846
--- /dev/null
+++ b/spring-social-facebook/src/main/java/org/springframework/social/facebook/api/ads/impl/AdSetTemplate.java
@@ -0,0 +1,112 @@
+package org.springframework.social.facebook.api.ads.impl;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.springframework.social.facebook.api.GraphApi;
+import org.springframework.social.facebook.api.PagedList;
+import org.springframework.social.facebook.api.ads.AdInsight;
+import org.springframework.social.facebook.api.ads.AdSet;
+import org.springframework.social.facebook.api.ads.AdSetOperations;
+import org.springframework.social.facebook.api.impl.AbstractFacebookOperations;
+import org.springframework.util.LinkedMultiValueMap;
+import org.springframework.util.MultiValueMap;
+
+/**
+ * @author Sebastian Górecki
+ */
+public class AdSetTemplate extends AbstractFacebookOperations implements AdSetOperations {
+ private GraphApi graphApi;
+ private ObjectMapper mapper;
+
+ public AdSetTemplate(GraphApi graphApi, ObjectMapper mapper, boolean authorized) {
+ super(authorized);
+ this.graphApi = graphApi;
+ this.mapper = mapper;
+ }
+
+ public PagedList getAccountAdSets(String accountId) {
+ requireAuthorization();
+ return graphApi.fetchConnections(getAdAccountId(accountId), "adcampaigns", AdSet.class, AdSetOperations.AD_SET_FIELDS);
+ }
+
+ public PagedList getCampaignAdSets(String campaignId) {
+ requireAuthorization();
+ return graphApi.fetchConnections(campaignId, "adcampaigns", AdSet.class, AdSetOperations.AD_SET_FIELDS);
+ }
+
+ public AdSet getAdSet(String id) {
+ requireAuthorization();
+ return graphApi.fetchObject(id, AdSet.class, AdSetOperations.AD_SET_FIELDS);
+ }
+
+ public AdInsight getAdSetInsight(String adSetId) {
+ requireAuthorization();
+ PagedList insights = graphApi.fetchConnections(adSetId, "insights", AdInsight.class, AdSetOperations.AD_SET_INSIGHT_FIELDS);
+ return insights.get(0);
+ }
+
+ public String createAdSet(String accountId, AdSet adSet) {
+ requireAuthorization();
+ MultiValueMap data = mapCommonFields(adSet);
+ data.set("campaign_group_id", adSet.getCampaignId());
+ return graphApi.publish(getAdAccountId(accountId), "adcampaigns", data);
+ }
+
+ public boolean updateAdSet(String adSetId, AdSet adSet) {
+ requireAuthorization();
+ MultiValueMap data = mapCommonFields(adSet);
+ return graphApi.update(adSetId, data);
+ }
+
+ public void deleteAdSet(String adSetId) {
+ requireAuthorization();
+ MultiValueMap data = new LinkedMultiValueMap();
+ data.add("campaign_status", "DELETED");
+ graphApi.post(adSetId, data);
+ }
+
+ private MultiValueMap mapCommonFields(AdSet adSet) {
+ MultiValueMap data = new LinkedMultiValueMap();
+ data.set("date_format", "U");
+ if (adSet.getName() != null) {
+ data.set("name", adSet.getName());
+ }
+ if (adSet.getStatus() != null) {
+ data.set("campaign_status", adSet.getStatus().name());
+ }
+ data.set("is_autobid", String.valueOf(adSet.isAutobid()));
+ if (adSet.getBidInfo() != null) {
+ try {
+ data.set("bid_info", mapper.writeValueAsString(adSet.getBidInfo()));
+ } catch (JsonProcessingException e) {
+ e.printStackTrace();
+ }
+ }
+ if (adSet.getBidType() != null) {
+ data.set("bid_type", adSet.getBidType().name());
+ }
+ data.set("daily_budget", String.valueOf(adSet.getDailyBudget()));
+ data.set("lifetime_budget", String.valueOf(adSet.getLifetimeBudget()));
+ if (adSet.getPromotedObject() != null) {
+ try {
+ data.set("promoted_object", mapper.writeValueAsString(adSet.getPromotedObject()));
+ } catch (JsonProcessingException e) {
+ e.printStackTrace();
+ }
+ }
+ if (adSet.getTargeting() != null) {
+ try {
+ data.set("targeting", mapper.writeValueAsString(adSet.getTargeting()));
+ } catch (JsonProcessingException e) {
+ e.printStackTrace();
+ }
+ }
+ if (adSet.getStartTime() != null) {
+ data.set("start_time", getUnixTime(adSet.getStartTime()));
+ }
+ if (adSet.getEndTime() != null) {
+ data.set("end_time", getUnixTime(adSet.getEndTime()));
+ }
+ return data;
+ }
+}
\ No newline at end of file
diff --git a/spring-social-facebook/src/main/java/org/springframework/social/facebook/api/ads/impl/AdTemplate.java b/spring-social-facebook/src/main/java/org/springframework/social/facebook/api/ads/impl/AdTemplate.java
new file mode 100644
index 000000000..e25eaad18
--- /dev/null
+++ b/spring-social-facebook/src/main/java/org/springframework/social/facebook/api/ads/impl/AdTemplate.java
@@ -0,0 +1,95 @@
+package org.springframework.social.facebook.api.ads.impl;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.springframework.social.facebook.api.GraphApi;
+import org.springframework.social.facebook.api.PagedList;
+import org.springframework.social.facebook.api.ads.Ad;
+import org.springframework.social.facebook.api.ads.AdInsight;
+import org.springframework.social.facebook.api.ads.AdOperations;
+import org.springframework.social.facebook.api.impl.AbstractFacebookOperations;
+import org.springframework.util.LinkedMultiValueMap;
+import org.springframework.util.MultiValueMap;
+import org.springframework.web.client.RestTemplate;
+
+/**
+ * @author Sebastian Górecki
+ */
+public class AdTemplate extends AbstractFacebookOperations implements AdOperations {
+
+ private final GraphApi graphApi;
+ private final RestTemplate restTemplate;
+ private ObjectMapper mapper;
+
+ public AdTemplate(GraphApi graphApi, RestTemplate restTemplate, ObjectMapper mapper, boolean isAuthorizedForUser) {
+ super(isAuthorizedForUser);
+ this.graphApi = graphApi;
+ this.restTemplate = restTemplate;
+ this.mapper = mapper;
+ }
+
+ public PagedList getAccountAds(String accountId) {
+ requireAuthorization();
+ return graphApi.fetchConnections(getAdAccountId(accountId), "adgroups", Ad.class, AdOperations.AD_FIELDS);
+ }
+
+ public PagedList getCampaignAds(String campaignId) {
+ requireAuthorization();
+ return graphApi.fetchConnections(campaignId, "adgroups", Ad.class, AdOperations.AD_FIELDS);
+ }
+
+ public PagedList getAdSetAds(String adSetId) {
+ requireAuthorization();
+ return graphApi.fetchConnections(adSetId, "adgroups", Ad.class, AdOperations.AD_FIELDS);
+ }
+
+ public Ad getAd(String adId) {
+ requireAuthorization();
+ return graphApi.fetchObject(adId, Ad.class, AdOperations.AD_FIELDS);
+ }
+
+ public AdInsight getAdInsight(String adId) {
+ requireAuthorization();
+ PagedList insights = graphApi.fetchConnections(adId, "insights", AdInsight.class, AdOperations.AD_INSIGHT_FIELDS);
+ return insights.get(0);
+ }
+
+ public String createAd(String accountId, Ad ad) {
+ requireAuthorization();
+ MultiValueMap data = mapCommonFields(ad);
+ data.add("campaign_id", ad.getAdSetId());
+ return graphApi.publish(getAdAccountId(accountId), "adgroups", data);
+ }
+
+ public boolean updateAd(String adId, Ad ad) {
+ requireAuthorization();
+ MultiValueMap data = mapCommonFields(ad);
+ return graphApi.update(adId, data);
+ }
+
+ public void deleteAd(String adId) {
+ requireAuthorization();
+ restTemplate.delete(GraphApi.GRAPH_API_URL + adId);
+ }
+
+ private MultiValueMap mapCommonFields(Ad ad) {
+ MultiValueMap data = new LinkedMultiValueMap();
+ if (ad.getName() != null) {
+ data.add("name", ad.getName());
+ }
+ if (ad.getStatus() != null) {
+ data.add("adgroup_status", ad.getStatus().name());
+ }
+ if (ad.getBidInfo() != null) {
+ try {
+ data.add("bid_info", mapper.writeValueAsString(ad.getBidInfo()));
+ } catch (JsonProcessingException e) {
+ e.printStackTrace();
+ }
+ }
+ if (ad.getCreativeId() != null) {
+ data.add("creative", "{\"creative_id\": \"" + ad.getCreativeId() + "\"}");
+ }
+ return data;
+ }
+}
diff --git a/spring-social-facebook/src/main/java/org/springframework/social/facebook/api/ads/impl/CampaignTemplate.java b/spring-social-facebook/src/main/java/org/springframework/social/facebook/api/ads/impl/CampaignTemplate.java
new file mode 100644
index 000000000..6293bff12
--- /dev/null
+++ b/spring-social-facebook/src/main/java/org/springframework/social/facebook/api/ads/impl/CampaignTemplate.java
@@ -0,0 +1,82 @@
+package org.springframework.social.facebook.api.ads.impl;
+
+import org.springframework.social.facebook.api.GraphApi;
+import org.springframework.social.facebook.api.PagedList;
+import org.springframework.social.facebook.api.ads.*;
+import org.springframework.social.facebook.api.ads.AdCampaign.CampaignStatus;
+import org.springframework.social.facebook.api.impl.AbstractFacebookOperations;
+import org.springframework.util.LinkedMultiValueMap;
+import org.springframework.util.MultiValueMap;
+
+/**
+ * @author Sebastian Górecki
+ */
+public class CampaignTemplate extends AbstractFacebookOperations implements CampaignOperations {
+
+ private final GraphApi graphApi;
+
+ public CampaignTemplate(GraphApi graphApi, boolean isAuthorizedForUser) {
+ super(isAuthorizedForUser);
+ this.graphApi = graphApi;
+ }
+
+ public PagedList getAdCampaigns(String accountId) {
+ requireAuthorization();
+ return graphApi.fetchConnections(getAdAccountId(accountId), "adcampaign_groups", AdCampaign.class, CampaignOperations.AD_CAMPAIGN_FIELDS);
+ }
+
+ public AdCampaign getAdCampaign(String id) {
+ requireAuthorization();
+ return graphApi.fetchObject(id, AdCampaign.class, CampaignOperations.AD_CAMPAIGN_FIELDS);
+ }
+
+ public PagedList getAdCampaignSets(String campaignId) {
+ requireAuthorization();
+ return graphApi.fetchConnections(campaignId, "adcampaigns", AdSet.class, AdSetOperations.AD_SET_FIELDS);
+ }
+
+ public AdInsight getAdCampaignInsight(String campaignId) {
+ requireAuthorization();
+ PagedList insights = graphApi.fetchConnections(campaignId, "insights", AdInsight.class, CampaignOperations.AD_CAMPAIGN_INSIGHT_FIELDS);
+ return insights.get(0);
+ }
+
+ public String createAdCampaign(String accountId, AdCampaign adCampaign) {
+ requireAuthorization();
+ MultiValueMap map = mapCommonFields(adCampaign);
+ if (adCampaign.getBuyingType() != null) {
+ map.add("buying_type", adCampaign.getBuyingType().name());
+ }
+ return graphApi.publish(getAdAccountId(accountId), "adcampaign_groups", map);
+ }
+
+ public boolean updateAdCampaign(String campaignId, AdCampaign adCampaign) {
+ requireAuthorization();
+ MultiValueMap map = mapCommonFields(adCampaign);
+ return graphApi.update(campaignId, map);
+ }
+
+ public void deleteAdCampaign(String campaignId) {
+ requireAuthorization();
+ MultiValueMap map = new LinkedMultiValueMap();
+ map.add("campaign_group_status", CampaignStatus.DELETED.name());
+ graphApi.post(campaignId, map);
+ }
+
+ private MultiValueMap mapCommonFields(AdCampaign adCampaign) {
+ MultiValueMap map = new LinkedMultiValueMap();
+ if (adCampaign.getName() != null) {
+ map.add("name", adCampaign.getName());
+ }
+ if (adCampaign.getStatus() != null) {
+ map.add("campaign_group_status", adCampaign.getStatus().name());
+ }
+ if (adCampaign.getObjective() != null) {
+ map.add("objective", adCampaign.getObjective().name());
+ }
+ if (adCampaign.getSpendCap() != 0) {
+ map.add("spend_cap", String.valueOf(adCampaign.getSpendCap()));
+ }
+ return map;
+ }
+}
\ No newline at end of file
diff --git a/spring-social-facebook/src/main/java/org/springframework/social/facebook/api/ads/impl/CreativeTemplate.java b/spring-social-facebook/src/main/java/org/springframework/social/facebook/api/ads/impl/CreativeTemplate.java
new file mode 100644
index 000000000..51f06a066
--- /dev/null
+++ b/spring-social-facebook/src/main/java/org/springframework/social/facebook/api/ads/impl/CreativeTemplate.java
@@ -0,0 +1,104 @@
+package org.springframework.social.facebook.api.ads.impl;
+
+import org.springframework.social.facebook.api.GraphApi;
+import org.springframework.social.facebook.api.PagedList;
+import org.springframework.social.facebook.api.ads.AdCreative;
+import org.springframework.social.facebook.api.ads.AdInsight;
+import org.springframework.social.facebook.api.ads.CreativeOperations;
+import org.springframework.social.facebook.api.impl.AbstractFacebookOperations;
+import org.springframework.util.LinkedMultiValueMap;
+import org.springframework.util.MultiValueMap;
+import org.springframework.web.client.RestTemplate;
+
+/**
+ * @author Sebastian Górecki
+ */
+public class CreativeTemplate extends AbstractFacebookOperations implements CreativeOperations {
+
+ private final GraphApi graphApi;
+ private final RestTemplate restTemplate;
+
+
+ public CreativeTemplate(GraphApi graphApi, RestTemplate restTemplate, boolean isAuthorizedForUser) {
+ super(isAuthorizedForUser);
+ this.graphApi = graphApi;
+ this.restTemplate = restTemplate;
+ }
+
+ public PagedList getAccountCreatives(String accountId) {
+ requireAuthorization();
+ return graphApi.fetchConnections(getAdAccountId(accountId), "adcreatives", AdCreative.class, CreativeOperations.AD_CREATIVE_FIELDS);
+ }
+
+ public PagedList getAdSetCreatives(String adSetId) {
+ requireAuthorization();
+ return graphApi.fetchConnections(adSetId, "adcreatives", AdCreative.class, CreativeOperations.AD_CREATIVE_FIELDS);
+ }
+
+ public AdCreative getAdCreative(String creativeId) {
+ requireAuthorization();
+ return graphApi.fetchObject(creativeId, AdCreative.class, CreativeOperations.AD_CREATIVE_FIELDS);
+ }
+
+ public String createAdCreative(String accountId, AdCreative creative) {
+ requireAuthorization();
+ MultiValueMap data = mapCommonFields(creative);
+ return graphApi.publish(getAdAccountId(accountId), "adcreatives", data);
+ }
+
+ public boolean renameAdCreative(String creativeId, String name) {
+ requireAuthorization();
+ MultiValueMap data = new LinkedMultiValueMap();
+ data.add("name", name);
+ return graphApi.update(creativeId, data);
+ }
+
+ public void deleteAdCreative(String creativeId) {
+ requireAuthorization();
+ restTemplate.delete(GraphApi.GRAPH_API_URL + creativeId);
+ }
+
+ private MultiValueMap mapCommonFields(AdCreative creative) {
+ MultiValueMap data = new LinkedMultiValueMap();
+ if (creative.getType() != null) {
+ data.add("object_type", creative.getType().name());
+ }
+ if (creative.getName() != null) {
+ data.add("name", creative.getName());
+ }
+ if (creative.getTitle() != null) {
+ data.add("title", creative.getTitle());
+ }
+ if (creative.getStatus() != null) {
+ data.add("run_status", creative.getStatus().name());
+ }
+ if (creative.getBody() != null) {
+ data.add("body", creative.getBody());
+ }
+ if (creative.getObjectId() != null) {
+ data.add("object_id", creative.getObjectId());
+ }
+ if (creative.getImageHash() != null) {
+ data.add("image_hash", creative.getImageHash());
+ }
+ if (creative.getImageUrl() != null) {
+ data.add("image_url", creative.getImageUrl());
+ }
+ if (creative.getLinkUrl() != null) {
+ data.add("link_url", creative.getLinkUrl());
+ }
+ if (creative.getObjectStoryId() != null) {
+ data.add("object_story_id", creative.getObjectStoryId());
+ }
+ if (creative.getObjectUrl() != null) {
+ data.add("object_url", creative.getObjectUrl());
+ }
+ if (creative.getUrlTags() != null) {
+ data.add("url_tags", creative.getUrlTags());
+ }
+ if (creative.getThumbnailUrl() != null) {
+ data.add("thumbnail_url", creative.getThumbnailUrl());
+ }
+ return data;
+ }
+}
diff --git a/spring-social-facebook/src/main/java/org/springframework/social/facebook/api/ads/impl/FacebookAdsTemplate.java b/spring-social-facebook/src/main/java/org/springframework/social/facebook/api/ads/impl/FacebookAdsTemplate.java
new file mode 100644
index 000000000..df43cb05b
--- /dev/null
+++ b/spring-social-facebook/src/main/java/org/springframework/social/facebook/api/ads/impl/FacebookAdsTemplate.java
@@ -0,0 +1,56 @@
+package org.springframework.social.facebook.api.ads.impl;
+
+import org.springframework.social.facebook.api.ads.*;
+import org.springframework.social.facebook.api.impl.FacebookTemplate;
+
+/**
+ * This is the central class for interacting with Facebook Marketing API.
+ *
+ * @author Sebastian Górecki
+ */
+public class FacebookAdsTemplate extends FacebookTemplate implements FacebookAds {
+
+ private AccountOperations accountOperations;
+ private CampaignOperations campaignOperations;
+ private AdSetOperations adSetOperations;
+ private CreativeOperations creativeOperations;
+ private AdOperations adOperations;
+
+ public FacebookAdsTemplate() {
+ super(null);
+ initSubApis();
+ }
+
+ public FacebookAdsTemplate(String accessToken) {
+ super(accessToken);
+ initSubApis();
+ }
+
+ public AccountOperations accountOperations() {
+ return accountOperations;
+ }
+
+ public CampaignOperations campaignOperations() {
+ return campaignOperations;
+ }
+
+ public AdSetOperations adSetOperations() {
+ return adSetOperations;
+ }
+
+ public CreativeOperations creativeOperations() {
+ return creativeOperations;
+ }
+
+ public AdOperations adOperations() {
+ return adOperations;
+ }
+
+ private void initSubApis() {
+ accountOperations = new AccountTemplate(this, isAuthorized());
+ campaignOperations = new CampaignTemplate(this, isAuthorized());
+ adSetOperations = new AdSetTemplate(this, getJsonMessageConverter().getObjectMapper(), isAuthorized());
+ creativeOperations = new CreativeTemplate(this, getRestTemplate(), isAuthorized());
+ adOperations = new AdTemplate(this, getRestTemplate(), getJsonMessageConverter().getObjectMapper(), isAuthorized());
+ }
+}
diff --git a/spring-social-facebook/src/main/java/org/springframework/social/facebook/api/ads/impl/json/AdAccountGroupMixin.java b/spring-social-facebook/src/main/java/org/springframework/social/facebook/api/ads/impl/json/AdAccountGroupMixin.java
new file mode 100644
index 000000000..40e68e7b7
--- /dev/null
+++ b/spring-social-facebook/src/main/java/org/springframework/social/facebook/api/ads/impl/json/AdAccountGroupMixin.java
@@ -0,0 +1,23 @@
+package org.springframework.social.facebook.api.ads.impl.json;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import org.springframework.social.facebook.api.impl.json.FacebookObjectMixin;
+
+/**
+ * Annotated mixin to add Jackson annotations to AdAccountGroup.
+ *
+ * @author Sebastian Górecki
+ */
+@JsonIgnoreProperties(ignoreUnknown = true)
+public abstract class AdAccountGroupMixin extends FacebookObjectMixin {
+
+ @JsonProperty("account_group_id")
+ String id;
+
+ @JsonProperty("name")
+ String name;
+
+ @JsonProperty("status")
+ int status;
+}
diff --git a/spring-social-facebook/src/main/java/org/springframework/social/facebook/api/ads/impl/json/AdAccountMixin.java b/spring-social-facebook/src/main/java/org/springframework/social/facebook/api/ads/impl/json/AdAccountMixin.java
new file mode 100644
index 000000000..ba5e4925f
--- /dev/null
+++ b/spring-social-facebook/src/main/java/org/springframework/social/facebook/api/ads/impl/json/AdAccountMixin.java
@@ -0,0 +1,199 @@
+package org.springframework.social.facebook.api.ads.impl.json;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.fasterxml.jackson.core.JsonParser;
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.core.JsonToken;
+import com.fasterxml.jackson.core.type.TypeReference;
+import com.fasterxml.jackson.databind.DeserializationContext;
+import com.fasterxml.jackson.databind.JsonDeserializer;
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
+import org.springframework.social.facebook.api.ads.AdAccount.AccountStatus;
+import org.springframework.social.facebook.api.ads.AdAccount.AgencyClientDeclaration;
+import org.springframework.social.facebook.api.ads.AdAccount.Capability;
+import org.springframework.social.facebook.api.ads.AdAccount.TaxStatus;
+import org.springframework.social.facebook.api.ads.AdAccountGroup;
+import org.springframework.social.facebook.api.ads.AdUser;
+import org.springframework.social.facebook.api.impl.json.FacebookModule;
+import org.springframework.social.facebook.api.impl.json.FacebookObjectMixin;
+
+import java.io.IOException;
+import java.util.*;
+
+/**
+ * Annotated mixin to add Jackson annotations to AdAccount.
+ *
+ * @author Sebastian Górecki
+ */
+@JsonIgnoreProperties(ignoreUnknown = true)
+public abstract class AdAccountMixin extends FacebookObjectMixin {
+
+ @JsonProperty("id")
+ String id;
+
+ @JsonProperty("account_groups")
+ List accountGroups;
+
+ @JsonProperty("account_id")
+ long accountId;
+
+ @JsonProperty("account_status")
+ AccountStatus status;
+
+ @JsonProperty("age")
+ int age;
+
+ @JsonProperty("agency_client_declaration")
+ AgencyClientDeclaration agencyClientDeclaration;
+
+ @JsonProperty("amount_spent")
+ String amountSpent;
+
+ @JsonProperty("balance")
+ String balance;
+
+ @JsonProperty("business_city")
+ String businessCity;
+
+ @JsonProperty("business_country_code")
+ String businessCountryCode;
+
+ @JsonProperty("business_name")
+ String businessName;
+
+ @JsonProperty("business_state")
+ String businessState;
+
+ @JsonProperty("business_street")
+ String businessStreet;
+
+ @JsonProperty("business_street2")
+ String businessStreet2;
+
+ @JsonProperty("business_zip")
+ String businessZip;
+
+ @JsonProperty("capabilities")
+ List capabilities;
+
+ @JsonProperty("created_time")
+ Date createdTime;
+
+ @JsonProperty("currency")
+ String currency;
+
+ @JsonProperty("daily_spend_limit")
+ String dailySpendLimit;
+
+ @JsonProperty("end_advertiser")
+ long endAdvertiser;
+
+ @JsonProperty("funding_source")
+ String fundingSource;
+
+ @JsonProperty("funding_source_details")
+ Map fundingSourceDetails;
+
+ @JsonProperty("is_personal")
+ int isPersonal;
+
+ @JsonProperty("media_agency")
+ long mediaAgency;
+
+ @JsonProperty("name")
+ String name;
+
+ @JsonProperty("offsite_pixels_tos_accepted")
+ boolean offsitePixelsTOSAccepted;
+
+ @JsonProperty("partner")
+ long partner;
+
+ @JsonProperty("spend_cap")
+ String spendCap;
+
+ @JsonProperty("timezone_id")
+ int timezoneId;
+
+ @JsonProperty("timezone_name")
+ String timezoneName;
+
+ @JsonProperty("timezone_offset_hours_utc")
+ int timezoneOffsetHoursUTC;
+
+ @JsonProperty("tos_accepted")
+ Map tosAccepted;
+
+ @JsonProperty("users")
+ @JsonDeserialize(using = AdUserListDeserializer.class)
+ List users;
+
+ @JsonProperty("tax_id_status")
+ TaxStatus taxStatus;
+
+ private static class AdUserListDeserializer extends JsonDeserializer> {
+ @SuppressWarnings("unchecked")
+ @Override
+ public List deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException, JsonProcessingException {
+ ObjectMapper mapper = new ObjectMapper();
+ mapper.registerModule(new FacebookModule());
+ jp.setCodec(mapper);
+ if (jp.hasCurrentToken()) {
+ try {
+ JsonNode dataNode = jp.readValueAs(JsonNode.class).get("data");
+ if (dataNode != null) {
+ return (List) mapper.reader(new TypeReference>() {
+ }).readValue(dataNode);
+ }
+ } catch (IOException e) {
+ return Collections.emptyList();
+ }
+ }
+
+ return Collections.emptyList();
+ }
+ }
+
+ @JsonIgnoreProperties(ignoreUnknown = true)
+ public abstract class AgencyClientDeclarationMixin extends FacebookObjectMixin {
+
+ @JsonProperty("agency_representing_client")
+ private int agencyRepresentingClient;
+
+ @JsonProperty("client_based_in_france")
+ private int clientBasedInFrance;
+
+ @JsonProperty("client_city")
+ private String clientCity;
+
+ @JsonProperty("client_country_code")
+ private String clientCountryCode;
+
+ @JsonProperty("client_email_address")
+ private String clientEmailAddress;
+
+ @JsonProperty("client_name")
+ private String clientName;
+
+ @JsonProperty("client_postal_code")
+ private String clientPostalCode;
+
+ @JsonProperty("client_province")
+ private String clientProvince;
+
+ @JsonProperty("client_street")
+ private String clientStreet;
+
+ @JsonProperty("client_street2")
+ private String clientStreet2;
+
+ @JsonProperty("has_written_mandate_from_advertiser")
+ private int hasWrittenMandateFromAdvertiser;
+
+ @JsonProperty("is_client_paying_invoices")
+ private int isClientPayingInvoices;
+ }
+}
diff --git a/spring-social-facebook/src/main/java/org/springframework/social/facebook/api/ads/impl/json/AdCampaignMixin.java b/spring-social-facebook/src/main/java/org/springframework/social/facebook/api/ads/impl/json/AdCampaignMixin.java
new file mode 100644
index 000000000..bca215d6f
--- /dev/null
+++ b/spring-social-facebook/src/main/java/org/springframework/social/facebook/api/ads/impl/json/AdCampaignMixin.java
@@ -0,0 +1,38 @@
+package org.springframework.social.facebook.api.ads.impl.json;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import org.springframework.social.facebook.api.ads.AdCampaign.BuyingType;
+import org.springframework.social.facebook.api.ads.AdCampaign.CampaignObjective;
+import org.springframework.social.facebook.api.ads.AdCampaign.CampaignStatus;
+import org.springframework.social.facebook.api.impl.json.FacebookObjectMixin;
+
+/**
+ * Annotated mixin to add Jackson annotations to AdCampaign.
+ *
+ * @author Sebastian Górecki
+ */
+@JsonIgnoreProperties(ignoreUnknown = true)
+abstract public class AdCampaignMixin extends FacebookObjectMixin {
+
+ @JsonProperty("id")
+ String id;
+
+ @JsonProperty("account_id")
+ String accountId;
+
+ @JsonProperty("buying_type")
+ BuyingType buyingType;
+
+ @JsonProperty("campaign_group_status")
+ CampaignStatus status;
+
+ @JsonProperty("name")
+ String name;
+
+ @JsonProperty("objective")
+ CampaignObjective objective;
+
+ @JsonProperty("spend_cap")
+ int spendCap;
+}
diff --git a/spring-social-facebook/src/main/java/org/springframework/social/facebook/api/ads/impl/json/AdCreativeMixin.java b/spring-social-facebook/src/main/java/org/springframework/social/facebook/api/ads/impl/json/AdCreativeMixin.java
new file mode 100644
index 000000000..e6bba4f6e
--- /dev/null
+++ b/spring-social-facebook/src/main/java/org/springframework/social/facebook/api/ads/impl/json/AdCreativeMixin.java
@@ -0,0 +1,57 @@
+package org.springframework.social.facebook.api.ads.impl.json;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import org.springframework.social.facebook.api.ads.AdCreative;
+import org.springframework.social.facebook.api.impl.json.FacebookObjectMixin;
+
+/**
+ * Annotated mixin to add Jackson annotations to AdCreative.
+ *
+ * @author Sebastian Górecki
+ */
+@JsonIgnoreProperties(ignoreUnknown = true)
+abstract public class AdCreativeMixin extends FacebookObjectMixin {
+
+ @JsonProperty("id")
+ String id;
+
+ @JsonProperty("object_type")
+ AdCreative.AdCreativeType type;
+
+ @JsonProperty("name")
+ String name;
+
+ @JsonProperty("title")
+ String title;
+
+ @JsonProperty("run_status")
+ AdCreative.AdCreativeStatus status;
+
+ @JsonProperty("body")
+ String body;
+
+ @JsonProperty("object_id")
+ String objectId;
+
+ @JsonProperty("image_hash")
+ String imageHash;
+
+ @JsonProperty("image_url")
+ String imageUrl;
+
+ @JsonProperty("link_url")
+ String linkUrl;
+
+ @JsonProperty("object_story_id")
+ String objectStoryId;
+
+ @JsonProperty("object_url")
+ String objectUrl;
+
+ @JsonProperty("url_tags")
+ String urlTags;
+
+ @JsonProperty("thumbnail_url")
+ String thumbnailUrl;
+}
diff --git a/spring-social-facebook/src/main/java/org/springframework/social/facebook/api/ads/impl/json/AdInsightActionMixin.java b/spring-social-facebook/src/main/java/org/springframework/social/facebook/api/ads/impl/json/AdInsightActionMixin.java
new file mode 100644
index 000000000..f0215ab0d
--- /dev/null
+++ b/spring-social-facebook/src/main/java/org/springframework/social/facebook/api/ads/impl/json/AdInsightActionMixin.java
@@ -0,0 +1,19 @@
+package org.springframework.social.facebook.api.ads.impl.json;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import org.springframework.social.facebook.api.impl.json.FacebookObjectMixin;
+
+/**
+ * Annotated mixin to add Jackson annotations to AdInsightAction.
+ *
+ * @author Sebastian Górecki
+ */
+@JsonIgnoreProperties(ignoreUnknown = true)
+public abstract class AdInsightActionMixin extends FacebookObjectMixin {
+ @JsonProperty("action_type")
+ String actionType;
+
+ @JsonProperty("value")
+ double value;
+}
diff --git a/spring-social-facebook/src/main/java/org/springframework/social/facebook/api/ads/impl/json/AdInsightMixin.java b/spring-social-facebook/src/main/java/org/springframework/social/facebook/api/ads/impl/json/AdInsightMixin.java
new file mode 100644
index 000000000..ab9c633c9
--- /dev/null
+++ b/spring-social-facebook/src/main/java/org/springframework/social/facebook/api/ads/impl/json/AdInsightMixin.java
@@ -0,0 +1,160 @@
+package org.springframework.social.facebook.api.ads.impl.json;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import org.springframework.social.facebook.api.ads.AdInsightAction;
+import org.springframework.social.facebook.api.impl.json.FacebookObjectMixin;
+
+import java.util.Date;
+import java.util.List;
+
+/**
+ * Annotated mixin to add Jackson annotations to AdInsight.
+ *
+ * @author Sebastian Górecki
+ */
+@JsonIgnoreProperties(ignoreUnknown = true)
+public abstract class AdInsightMixin extends FacebookObjectMixin {
+ // id fields
+ @JsonProperty("account_id")
+ String accountId;
+
+ @JsonProperty("adgroup_id")
+ String adGroupId;
+
+ @JsonProperty("campaign_id")
+ String campaignId;
+
+ @JsonProperty("campaign_group_id")
+ String campaignGroupId;
+
+ // name fields
+ @JsonProperty("account_name")
+ String accountName;
+
+ @JsonProperty("adgroup_name")
+ String adGroupName;
+
+ @JsonProperty("campaign_group_name")
+ String campaignGroupName;
+
+ @JsonProperty("campaign_name")
+ String camapignName;
+
+ // date fields
+ @JsonProperty("date_start")
+ Date dateStart;
+
+ @JsonProperty("date_stop")
+ Date dateStop;
+
+ @JsonProperty("campaign_start")
+ Date campaignStart;
+
+ @JsonProperty("campaign_end")
+ Date campaignEnd;
+
+ @JsonProperty("campaign_group_end")
+ Date campaignGroupEnd;
+
+ // general fields
+ @JsonProperty("actions_per_impression")
+ double actionsPerImpression;
+
+ @JsonProperty("clicks")
+ int clicks;
+
+ @JsonProperty("unique_clicks")
+ int uniqueClicks;
+
+ @JsonProperty("cost_per_result")
+ double costPerResult;
+
+ @JsonProperty("cost_per_total_action")
+ double costPerTotalAction;
+
+ @JsonProperty("cpc")
+ double costPerClick;
+
+ @JsonProperty("cost_per_unique_click")
+ double costPerUniqueClick;
+
+ @JsonProperty("cpm")
+ double cpm;
+
+ @JsonProperty("cpp")
+ double cpp;
+
+ @JsonProperty("ctr")
+ double ctr;
+
+ @JsonProperty("unique_ctr")
+ double uniqueCtr;
+
+ @JsonProperty("frequency")
+ double frequency;
+
+ @JsonProperty("impressions")
+ int impressions;
+
+ @JsonProperty("unique_impressions")
+ int uniqueImpressions;
+
+ @JsonProperty("objective")
+ String objective;
+
+ @JsonProperty("reach")
+ int reach;
+
+ @JsonProperty("result_rate")
+ double resultRate;
+
+ @JsonProperty("results")
+ int results;
+
+ @JsonProperty("roas")
+ int roas;
+
+ @JsonProperty("social_clicks")
+ int socialClicks;
+
+ @JsonProperty("unique_social_clicks")
+ int uniqueSocialClicks;
+
+ @JsonProperty("social_impressions")
+ int socialImpressions;
+
+ @JsonProperty("unique_social_impressions")
+ int uniqueSocialImpressions;
+
+ @JsonProperty("social_reach")
+ int socialReach;
+
+ @JsonProperty("spend")
+ int spend;
+
+ @JsonProperty("today_spend")
+ int todaySpend;
+
+ @JsonProperty("total_action_value")
+ int totalActionValue;
+
+ @JsonProperty("total_actions")
+ int totalActions;
+
+ @JsonProperty("total_unique_actions")
+ int totalUniqueActions;
+
+ // action and video fields
+ @JsonProperty("actions")
+ List actions;
+
+ @JsonProperty("unique_actions")
+ List uniqueActions;
+
+ @JsonProperty("cost_per_action_type")
+ List costPerActionType;
+
+ @JsonProperty("video_start_actions")
+ List videoStartActions;
+}
\ No newline at end of file
diff --git a/spring-social-facebook/src/main/java/org/springframework/social/facebook/api/ads/impl/json/AdMixin.java b/spring-social-facebook/src/main/java/org/springframework/social/facebook/api/ads/impl/json/AdMixin.java
new file mode 100644
index 000000000..4a0c589a7
--- /dev/null
+++ b/spring-social-facebook/src/main/java/org/springframework/social/facebook/api/ads/impl/json/AdMixin.java
@@ -0,0 +1,81 @@
+package org.springframework.social.facebook.api.ads.impl.json;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.fasterxml.jackson.core.JsonParser;
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.core.JsonToken;
+import com.fasterxml.jackson.databind.DeserializationContext;
+import com.fasterxml.jackson.databind.JsonDeserializer;
+import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
+import org.springframework.social.facebook.api.ads.Ad;
+import org.springframework.social.facebook.api.ads.BidInfo;
+import org.springframework.social.facebook.api.ads.BidType;
+import org.springframework.social.facebook.api.ads.Targeting;
+
+import java.io.IOException;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Annotated mixin to add Jackson annotations to Ad.
+ *
+ * @author Sebastian Górecki
+ */
+@JsonIgnoreProperties(ignoreUnknown = true)
+public class AdMixin {
+
+ @JsonProperty("id")
+ String id;
+
+ @JsonProperty("adgroup_status")
+ Ad.AdStatus status;
+
+ @JsonProperty("name")
+ String name;
+
+ @JsonProperty("bid_type")
+ BidType bidType;
+
+ @JsonProperty("bid_info")
+ BidInfo bidInfo;
+
+ @JsonProperty("account_id")
+ String accountId;
+
+ @JsonProperty("campaign_id")
+ String adSetId;
+
+ @JsonProperty("campaign_group_id")
+ String campaignId;
+
+ @JsonProperty("creative")
+ @JsonDeserialize(using = CreativeIdDeserializer.class)
+ String creativeId;
+
+ @JsonProperty("targeting")
+ Targeting targeting;
+
+ @JsonProperty("created_time")
+ Date createdTime;
+
+ @JsonProperty("updated_time")
+ Date updatedTime;
+
+ private static class CreativeIdDeserializer extends JsonDeserializer {
+ @SuppressWarnings("unchecked")
+ @Override
+ public String deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException, JsonProcessingException {
+ if (jp.getCurrentToken() == JsonToken.START_OBJECT) {
+ try {
+ Map map = jp.readValueAs(HashMap.class);
+ return map.get("id");
+ } catch (IOException e) {
+ return null;
+ }
+ }
+ return null;
+ }
+ }
+}
diff --git a/spring-social-facebook/src/main/java/org/springframework/social/facebook/api/ads/impl/json/AdSetMixin.java b/spring-social-facebook/src/main/java/org/springframework/social/facebook/api/ads/impl/json/AdSetMixin.java
new file mode 100644
index 000000000..55681508c
--- /dev/null
+++ b/spring-social-facebook/src/main/java/org/springframework/social/facebook/api/ads/impl/json/AdSetMixin.java
@@ -0,0 +1,75 @@
+package org.springframework.social.facebook.api.ads.impl.json;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import org.springframework.social.facebook.api.ads.AdSet.AdSetStatus;
+import org.springframework.social.facebook.api.ads.BidInfo;
+import org.springframework.social.facebook.api.ads.BidType;
+import org.springframework.social.facebook.api.ads.PromotedObject;
+import org.springframework.social.facebook.api.ads.Targeting;
+import org.springframework.social.facebook.api.impl.json.FacebookObjectMixin;
+
+import java.util.Date;
+import java.util.List;
+
+/**
+ * Annotated mixin to add Jackson annotations to AdSet.
+ *
+ * @author Sebastian Górecki
+ */
+@JsonIgnoreProperties(ignoreUnknown = true)
+abstract public class AdSetMixin extends FacebookObjectMixin {
+ @JsonProperty("id")
+ String id;
+
+ @JsonProperty("account_id")
+ String accountId;
+
+ @JsonProperty("campaign_group_id")
+ String campaignId;
+
+ @JsonProperty("name")
+ String name;
+
+ @JsonProperty("campaign_status")
+ AdSetStatus status;
+
+ @JsonProperty("is_autobid")
+ boolean autobid;
+
+ @JsonProperty("bid_info")
+ BidInfo bidInfo;
+
+ @JsonProperty("bid_type")
+ BidType bidType;
+
+ @JsonProperty("budget_remaining")
+ int budgetRemaining;
+
+ @JsonProperty("daily_budget")
+ int dailyBudget;
+
+ @JsonProperty("lifetime_budget")
+ int lifetimeBudget;
+
+ @JsonProperty("creative_sequence")
+ List creativeSequence;
+
+ @JsonProperty("promoted_object")
+ PromotedObject promotedObject;
+
+ @JsonProperty("targeting")
+ Targeting targeting;
+
+ @JsonProperty("start_time")
+ Date startTime;
+
+ @JsonProperty("end_time")
+ Date endTime;
+
+ @JsonProperty("created_time")
+ Date createdTime;
+
+ @JsonProperty("updated_time")
+ Date updatedTime;
+}
diff --git a/spring-social-facebook/src/main/java/org/springframework/social/facebook/api/ads/impl/json/AdUserMixin.java b/spring-social-facebook/src/main/java/org/springframework/social/facebook/api/ads/impl/json/AdUserMixin.java
new file mode 100644
index 000000000..34007dff6
--- /dev/null
+++ b/spring-social-facebook/src/main/java/org/springframework/social/facebook/api/ads/impl/json/AdUserMixin.java
@@ -0,0 +1,29 @@
+package org.springframework.social.facebook.api.ads.impl.json;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import org.springframework.social.facebook.api.ads.AdUser.AdUserPermission;
+import org.springframework.social.facebook.api.ads.AdUser.AdUserRole;
+import org.springframework.social.facebook.api.impl.json.FacebookObjectMixin;
+
+import java.util.List;
+
+/**
+ * Annotated mixin to add Jackson annotations to AdUser.
+ *
+ * @author Sebastian Górecki
+ */
+@JsonIgnoreProperties(ignoreUnknown = true)
+public abstract class AdUserMixin extends FacebookObjectMixin {
+ @JsonProperty("id")
+ String id;
+
+ @JsonProperty("name")
+ String name;
+
+ @JsonProperty("permissions")
+ List permissions;
+
+ @JsonProperty("role")
+ AdUserRole role;
+}
diff --git a/spring-social-facebook/src/main/java/org/springframework/social/facebook/api/ads/impl/json/TargetingCityEntryMixin.java b/spring-social-facebook/src/main/java/org/springframework/social/facebook/api/ads/impl/json/TargetingCityEntryMixin.java
new file mode 100644
index 000000000..0587d5230
--- /dev/null
+++ b/spring-social-facebook/src/main/java/org/springframework/social/facebook/api/ads/impl/json/TargetingCityEntryMixin.java
@@ -0,0 +1,22 @@
+package org.springframework.social.facebook.api.ads.impl.json;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import org.springframework.social.facebook.api.impl.json.FacebookObjectMixin;
+
+/**
+ * Annotated mixin to add Jackson annotations to TargetingCityEntry.
+ *
+ * @author Sebastian Górecki
+ */
+@JsonIgnoreProperties(ignoreUnknown = true)
+public abstract class TargetingCityEntryMixin extends FacebookObjectMixin {
+ @JsonProperty("key")
+ String key;
+
+ @JsonProperty("radius")
+ int radius;
+
+ @JsonProperty("distance_unit")
+ String distanceUnit;
+}
diff --git a/spring-social-facebook/src/main/java/org/springframework/social/facebook/api/ads/impl/json/TargetingEntryMixin.java b/spring-social-facebook/src/main/java/org/springframework/social/facebook/api/ads/impl/json/TargetingEntryMixin.java
new file mode 100644
index 000000000..cad5a8bb8
--- /dev/null
+++ b/spring-social-facebook/src/main/java/org/springframework/social/facebook/api/ads/impl/json/TargetingEntryMixin.java
@@ -0,0 +1,17 @@
+package org.springframework.social.facebook.api.ads.impl.json;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import org.springframework.social.facebook.api.impl.json.FacebookObjectMixin;
+
+/**
+ * Annotated mixin to add Jackson annotations to TargetingEntry.
+ *
+ * @author Sebastian Górecki
+ */
+public abstract class TargetingEntryMixin extends FacebookObjectMixin {
+ @JsonProperty("id")
+ long id;
+
+ @JsonProperty("name")
+ String name;
+}
diff --git a/spring-social-facebook/src/main/java/org/springframework/social/facebook/api/ads/impl/json/TargetingLocationMixin.java b/spring-social-facebook/src/main/java/org/springframework/social/facebook/api/ads/impl/json/TargetingLocationMixin.java
new file mode 100644
index 000000000..a81dbafe9
--- /dev/null
+++ b/spring-social-facebook/src/main/java/org/springframework/social/facebook/api/ads/impl/json/TargetingLocationMixin.java
@@ -0,0 +1,66 @@
+package org.springframework.social.facebook.api.ads.impl.json;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.fasterxml.jackson.core.JsonParser;
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.core.JsonToken;
+import com.fasterxml.jackson.databind.DeserializationContext;
+import com.fasterxml.jackson.databind.JsonDeserializer;
+import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
+import com.fasterxml.jackson.databind.annotation.JsonSerialize;
+import org.springframework.social.facebook.api.ads.TargetingCityEntry;
+import org.springframework.social.facebook.api.ads.TargetingLocation.LocationType;
+import org.springframework.social.facebook.api.impl.json.FacebookObjectMixin;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+
+/**
+ * Annotated mixin to add Jackson annotations to TargetingLocation.
+ *
+ * @author Sebastian Górecki
+ */
+@JsonIgnoreProperties(ignoreUnknown = true)
+@JsonSerialize(using = TargetingLocationSerializer.class)
+public abstract class TargetingLocationMixin extends FacebookObjectMixin {
+ @JsonProperty("countries")
+ List countries;
+
+ @JsonProperty("regions")
+ @JsonDeserialize(using = ListOfMapsDeserializer.class)
+ List regions;
+
+ @JsonProperty("cities")
+ List cities;
+
+ @JsonProperty("zips")
+ @JsonDeserialize(using = ListOfMapsDeserializer.class)
+ List zips;
+
+ @JsonProperty("location_types")
+ List locationTypes;
+
+ private static class ListOfMapsDeserializer extends JsonDeserializer> {
+ @SuppressWarnings("unchecked")
+ @Override
+ public List deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException, JsonProcessingException {
+ if (jp.getCurrentToken() == JsonToken.START_ARRAY) {
+ List retList = new ArrayList();
+ try {
+ while (jp.nextToken() != JsonToken.END_ARRAY) {
+ HashMap regionMap = jp.readValueAs(HashMap.class);
+ retList.add(regionMap.get("key"));
+ }
+ return retList;
+ } catch (IOException e) {
+ return Collections.emptyList();
+ }
+ }
+ return Collections.emptyList();
+ }
+ }
+}
diff --git a/spring-social-facebook/src/main/java/org/springframework/social/facebook/api/ads/impl/json/TargetingLocationSerializer.java b/spring-social-facebook/src/main/java/org/springframework/social/facebook/api/ads/impl/json/TargetingLocationSerializer.java
new file mode 100644
index 000000000..99b748146
--- /dev/null
+++ b/spring-social-facebook/src/main/java/org/springframework/social/facebook/api/ads/impl/json/TargetingLocationSerializer.java
@@ -0,0 +1,49 @@
+package org.springframework.social.facebook.api.ads.impl.json;
+
+import com.fasterxml.jackson.core.JsonGenerator;
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.JsonSerializer;
+import com.fasterxml.jackson.databind.SerializerProvider;
+import org.springframework.social.facebook.api.ads.TargetingLocation;
+
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * @author Sebastian Górecki
+ */
+public class TargetingLocationSerializer extends JsonSerializer {
+ @Override
+ public void serialize(TargetingLocation location, JsonGenerator jgen, SerializerProvider provider) throws IOException, JsonProcessingException {
+ jgen.writeStartObject();
+ if (location.getCountries() != null) {
+ jgen.writeObjectField("countries", location.getCountries());
+ }
+ if (location.getRegions() != null) {
+ serializeLocationValueEntries("regions", location.getRegions(), jgen);
+ }
+ if (location.getCities() != null) {
+ jgen.writeObjectField("cities", location.getCities());
+ }
+ if (location.getZips() != null) {
+ serializeLocationValueEntries("zips", location.getZips(), jgen);
+ }
+ if (location.getLocationTypes() != null) {
+ jgen.writeObjectField("location_types", location.getLocationTypes());
+ }
+ jgen.writeEndObject();
+ }
+
+ private void serializeLocationValueEntries(String entryName, List entries, JsonGenerator jgen) throws IOException {
+ jgen.writeFieldName(entryName);
+ jgen.writeStartArray();
+ for (String entry : entries) {
+ Map map = new HashMap();
+ map.put("key", entry);
+ jgen.writeObject(map);
+ }
+ jgen.writeEndArray();
+ }
+}
diff --git a/spring-social-facebook/src/main/java/org/springframework/social/facebook/api/ads/impl/json/TargetingMixin.java b/spring-social-facebook/src/main/java/org/springframework/social/facebook/api/ads/impl/json/TargetingMixin.java
new file mode 100644
index 000000000..87cb8d08a
--- /dev/null
+++ b/spring-social-facebook/src/main/java/org/springframework/social/facebook/api/ads/impl/json/TargetingMixin.java
@@ -0,0 +1,84 @@
+package org.springframework.social.facebook.api.ads.impl.json;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.fasterxml.jackson.databind.annotation.JsonSerialize;
+import org.springframework.social.facebook.api.ads.Targeting.*;
+import org.springframework.social.facebook.api.ads.TargetingEntry;
+import org.springframework.social.facebook.api.ads.TargetingLocation;
+import org.springframework.social.facebook.api.impl.json.FacebookObjectMixin;
+
+import java.util.List;
+
+/**
+ * Annotated mixin to add Jackson annotations to Targeting.
+ *
+ * @author Sebastian Górecki
+ */
+@JsonIgnoreProperties(ignoreUnknown = true)
+@JsonSerialize(using = TargetingSerializer.class)
+public abstract class TargetingMixin extends FacebookObjectMixin {
+ // demographics
+ @JsonProperty("genders")
+ List genders;
+
+ @JsonProperty("age_min")
+ Integer ageMin;
+
+ @JsonProperty("age_max")
+ Integer ageMax;
+
+ @JsonProperty("relationship_statuses")
+ List relationshipStatuses;
+
+ @JsonProperty("interested_in")
+ List interestedIn;
+
+ // location
+ @JsonProperty("geo_locations")
+ TargetingLocation geoLocations;
+
+ @JsonProperty("excluded_geo_locations")
+ TargetingLocation excludedGeoLocations;
+
+ // placement
+ @JsonProperty("page_types")
+ List pageTypes;
+
+ // connections
+ @JsonProperty("connections")
+ List connections;
+
+ @JsonProperty("excluded_connections")
+ List excludedConnections;
+
+ @JsonProperty("friends_of_connections")
+ List friendsOfConnections;
+
+ // interests
+ @JsonProperty("interests")
+ List interests;
+
+ // behaviors
+ @JsonProperty("behaviors")
+ List behaviors;
+
+ // education and workplace
+ @JsonProperty("education_schools")
+ List educationSchools;
+
+ @JsonProperty("education_statuses")
+ List educationStatuses;
+
+ @JsonProperty("college_years")
+ List collegeYears;
+
+ @JsonProperty("education_majors")
+ List educationMajors;
+
+ @JsonProperty("work_employers")
+ List workEmployers;
+
+ @JsonProperty("work_positions")
+ List workPositions;
+}
diff --git a/spring-social-facebook/src/main/java/org/springframework/social/facebook/api/ads/impl/json/TargetingSerializer.java b/spring-social-facebook/src/main/java/org/springframework/social/facebook/api/ads/impl/json/TargetingSerializer.java
new file mode 100644
index 000000000..88ba845ea
--- /dev/null
+++ b/spring-social-facebook/src/main/java/org/springframework/social/facebook/api/ads/impl/json/TargetingSerializer.java
@@ -0,0 +1,79 @@
+package org.springframework.social.facebook.api.ads.impl.json;
+
+import com.fasterxml.jackson.core.JsonGenerator;
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.JsonSerializer;
+import com.fasterxml.jackson.databind.SerializerProvider;
+import org.springframework.social.facebook.api.ads.Targeting;
+
+import java.io.IOException;
+
+/**
+ * @author Sebastian Górecki
+ */
+public class TargetingSerializer extends JsonSerializer {
+ @Override
+ public void serialize(Targeting targeting, JsonGenerator jgen, SerializerProvider provider) throws IOException, JsonProcessingException {
+ jgen.writeStartObject();
+
+ if (targeting.getGenders() != null) {
+ jgen.writeObjectField("genders", targeting.getGenders());
+ }
+ if (targeting.getAgeMin() != null) {
+ jgen.writeObjectField("age_min", targeting.getAgeMin());
+ }
+ if (targeting.getAgeMax() != null) {
+ jgen.writeObjectField("age_max", targeting.getAgeMax());
+ }
+ if (targeting.getRelationshipStatuses() != null) {
+ jgen.writeObjectField("relationship_statuses", targeting.getRelationshipStatuses());
+ }
+ if (targeting.getInterestedIn() != null) {
+ jgen.writeObjectField("interested_in", targeting.getInterestedIn());
+ }
+ if (targeting.getGeoLocations() != null) {
+ jgen.writeObjectField("geo_locations", targeting.getGeoLocations());
+ }
+ if (targeting.getExcludedGeoLocations() != null) {
+ jgen.writeObjectField("excluded_geo_locations", targeting.getExcludedGeoLocations());
+ }
+ if (targeting.getPageTypes() != null) {
+ jgen.writeObjectField("page_types", targeting.getPageTypes());
+ }
+ if (targeting.getConnections() != null) {
+ jgen.writeObjectField("connections", targeting.getConnections());
+ }
+ if (targeting.getExcludedConnections() != null) {
+ jgen.writeObjectField("excluded_connections", targeting.getExcludedConnections());
+ }
+ if (targeting.getFriendsOfConnections() != null) {
+ jgen.writeObjectField("friends_of_connections", targeting.getFriendsOfConnections());
+ }
+ if (targeting.getInterests() != null) {
+ jgen.writeObjectField("interests", targeting.getInterests());
+ }
+ if (targeting.getBehaviors() != null) {
+ jgen.writeObjectField("behaviors", targeting.getBehaviors());
+ }
+ if (targeting.getEducationSchools() != null) {
+ jgen.writeObjectField("education_schools", targeting.getEducationSchools());
+ }
+ if (targeting.getEducationStatuses() != null) {
+ jgen.writeObjectField("education_statuses", targeting.getEducationStatuses());
+ }
+ if (targeting.getCollegeYears() != null) {
+ jgen.writeObjectField("college_years", targeting.getCollegeYears());
+ }
+ if (targeting.getEducationMajors() != null) {
+ jgen.writeObjectField("education_majors", targeting.getEducationMajors());
+ }
+ if (targeting.getWorkEmployers() != null) {
+ jgen.writeObjectField("work_employers", targeting.getWorkEmployers());
+ }
+ if (targeting.getWorkPositions() != null) {
+ jgen.writeObjectField("work_positions", targeting.getWorkPositions());
+ }
+
+ jgen.writeEndObject();
+ }
+}
diff --git a/spring-social-facebook/src/main/java/org/springframework/social/facebook/api/impl/AbstractFacebookOperations.java b/spring-social-facebook/src/main/java/org/springframework/social/facebook/api/impl/AbstractFacebookOperations.java
index e2d7816e6..c28619880 100644
--- a/spring-social-facebook/src/main/java/org/springframework/social/facebook/api/impl/AbstractFacebookOperations.java
+++ b/spring-social-facebook/src/main/java/org/springframework/social/facebook/api/impl/AbstractFacebookOperations.java
@@ -17,18 +17,27 @@
import org.springframework.social.MissingAuthorizationException;
-class AbstractFacebookOperations {
+import java.util.Date;
+
+public class AbstractFacebookOperations {
private final boolean isAuthorized;
public AbstractFacebookOperations(boolean isAuthorized) {
this.isAuthorized = isAuthorized;
}
-
+
+ public String getAdAccountId(String id) {
+ return "act_" + id;
+ }
+
protected void requireAuthorization() {
if (!isAuthorized) {
throw new MissingAuthorizationException("facebook");
}
}
-
+
+ public String getUnixTime(Date date) {
+ return date != null ? String.valueOf(date.getTime() / 1000L) : "";
+ }
}
diff --git a/spring-social-facebook/src/main/java/org/springframework/social/facebook/api/impl/FacebookErrorHandler.java b/spring-social-facebook/src/main/java/org/springframework/social/facebook/api/impl/FacebookErrorHandler.java
index fc695c500..a6963c056 100644
--- a/spring-social-facebook/src/main/java/org/springframework/social/facebook/api/impl/FacebookErrorHandler.java
+++ b/spring-social-facebook/src/main/java/org/springframework/social/facebook/api/impl/FacebookErrorHandler.java
@@ -36,6 +36,8 @@
import org.springframework.social.ServerException;
import org.springframework.social.UncategorizedApiException;
import org.springframework.social.facebook.api.FacebookError;
+import org.springframework.social.facebook.api.InvalidParameterException;
+import org.springframework.social.facebook.api.ads.InvalidCampaignStatusException;
import org.springframework.web.client.DefaultResponseErrorHandler;
import com.fasterxml.jackson.core.JsonFactory;
@@ -71,7 +73,9 @@ void handleFacebookError(HttpStatus statusCode, FacebookError error) {
throw new UncategorizedApiException(FACEBOOK_PROVIDER_ID, error.getMessage(), null);
} else if (code == SERVICE) {
throw new ServerException(FACEBOOK_PROVIDER_ID, error.getMessage());
- } else if (code == TOO_MANY_CALLS || code == USER_TOO_MANY_CALLS || code == EDIT_FEED_TOO_MANY_USER_CALLS || code == EDIT_FEED_TOO_MANY_USER_ACTION_CALLS) {
+ } else if (code == TOO_MANY_CALLS || code == USER_TOO_MANY_CALLS || code == EDIT_FEED_TOO_MANY_USER_CALLS ||
+ code == EDIT_FEED_TOO_MANY_USER_ACTION_CALLS || code == USER_APP_TOO_MANY_CALLS ||
+ code == AD_CREATION_LIMIT_EXCEEDED) {
throw new RateLimitExceededException(FACEBOOK_PROVIDER_ID);
} else if (code == PERMISSION_DENIED || isUserPermissionError(code)) {
throw new InsufficientPermissionException(FACEBOOK_PROVIDER_ID);
@@ -87,6 +91,10 @@ void handleFacebookError(HttpStatus statusCode, FacebookError error) {
throw new DuplicateStatusException(FACEBOOK_PROVIDER_ID, error.getMessage());
} else if (code == DATA_OBJECT_NOT_FOUND || code == PATH_UNKNOWN) {
throw new ResourceNotFoundException(FACEBOOK_PROVIDER_ID, error.getMessage());
+ } else if (code == PARAM && error.getSubcode() != null && error.getSubcode() == 1487564) {
+ throw new InvalidCampaignStatusException(FACEBOOK_PROVIDER_ID, error.getUserMessage());
+ } else if (code == PARAM && error.getSubcode() != null) {
+ throw new InvalidParameterException(FACEBOOK_PROVIDER_ID, error.getUserMessage());
} else {
throw new UncategorizedApiException(FACEBOOK_PROVIDER_ID, error.getMessage(), null);
}
diff --git a/spring-social-facebook/src/main/java/org/springframework/social/facebook/api/impl/FacebookTemplate.java b/spring-social-facebook/src/main/java/org/springframework/social/facebook/api/impl/FacebookTemplate.java
index 3cca6c86c..fb44b1d83 100644
--- a/spring-social-facebook/src/main/java/org/springframework/social/facebook/api/impl/FacebookTemplate.java
+++ b/spring-social-facebook/src/main/java/org/springframework/social/facebook/api/impl/FacebookTemplate.java
@@ -15,39 +15,16 @@
*/
package org.springframework.social.facebook.api.impl;
-import static org.springframework.social.facebook.api.impl.PagedListUtils.*;
-
-import java.io.IOException;
-import java.net.URI;
-import java.util.List;
-import java.util.Map;
-
-import org.springframework.http.HttpEntity;
-import org.springframework.http.HttpHeaders;
-import org.springframework.http.HttpMethod;
-import org.springframework.http.HttpStatus;
-import org.springframework.http.ResponseEntity;
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.type.CollectionType;
+import com.fasterxml.jackson.databind.type.TypeFactory;
+import org.springframework.http.*;
import org.springframework.http.client.ClientHttpRequestFactory;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.social.NotAuthorizedException;
import org.springframework.social.UncategorizedApiException;
-import org.springframework.social.facebook.api.AchievementOperations;
-import org.springframework.social.facebook.api.CommentOperations;
-import org.springframework.social.facebook.api.EventOperations;
-import org.springframework.social.facebook.api.Facebook;
-import org.springframework.social.facebook.api.FeedOperations;
-import org.springframework.social.facebook.api.FriendOperations;
-import org.springframework.social.facebook.api.GroupOperations;
-import org.springframework.social.facebook.api.ImageType;
-import org.springframework.social.facebook.api.LikeOperations;
-import org.springframework.social.facebook.api.MediaOperations;
-import org.springframework.social.facebook.api.OpenGraphOperations;
-import org.springframework.social.facebook.api.PageOperations;
-import org.springframework.social.facebook.api.PagedList;
-import org.springframework.social.facebook.api.PagingParameters;
-import org.springframework.social.facebook.api.SocialContextOperations;
-import org.springframework.social.facebook.api.TestUserOperations;
-import org.springframework.social.facebook.api.UserOperations;
+import org.springframework.social.facebook.api.*;
import org.springframework.social.facebook.api.impl.json.FacebookModule;
import org.springframework.social.oauth2.AbstractOAuth2ApiBinding;
import org.springframework.social.oauth2.OAuth2Version;
@@ -58,10 +35,12 @@
import org.springframework.web.client.RestOperations;
import org.springframework.web.client.RestTemplate;
-import com.fasterxml.jackson.databind.JsonNode;
-import com.fasterxml.jackson.databind.ObjectMapper;
-import com.fasterxml.jackson.databind.type.CollectionType;
-import com.fasterxml.jackson.databind.type.TypeFactory;
+import java.io.IOException;
+import java.net.URI;
+import java.util.List;
+import java.util.Map;
+
+import static org.springframework.social.facebook.api.impl.PagedListUtils.getPagedListParameters;
/**
* This is the central class for interacting with Facebook.
@@ -72,38 +51,39 @@
* Attempts to perform secured operations through such an instance, however,
* will result in {@link NotAuthorizedException} being thrown.
*
+ *
* @author Craig Walls
*/
public class FacebookTemplate extends AbstractOAuth2ApiBinding implements Facebook {
private String appId;
-
+
private AchievementOperations achievementOperations;
-
+
private UserOperations userOperations;
-
+
private FriendOperations friendOperations;
-
+
private FeedOperations feedOperations;
-
+
private GroupOperations groupOperations;
-
+
private CommentOperations commentOperations;
-
+
private LikeOperations likeOperations;
-
+
private EventOperations eventOperations;
-
+
private MediaOperations mediaOperations;
-
+
private PageOperations pageOperations;
-
+
private OpenGraphOperations openGraphOperations;
-
+
private SocialContextOperations socialContextOperations;
-
+
private TestUserOperations testUserOperations;
-
+
private ObjectMapper objectMapper;
private String applicationNamespace;
@@ -111,6 +91,7 @@ public class FacebookTemplate extends AbstractOAuth2ApiBinding implements Facebo
/**
* Create a new instance of FacebookTemplate.
* This constructor creates the FacebookTemplate using a given access token.
+ *
* @param accessToken An access token given by Facebook after a successful OAuth 2 authentication (or through Facebook's JS library).
*/
public FacebookTemplate(String accessToken) {
@@ -120,14 +101,14 @@ public FacebookTemplate(String accessToken) {
public FacebookTemplate(String accessToken, String applicationNamespace) {
this(accessToken, applicationNamespace, null);
}
-
+
public FacebookTemplate(String accessToken, String applicationNamespace, String appId) {
super(accessToken);
this.applicationNamespace = applicationNamespace;
this.appId = appId;
initialize();
}
-
+
@Override
public void setRequestFactory(ClientHttpRequestFactory requestFactory) {
// Wrap the request factory with a BufferingClientHttpRequestFactory so that the error handler can do repeat reads on the response.getBody()
@@ -137,11 +118,11 @@ public void setRequestFactory(ClientHttpRequestFactory requestFactory) {
public AchievementOperations achievementOperations() {
return achievementOperations;
}
-
+
public UserOperations userOperations() {
return userOperations;
}
-
+
public LikeOperations likeOperations() {
return likeOperations;
}
@@ -149,11 +130,11 @@ public LikeOperations likeOperations() {
public FriendOperations friendOperations() {
return friendOperations;
}
-
+
public FeedOperations feedOperations() {
return feedOperations;
}
-
+
public GroupOperations groupOperations() {
return groupOperations;
}
@@ -161,39 +142,39 @@ public GroupOperations groupOperations() {
public CommentOperations commentOperations() {
return commentOperations;
}
-
+
public EventOperations eventOperations() {
return eventOperations;
}
-
+
public MediaOperations mediaOperations() {
return mediaOperations;
}
-
+
public PageOperations pageOperations() {
return pageOperations;
}
-
+
public RestOperations restOperations() {
return getRestTemplate();
}
-
+
public OpenGraphOperations openGraphOperations() {
return openGraphOperations;
}
-
+
public SocialContextOperations socialContextOperations() {
return socialContextOperations;
}
-
+
public String getApplicationNamespace() {
return applicationNamespace;
}
-
+
public TestUserOperations testUserOperations() {
return testUserOperations;
}
-
+
// low-level Graph API operations
public T fetchObject(String objectId, Class type) {
URI uri = URIBuilder.fromUri(GRAPH_API_URL + objectId).build();
@@ -202,10 +183,10 @@ public T fetchObject(String objectId, Class type) {
public T fetchObject(String objectId, Class type, String... fields) {
MultiValueMap queryParameters = new LinkedMultiValueMap();
- if(fields.length > 0) {
+ if (fields.length > 0) {
String joinedFields = join(fields);
queryParameters.set("fields", joinedFields);
- }
+ }
return fetchObject(objectId, type, queryParameters);
}
@@ -216,10 +197,10 @@ public T fetchObject(String objectId, Class type, MultiValueMap PagedList fetchConnections(String objectId, String connectionType, Class type, String... fields) {
MultiValueMap queryParameters = new LinkedMultiValueMap();
- if(fields.length > 0) {
+ if (fields.length > 0) {
String joinedFields = join(fields);
queryParameters.set("fields", joinedFields);
- }
+ }
return fetchConnections(objectId, connectionType, type, queryParameters);
}
@@ -238,23 +219,23 @@ public PagedList fetchPagedConnections(String objectId, String connection
}
public PagedList fetchConnections(String objectId, String connectionType, Class type, MultiValueMap queryParameters, String... fields) {
- if(fields.length > 0) {
+ if (fields.length > 0) {
String joinedFields = join(fields);
queryParameters.set("fields", joinedFields);
}
return fetchPagedConnections(objectId, connectionType, type, queryParameters);
}
-
+
private PagedList pagify(Class type, JsonNode jsonNode) {
List data = deserializeDataList(jsonNode.get("data"), type);
if (!jsonNode.has("paging")) {
return new PagedList(data, null, null);
}
-
+
JsonNode pagingNode = jsonNode.get("paging");
PagingParameters previousPage = getPagedListParameters(pagingNode, "previous");
PagingParameters nextPage = getPagedListParameters(pagingNode, "next");
-
+
Integer totalCount = null;
if (jsonNode.has("summary")) {
JsonNode summaryNode = jsonNode.get("summary");
@@ -262,20 +243,20 @@ private PagedList pagify(Class type, JsonNode jsonNode) {
totalCount = summaryNode.get("total_count").intValue();
}
}
-
+
return new PagedList(data, previousPage, nextPage, totalCount);
}
public byte[] fetchImage(String objectId, String connectionType, ImageType type) {
URI uri = URIBuilder.fromUri(GRAPH_API_URL + objectId + "/" + connectionType + "?type=" + type.toString().toLowerCase()).build();
ResponseEntity response = getRestTemplate().getForEntity(uri, byte[].class);
- if(response.getStatusCode() == HttpStatus.FOUND) {
+ if (response.getStatusCode() == HttpStatus.FOUND) {
throw new UnsupportedOperationException("Attempt to fetch image resulted in a redirect which could not be followed. Add Apache HttpComponents HttpClient to the classpath " +
"to be able to follow redirects.");
}
return response.getBody();
}
-
+
@SuppressWarnings("unchecked")
public String publish(String objectId, String connectionType, MultiValueMap data) {
MultiValueMap requestData = new LinkedMultiValueMap(data);
@@ -283,38 +264,45 @@ public String publish(String objectId, String connectionType, MultiValueMap response = getRestTemplate().postForObject(uri, requestData, Map.class);
return (String) response.get("id");
}
-
+
public void post(String objectId, MultiValueMap data) {
post(objectId, null, data);
}
-
+
public void post(String objectId, String connectionType, MultiValueMap data) {
String connectionPath = connectionType != null ? "/" + connectionType : "";
URI uri = URIBuilder.fromUri(GRAPH_API_URL + objectId + connectionPath).build();
getRestTemplate().postForObject(uri, new LinkedMultiValueMap(data), String.class);
}
-
+
+ public boolean update(String objectId, MultiValueMap data) {
+ MultiValueMap requestData = new LinkedMultiValueMap(data);
+ URI uri = URIBuilder.fromUri(GRAPH_API_URL + objectId).build();
+ Map response = getRestTemplate().postForObject(uri, requestData, Map.class);
+ return (Boolean) response.get("success");
+ }
+
public void delete(String objectId) {
LinkedMultiValueMap deleteRequest = new LinkedMultiValueMap();
deleteRequest.set("method", "delete");
URI uri = URIBuilder.fromUri(GRAPH_API_URL + objectId).build();
getRestTemplate().postForObject(uri, deleteRequest, String.class);
}
-
+
public void delete(String objectId, String connectionType) {
LinkedMultiValueMap deleteRequest = new LinkedMultiValueMap();
deleteRequest.set("method", "delete");
URI uri = URIBuilder.fromUri(GRAPH_API_URL + objectId + "/" + connectionType).build();
getRestTemplate().postForObject(uri, deleteRequest, String.class);
}
-
+
public void delete(String objectId, String connectionType, MultiValueMap data) {
data.set("method", "delete");
URI uri = URIBuilder.fromUri(GRAPH_API_URL + objectId + "/" + connectionType).build();
HttpEntity> entity = new HttpEntity>(data, new HttpHeaders());
getRestTemplate().exchange(uri, HttpMethod.POST, entity, String.class);
}
-
+
// AbstractOAuth2ApiBinding hooks
@Override
protected OAuth2Version getOAuth2Version() {
@@ -329,19 +317,19 @@ protected void configureRestTemplate(RestTemplate restTemplate) {
@Override
protected MappingJackson2HttpMessageConverter getJsonMessageConverter() {
MappingJackson2HttpMessageConverter converter = super.getJsonMessageConverter();
- objectMapper = new ObjectMapper();
+ objectMapper = new ObjectMapper();
objectMapper.registerModule(new FacebookModule());
- converter.setObjectMapper(objectMapper);
+ converter.setObjectMapper(objectMapper);
return converter;
}
-
+
// private helpers
private void initialize() {
// Wrap the request factory with a BufferingClientHttpRequestFactory so that the error handler can do repeat reads on the response.getBody()
super.setRequestFactory(ClientHttpRequestFactorySelector.bufferRequests(getRestTemplate().getRequestFactory()));
initSubApis();
}
-
+
private void initSubApis() {
achievementOperations = new AchievementTemplate(this);
openGraphOperations = new OpenGraphTemplate(this);
@@ -357,7 +345,7 @@ private void initSubApis() {
testUserOperations = new TestUserTemplate(getRestTemplate(), appId);
socialContextOperations = new SocialContextTemplate(getRestTemplate());
}
-
+
@SuppressWarnings("unchecked")
private List deserializeDataList(JsonNode jsonNode, final Class elementType) {
try {
@@ -367,10 +355,10 @@ private List deserializeDataList(JsonNode jsonNode, final Class elemen
throw new UncategorizedApiException("facebook", "Error deserializing data from Facebook: " + e.getMessage(), e);
}
}
-
+
private String join(String[] strings) {
StringBuilder builder = new StringBuilder();
- if(strings.length > 0) {
+ if (strings.length > 0) {
builder.append(strings[0]);
for (int i = 1; i < strings.length; i++) {
builder.append("," + strings[i]);
@@ -378,5 +366,5 @@ private String join(String[] strings) {
}
return builder.toString();
}
-
+
}
diff --git a/spring-social-facebook/src/main/java/org/springframework/social/facebook/api/impl/json/FacebookModule.java b/spring-social-facebook/src/main/java/org/springframework/social/facebook/api/impl/json/FacebookModule.java
index ade1c0078..2907f264c 100644
--- a/spring-social-facebook/src/main/java/org/springframework/social/facebook/api/impl/json/FacebookModule.java
+++ b/spring-social-facebook/src/main/java/org/springframework/social/facebook/api/impl/json/FacebookModule.java
@@ -15,61 +15,12 @@
*/
package org.springframework.social.facebook.api.impl.json;
-import org.springframework.social.facebook.api.Account;
-import org.springframework.social.facebook.api.Achievement;
-import org.springframework.social.facebook.api.AchievementType;
-import org.springframework.social.facebook.api.Action;
-import org.springframework.social.facebook.api.Album;
-import org.springframework.social.facebook.api.ApplicationReference;
-import org.springframework.social.facebook.api.Comment;
-import org.springframework.social.facebook.api.CoverPhoto;
-import org.springframework.social.facebook.api.Currency;
-import org.springframework.social.facebook.api.Device;
-import org.springframework.social.facebook.api.EducationExperience;
-import org.springframework.social.facebook.api.Engagement;
-import org.springframework.social.facebook.api.Event;
-import org.springframework.social.facebook.api.EventInvitee;
-import org.springframework.social.facebook.api.Experience;
-import org.springframework.social.facebook.api.FamilyMember;
-import org.springframework.social.facebook.api.FriendList;
-import org.springframework.social.facebook.api.Group;
-import org.springframework.social.facebook.api.GroupMemberReference;
-import org.springframework.social.facebook.api.GroupMembership;
-import org.springframework.social.facebook.api.ImageSource;
-import org.springframework.social.facebook.api.Invitation;
-import org.springframework.social.facebook.api.Location;
-import org.springframework.social.facebook.api.MailingAddress;
-import org.springframework.social.facebook.api.MessageTag;
-import org.springframework.social.facebook.api.Page;
-import org.springframework.social.facebook.api.PageParking;
-import org.springframework.social.facebook.api.PagePaymentOptions;
-import org.springframework.social.facebook.api.PageRestaurantServices;
-import org.springframework.social.facebook.api.PageRestaurantSpecialties;
-import org.springframework.social.facebook.api.PaymentPricePoint;
-import org.springframework.social.facebook.api.PaymentPricePoints;
-import org.springframework.social.facebook.api.Photo;
+import org.springframework.social.facebook.api.*;
import org.springframework.social.facebook.api.Photo.Image;
-import org.springframework.social.facebook.api.Place;
-import org.springframework.social.facebook.api.PlaceTag;
-import org.springframework.social.facebook.api.Post;
-import org.springframework.social.facebook.api.PostProperty;
-import org.springframework.social.facebook.api.ProfilePictureSource;
-import org.springframework.social.facebook.api.Reference;
-import org.springframework.social.facebook.api.RestaurantServices;
-import org.springframework.social.facebook.api.SecuritySettings;
-import org.springframework.social.facebook.api.StoryAttachment;
-import org.springframework.social.facebook.api.Tag;
-import org.springframework.social.facebook.api.TestUser;
-import org.springframework.social.facebook.api.User;
-import org.springframework.social.facebook.api.UserIdForApp;
-import org.springframework.social.facebook.api.UserInvitableFriend;
-import org.springframework.social.facebook.api.UserTaggableFriend;
-import org.springframework.social.facebook.api.Video;
import org.springframework.social.facebook.api.Video.VideoFormat;
-import org.springframework.social.facebook.api.VideoUploadLimits;
-import org.springframework.social.facebook.api.VoipInfo;
-import org.springframework.social.facebook.api.WorkEntry;
import org.springframework.social.facebook.api.WorkEntry.Project;
+import org.springframework.social.facebook.api.ads.*;
+import org.springframework.social.facebook.api.ads.impl.json.*;
import org.springframework.social.facebook.api.impl.json.PhotoMixin.ImageMixin;
import org.springframework.social.facebook.api.impl.json.VideoMixin.VideoFormatMixin;
import org.springframework.social.facebook.api.impl.json.WorkEntryMixin.ProjectMixin;
@@ -170,6 +121,25 @@ public void setupModule(SetupContext context) {
context.setMixInAnnotations(VideoUploadLimits.class, VideoUploadLimitsMixin.class);
context.setMixInAnnotations(ProfilePictureSource.class, ProfilePictureSourceMixin.class);
-
+
+
+ context.setMixInAnnotations(AdAccountGroup.class, AdAccountGroupMixin.class);
+ context.setMixInAnnotations(AdAccount.AgencyClientDeclaration.class, AdAccountMixin.AgencyClientDeclarationMixin.class);
+ context.setMixInAnnotations(AdUser.class, AdUserMixin.class);
+
+ context.setMixInAnnotations(AdInsightAction.class, AdInsightActionMixin.class);
+ context.setMixInAnnotations(AdInsight.class, AdInsightMixin.class);
+
+ context.setMixInAnnotations(AdAccount.class, AdAccountMixin.class);
+ context.setMixInAnnotations(AdCampaign.class, AdCampaignMixin.class);
+
+ context.setMixInAnnotations(Targeting.class, TargetingMixin.class);
+ context.setMixInAnnotations(TargetingCityEntry.class, TargetingCityEntryMixin.class);
+ context.setMixInAnnotations(TargetingEntry.class, TargetingEntryMixin.class);
+ context.setMixInAnnotations(TargetingLocation.class, TargetingLocationMixin.class);
+ context.setMixInAnnotations(AdSet.class, AdSetMixin.class);
+
+ context.setMixInAnnotations(AdCreative.class, AdCreativeMixin.class);
+ context.setMixInAnnotations(Ad.class, AdMixin.class);
}
}
diff --git a/spring-social-facebook/src/main/java/org/springframework/social/facebook/api/impl/json/FacebookObjectMixin.java b/spring-social-facebook/src/main/java/org/springframework/social/facebook/api/impl/json/FacebookObjectMixin.java
index 823687680..a8274c36d 100644
--- a/spring-social-facebook/src/main/java/org/springframework/social/facebook/api/impl/json/FacebookObjectMixin.java
+++ b/spring-social-facebook/src/main/java/org/springframework/social/facebook/api/impl/json/FacebookObjectMixin.java
@@ -23,9 +23,9 @@
* @author Craig Walls
*/
@JsonIgnoreProperties(ignoreUnknown = true)
-abstract class FacebookObjectMixin {
+public abstract class FacebookObjectMixin {
@JsonAnySetter
- abstract void add(String key, Object value);
+ protected abstract void add(String key, Object value);
}
diff --git a/spring-social-facebook/src/test/java/org/springframework/social/facebook/api/ErrorHandlingTest.java b/spring-social-facebook/src/test/java/org/springframework/social/facebook/api/ErrorHandlingTest.java
index 2c5444bbc..2933a90b8 100644
--- a/spring-social-facebook/src/test/java/org/springframework/social/facebook/api/ErrorHandlingTest.java
+++ b/spring-social-facebook/src/test/java/org/springframework/social/facebook/api/ErrorHandlingTest.java
@@ -83,7 +83,18 @@ public void code100NonExistingFields() throws Exception {
facebook.fetchObject("me", User.class);
fail();
}
-
+
+ @Test(expected = InvalidParameterException.class)
+ public void code100InvalidParameter() throws Exception {
+ mockServer.expect(requestTo(fbUrl("me/feed")))
+ .andExpect(method(POST))
+ .andExpect(header("Authorization", "OAuth someAccessToken"))
+ .andRespond(withStatus(HttpStatus.BAD_REQUEST).body(jsonResource("error-100-invalidParameter")).contentType(MediaType.APPLICATION_JSON));
+ facebook.feedOperations().post("me", "");
+ fail();
+ }
+
+
@Test(expected=InvalidAuthorizationException.class)
public void code104NoAccessToken() throws Exception {
mockServer.expect(requestTo(fbUrl("me")))
@@ -182,6 +193,5 @@ public void code803UnknownPath() throws Exception {
.andRespond(withStatus(HttpStatus.BAD_REQUEST).body(jsonResource("error-2500-bogusPath")).contentType(MediaType.APPLICATION_JSON));
facebook.fetchObject("me", User.class);
fail();
- }
-
+ }
}
diff --git a/spring-social-facebook/src/test/java/org/springframework/social/facebook/api/ads/AbstractFacebookAdsApiTest.java b/spring-social-facebook/src/test/java/org/springframework/social/facebook/api/ads/AbstractFacebookAdsApiTest.java
new file mode 100644
index 000000000..0edd3e18f
--- /dev/null
+++ b/spring-social-facebook/src/test/java/org/springframework/social/facebook/api/ads/AbstractFacebookAdsApiTest.java
@@ -0,0 +1,48 @@
+package org.springframework.social.facebook.api.ads;
+
+import org.junit.Before;
+import org.springframework.core.io.ClassPathResource;
+import org.springframework.core.io.Resource;
+import org.springframework.social.facebook.api.ads.impl.FacebookAdsTemplate;
+import org.springframework.test.web.client.MockRestServiceServer;
+
+import java.text.DateFormat;
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.Locale;
+
+/**
+ * @author Sebastian Górecki
+ */
+public class AbstractFacebookAdsApiTest {
+ protected static final String ACCESS_TOKEN = "someAccessToken";
+ private static final DateFormat FB_DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ", Locale.ENGLISH);
+ protected static final double EPSILON = 0.000000000001;
+ protected FacebookAdsTemplate facebookAds;
+ protected FacebookAdsTemplate unauthorizedFacebookAds;
+ protected MockRestServiceServer mockServer;
+ protected MockRestServiceServer unauthorizedMockServer;
+
+ @Before
+ public void setUp() throws Exception {
+ facebookAds = new FacebookAdsTemplate(ACCESS_TOKEN);
+ mockServer = MockRestServiceServer.createServer(facebookAds.getRestTemplate());
+
+ unauthorizedFacebookAds = new FacebookAdsTemplate();
+ unauthorizedMockServer = MockRestServiceServer.createServer(unauthorizedFacebookAds.getRestTemplate());
+ }
+
+ protected Resource jsonResource(String filename) {
+ return new ClassPathResource(filename + ".json", getClass());
+ }
+
+ protected Date toDate(String dateString) {
+ try {
+ return FB_DATE_FORMAT.parse(dateString);
+ } catch (ParseException e) {
+ return null;
+ }
+ }
+
+}
diff --git a/spring-social-facebook/src/test/java/org/springframework/social/facebook/api/ads/AccountTemplateTest.java b/spring-social-facebook/src/test/java/org/springframework/social/facebook/api/ads/AccountTemplateTest.java
new file mode 100644
index 000000000..b86f10409
--- /dev/null
+++ b/spring-social-facebook/src/test/java/org/springframework/social/facebook/api/ads/AccountTemplateTest.java
@@ -0,0 +1,522 @@
+package org.springframework.social.facebook.api.ads;
+
+import org.junit.Test;
+import org.springframework.http.MediaType;
+import org.springframework.social.NotAuthorizedException;
+import org.springframework.social.facebook.api.PagedList;
+import org.springframework.social.facebook.api.ads.AdAccount.Capability;
+import org.springframework.social.facebook.api.ads.AdAccount.TaxStatus;
+import org.springframework.social.facebook.api.ads.AdCampaign.BuyingType;
+import org.springframework.social.facebook.api.ads.AdCampaign.CampaignObjective;
+import org.springframework.social.facebook.api.ads.AdCampaign.CampaignStatus;
+import org.springframework.social.facebook.api.ads.AdUser.AdUserPermission;
+import org.springframework.social.facebook.api.ads.AdUser.AdUserRole;
+
+import java.util.List;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+import static org.springframework.http.HttpMethod.GET;
+import static org.springframework.http.HttpMethod.POST;
+import static org.springframework.test.web.client.match.MockRestRequestMatchers.*;
+import static org.springframework.test.web.client.response.MockRestResponseCreators.withSuccess;
+
+/**
+ * @author Sebastian Górecki
+ */
+public class AccountTemplateTest extends AbstractFacebookAdsApiTest {
+
+ private static final String GET_ADACCOUNT_REQUEST_URI = "https://graph.facebook.com/v2.3/act_123456789?fields=id%2Caccount_id%2Caccount_status%2Cage%2Camount_spent%2Cbalance%2Cbusiness_city%2Cbusiness_country_code%2Cbusiness_name%2Cbusiness_state%2Cbusiness_street%2Cbusiness_street2%2Cbusiness_zip%2Ccapabilities%2Ccreated_time%2Ccurrency%2Cdaily_spend_limit%2Cend_advertiser%2Cfunding_source%2Cfunding_source_details%2Cis_personal%2Cmedia_agency%2Cname%2Coffsite_pixels_tos_accepted%2Cpartner%2Cspend_cap%2Ctimezone_id%2Ctimezone_name%2Ctimezone_offset_hours_utc%2Cusers%2Ctax_id_status";
+ private static final String GET_ADACCOUNTS_REQUEST_URI = "https://graph.facebook.com/v2.3/1234/adaccounts?fields=id%2Caccount_id%2Caccount_status%2Cage%2Camount_spent%2Cbalance%2Cbusiness_city%2Cbusiness_country_code%2Cbusiness_name%2Cbusiness_state%2Cbusiness_street%2Cbusiness_street2%2Cbusiness_zip%2Ccapabilities%2Ccreated_time%2Ccurrency%2Cdaily_spend_limit%2Cend_advertiser%2Cfunding_source%2Cfunding_source_details%2Cis_personal%2Cmedia_agency%2Cname%2Coffsite_pixels_tos_accepted%2Cpartner%2Cspend_cap%2Ctimezone_id%2Ctimezone_name%2Ctimezone_offset_hours_utc%2Cusers%2Ctax_id_status";
+ private static final String GET_ADACCOUNT_USERS_REQUEST_URI = "https://graph.facebook.com/v2.3/act_123456789/users";
+ private static final String GET_ADACCOUNT_INSIGHT = "https://graph.facebook.com/v2.3/act_123456789/insights?fields=account_id%2Caccount_name%2Cdate_start%2Cdate_stop%2Cactions_per_impression%2Cclicks%2Cunique_clicks%2Ccost_per_result%2Ccost_per_total_action%2Ccpc%2Ccost_per_unique_click%2Ccpm%2Ccpp%2Cctr%2Cunique_ctr%2Cfrequency%2Cimpressions%2Cunique_impressions%2Cobjective%2Creach%2Cresult_rate%2Cresults%2Croas%2Csocial_clicks%2Cunique_social_clicks%2Csocial_impressions%2Cunique_social_impressions%2Csocial_reach%2Cspend%2Ctoday_spend%2Ctotal_action_value%2Ctotal_actions%2Ctotal_unique_actions%2Cactions%2Cunique_actions%2Ccost_per_action_type%2Cvideo_start_actions";
+
+
+ @Test
+ public void getAccounts() throws Exception {
+ mockServer.expect(requestTo(GET_ADACCOUNTS_REQUEST_URI))
+ .andExpect(method(GET))
+ .andExpect(header("Authorization", "OAuth someAccessToken"))
+ .andRespond(withSuccess(jsonResource("ad-accounts"), MediaType.APPLICATION_JSON));
+
+ List adAccounts = facebookAds.accountOperations().getAdAccounts("1234");
+ assertEquals(2, adAccounts.size());
+ assertAdAccountsFields(adAccounts);
+ }
+
+ @Test(expected = NotAuthorizedException.class)
+ public void getAccounts_unauthorized() throws Exception {
+ unauthorizedFacebookAds.accountOperations().getAdAccounts("1234");
+ }
+
+ @Test
+ public void getAdAccount() throws Exception {
+ mockServer.expect(requestTo(GET_ADACCOUNT_REQUEST_URI))
+ .andExpect(method(GET))
+ .andExpect(header("Authorization", "OAuth someAccessToken"))
+ .andRespond(withSuccess(jsonResource("ad-account"), MediaType.APPLICATION_JSON));
+
+ AdAccount adAccount = facebookAds.accountOperations().getAdAccount("123456789");
+ assertAdAccountFields(adAccount);
+ assertEquals(AdAccount.AccountStatus.ACTIVE, adAccount.getStatus());
+ assertEquals(1, adAccount.getUsers().size());
+ assertEquals("1234", adAccount.getUsers().get(0).getId());
+ assertEquals(AdUserPermission.ACCOUNT_ADMIN, adAccount.getUsers().get(0).getPermissions().get(0));
+ assertEquals(AdUserPermission.ADMANAGER_READ, adAccount.getUsers().get(0).getPermissions().get(1));
+ assertEquals(AdUserPermission.ADMANAGER_WRITE, adAccount.getUsers().get(0).getPermissions().get(2));
+ assertEquals(AdUserRole.ADMINISTRATOR, adAccount.getUsers().get(0).getRole());
+ }
+
+ @Test
+ public void getAdAccount_withStatusTemporarilyUnavailable() throws Exception {
+ mockServer.expect(requestTo(GET_ADACCOUNT_REQUEST_URI))
+ .andExpect(method(GET))
+ .andExpect(header("Authorization", "OAuth someAccessToken"))
+ .andRespond(withSuccess(jsonResource("ad-account-temporarily-unavailable"), MediaType.APPLICATION_JSON));
+
+ AdAccount adAccount = facebookAds.accountOperations().getAdAccount("123456789");
+ assertAdAccountFields(adAccount);
+ assertEquals(AdAccount.AccountStatus.TEMPORARILY_UNAVAILABLE, adAccount.getStatus());
+ assertEquals(1, adAccount.getUsers().size());
+ assertEquals("1234", adAccount.getUsers().get(0).getId());
+ assertEquals(AdUserPermission.ACCOUNT_ADMIN, adAccount.getUsers().get(0).getPermissions().get(0));
+ assertEquals(AdUserPermission.ADMANAGER_READ, adAccount.getUsers().get(0).getPermissions().get(1));
+ assertEquals(AdUserPermission.ADMANAGER_WRITE, adAccount.getUsers().get(0).getPermissions().get(2));
+ assertEquals(AdUserRole.ADMINISTRATOR, adAccount.getUsers().get(0).getRole());
+ }
+
+ @Test
+ public void getAdAccount_withUnknownCapabilities() throws Exception {
+ mockServer.expect(requestTo(GET_ADACCOUNT_REQUEST_URI))
+ .andExpect(method(GET))
+ .andExpect(header("Authorization", "OAuth someAccessToken"))
+ .andRespond(withSuccess(jsonResource("ad-account-unknown-capabilities"), MediaType.APPLICATION_JSON));
+ AdAccount adAccount = facebookAds.accountOperations().getAdAccount("123456789");
+ assertEquals("act_123456789", adAccount.getId());
+ assertEquals(123456789, adAccount.getAccountId());
+ assertEquals(4, adAccount.getCapabilities().size());
+ assertEquals(Capability.UNKNOWN, adAccount.getCapabilities().get(0));
+ assertEquals(Capability.UNKNOWN, adAccount.getCapabilities().get(1));
+ assertEquals(Capability.UNKNOWN, adAccount.getCapabilities().get(2));
+ assertEquals(Capability.PREMIUM, adAccount.getCapabilities().get(3));
+ }
+
+ @Test
+ public void getAdAccount_withEmptyUsers() throws Exception {
+ mockServer.expect(requestTo(GET_ADACCOUNT_REQUEST_URI))
+ .andExpect(method(GET))
+ .andExpect(header("Authorization", "OAuth someAccessToken"))
+ .andRespond(withSuccess(jsonResource("ad-account-no-users"), MediaType.APPLICATION_JSON));
+
+ AdAccount adAccount = facebookAds.accountOperations().getAdAccount("123456789");
+ assertAdAccountFields(adAccount);
+ assertEquals(0, adAccount.getUsers().size());
+ }
+
+ @Test
+ public void getAdAccount_withFewUsers() throws Exception {
+ mockServer.expect(requestTo(GET_ADACCOUNT_REQUEST_URI))
+ .andExpect(method(GET))
+ .andExpect(header("Authorization", "OAuth someAccessToken"))
+ .andRespond(withSuccess(jsonResource("ad-account-with-few-users"), MediaType.APPLICATION_JSON));
+
+ AdAccount adAccount = facebookAds.accountOperations().getAdAccount("123456789");
+ assertAdAccountFields(adAccount);
+ assertEquals(2, adAccount.getUsers().size());
+ assertEquals("1234", adAccount.getUsers().get(0).getId());
+ assertEquals(AdUserPermission.ACCOUNT_ADMIN, adAccount.getUsers().get(0).getPermissions().get(0));
+ assertEquals(AdUserPermission.ADMANAGER_READ, adAccount.getUsers().get(0).getPermissions().get(1));
+ assertEquals(AdUserPermission.ADMANAGER_WRITE, adAccount.getUsers().get(0).getPermissions().get(2));
+ assertEquals(AdUserRole.ADMINISTRATOR, adAccount.getUsers().get(0).getRole());
+ assertEquals("3421", adAccount.getUsers().get(1).getId());
+ assertEquals(AdUserPermission.BILLING_WRITE, adAccount.getUsers().get(1).getPermissions().get(0));
+ assertEquals(AdUserPermission.REPORTS, adAccount.getUsers().get(1).getPermissions().get(1));
+ assertEquals(AdUserRole.ANALYST, adAccount.getUsers().get(1).getRole());
+ }
+
+ @Test
+ public void getAdAccount_userWithNoPermissions() throws Exception {
+ mockServer.expect(requestTo(GET_ADACCOUNT_REQUEST_URI))
+ .andExpect(method(GET))
+ .andExpect(header("Authorization", "OAuth someAccessToken"))
+ .andRespond(withSuccess(jsonResource("ad-account-without-permission"), MediaType.APPLICATION_JSON));
+
+ AdAccount adAccount = facebookAds.accountOperations().getAdAccount("123456789");
+ assertAdAccountFields(adAccount);
+ assertEquals(1, adAccount.getUsers().size());
+ assertEquals("1234", adAccount.getUsers().get(0).getId());
+ assertEquals(0, adAccount.getUsers().get(0).getPermissions().size());
+ assertEquals(AdUserRole.ADMINISTRATOR, adAccount.getUsers().get(0).getRole());
+ }
+
+ @Test
+ public void getAdAccount_withAgencyClientDeclaration() throws Exception {
+ mockServer.expect(requestTo(GET_ADACCOUNT_REQUEST_URI))
+ .andExpect(method(GET))
+ .andExpect(header("Authorization", "OAuth someAccessToken"))
+ .andRespond(withSuccess(jsonResource("ad-account-with-agency"), MediaType.APPLICATION_JSON));
+
+ AdAccount adAccount = facebookAds.accountOperations().getAdAccount("123456789");
+ assertAdAccountFields(adAccount);
+ assertEquals(1, adAccount.getAgencyClientDeclaration().getAgencyRepresentingClient());
+ assertEquals(0, adAccount.getAgencyClientDeclaration().getClientBasedInFrance());
+ assertEquals("Warsaw", adAccount.getAgencyClientDeclaration().getClientCity());
+ assertEquals("PL", adAccount.getAgencyClientDeclaration().getClientCountryCode());
+ assertEquals("example@example.com", adAccount.getAgencyClientDeclaration().getClientEmailAddress());
+ assertEquals("Some client", adAccount.getAgencyClientDeclaration().getClientName());
+ assertEquals("66-777", adAccount.getAgencyClientDeclaration().getClientPostalCode());
+ assertEquals("malopolska", adAccount.getAgencyClientDeclaration().getClientProvince());
+ assertEquals("Marszalkowska", adAccount.getAgencyClientDeclaration().getClientStreet());
+ assertEquals("1A", adAccount.getAgencyClientDeclaration().getClientStreet2());
+ assertEquals(0, adAccount.getAgencyClientDeclaration().getHasWrittenMandateFromAdvertiser());
+ assertEquals(1, adAccount.getAgencyClientDeclaration().getIsClientPayingInvoices());
+ }
+
+ @Test
+ public void getAdAccount_withFundingDetails() throws Exception {
+ mockServer.expect(requestTo(GET_ADACCOUNT_REQUEST_URI))
+ .andExpect(method(GET))
+ .andExpect(header("Authorization", "OAuth someAccessToken"))
+ .andRespond(withSuccess(jsonResource("ad-account-with-funding-details"), MediaType.APPLICATION_JSON));
+
+ AdAccount adAccount = facebookAds.accountOperations().getAdAccount("123456789");
+ assertAdAccountFields(adAccount);
+ assertTrue(adAccount.getFundingSourceDetails().containsKey("id"));
+ assertEquals("12345678987654321", adAccount.getFundingSourceDetails().get("id"));
+ assertTrue(adAccount.getFundingSourceDetails().containsKey("display_string"));
+ assertEquals("Visa *0001", adAccount.getFundingSourceDetails().get("display_string"));
+ assertTrue(adAccount.getFundingSourceDetails().containsKey("type"));
+ assertEquals(1, adAccount.getFundingSourceDetails().get("type"));
+ }
+
+ @Test
+ public void getAdAccount_withTosAccepted() throws Exception {
+ mockServer.expect(requestTo(GET_ADACCOUNT_REQUEST_URI))
+ .andExpect(method(GET))
+ .andExpect(header("Authorization", "OAuth someAccessToken"))
+ .andRespond(withSuccess(jsonResource("ad-account-with-tos-accepted"), MediaType.APPLICATION_JSON));
+
+ AdAccount adAccount = facebookAds.accountOperations().getAdAccount("123456789");
+ assertAdAccountFields(adAccount);
+ assertTrue(adAccount.getTosAccepted().containsKey("206760949512025"));
+ assertEquals((Integer) 1, adAccount.getTosAccepted().get("206760949512025"));
+ assertTrue(adAccount.getTosAccepted().containsKey("215449065224656"));
+ assertEquals((Integer) 1, adAccount.getTosAccepted().get("215449065224656"));
+ }
+
+ @Test(expected = NotAuthorizedException.class)
+ public void getAdAccount_unauthorized() throws Exception {
+ unauthorizedFacebookAds.accountOperations().getAdAccount("123456789");
+ }
+
+ @Test
+ public void getAdAccountCampaigns() throws Exception {
+ mockServer.expect(requestTo("https://graph.facebook.com/v2.3/act_123456789/adcampaign_groups?fields=id%2Caccount_id%2Cbuying_type%2Ccampaign_group_status%2Cname%2Cobjective%2Cspend_cap"))
+ .andExpect(method(GET))
+ .andExpect(header("Authorization", "OAuth someAccessToken"))
+ .andRespond(withSuccess(jsonResource("ad-account-campaigns"), MediaType.APPLICATION_JSON));
+ PagedList campaigns = facebookAds.accountOperations().getAdAccountCampaigns("123456789");
+ assertEquals(3, campaigns.size());
+ assertEquals("601123456789", campaigns.get(0).getId());
+ assertEquals("123456789", campaigns.get(0).getAccountId());
+ assertEquals(BuyingType.AUCTION, campaigns.get(0).getBuyingType());
+ assertEquals(CampaignStatus.ACTIVE, campaigns.get(0).getStatus());
+ assertEquals("Campaign #1", campaigns.get(0).getName());
+ assertEquals(CampaignObjective.POST_ENGAGEMENT, campaigns.get(0).getObjective());
+ assertEquals(0, campaigns.get(0).getSpendCap());
+ assertEquals("602123456789", campaigns.get(1).getId());
+ assertEquals("123456789", campaigns.get(1).getAccountId());
+ assertEquals(BuyingType.FIXED_CPM, campaigns.get(1).getBuyingType());
+ assertEquals(CampaignStatus.PAUSED, campaigns.get(1).getStatus());
+ assertEquals("Campaign #2", campaigns.get(1).getName());
+ assertEquals(CampaignObjective.NONE, campaigns.get(1).getObjective());
+ assertEquals(0, campaigns.get(1).getSpendCap());
+ assertEquals("603123456789", campaigns.get(2).getId());
+ assertEquals("123456789", campaigns.get(2).getAccountId());
+ assertEquals(BuyingType.RESERVED, campaigns.get(2).getBuyingType());
+ assertEquals(CampaignStatus.ARCHIVED, campaigns.get(2).getStatus());
+ assertEquals("Campaign #3", campaigns.get(2).getName());
+ assertEquals(CampaignObjective.WEBSITE_CONVERSIONS, campaigns.get(2).getObjective());
+ assertEquals(50000, campaigns.get(2).getSpendCap());
+ }
+
+ @Test
+ public void getAccountUsers() throws Exception {
+ mockServer.expect(requestTo(GET_ADACCOUNT_USERS_REQUEST_URI))
+ .andExpect(method(GET))
+ .andExpect(header("Authorization", "OAuth someAccessToken"))
+ .andRespond(withSuccess(jsonResource("ad-account-users"), MediaType.APPLICATION_JSON));
+
+ List adAccountUsers = facebookAds.accountOperations().getAdAccountUsers("123456789");
+ assertEquals(3, adAccountUsers.size());
+ assertEquals("123456789", adAccountUsers.get(0).getId());
+ assertEquals("Account #1", adAccountUsers.get(0).getName());
+ assertEquals(6, adAccountUsers.get(0).getPermissions().size());
+ assertEquals(AdUserPermission.ACCOUNT_ADMIN, adAccountUsers.get(0).getPermissions().get(0));
+ assertEquals(AdUserPermission.ADMANAGER_READ, adAccountUsers.get(0).getPermissions().get(1));
+ assertEquals(AdUserPermission.ADMANAGER_WRITE, adAccountUsers.get(0).getPermissions().get(2));
+ assertEquals(AdUserPermission.BILLING_READ, adAccountUsers.get(0).getPermissions().get(3));
+ assertEquals(AdUserPermission.BILLING_WRITE, adAccountUsers.get(0).getPermissions().get(4));
+ assertEquals(AdUserPermission.REPORTS, adAccountUsers.get(0).getPermissions().get(5));
+ assertEquals(AdUserRole.ADMINISTRATOR, adAccountUsers.get(0).getRole());
+ assertEquals("987654321", adAccountUsers.get(1).getId());
+ assertEquals("Account #2", adAccountUsers.get(1).getName());
+ assertEquals(1, adAccountUsers.get(1).getPermissions().size());
+ assertEquals(AdUserPermission.REPORTS, adAccountUsers.get(1).getPermissions().get(0));
+ assertEquals(AdUserRole.ANALYST, adAccountUsers.get(1).getRole());
+ assertEquals("1122334455", adAccountUsers.get(2).getId());
+ assertEquals("Account #3", adAccountUsers.get(2).getName());
+ assertEquals(2, adAccountUsers.get(2).getPermissions().size());
+ assertEquals(AdUserPermission.ADMANAGER_READ, adAccountUsers.get(2).getPermissions().get(0));
+ assertEquals(AdUserPermission.BILLING_READ, adAccountUsers.get(2).getPermissions().get(1));
+ assertEquals(AdUserRole.SALES, adAccountUsers.get(2).getRole());
+ }
+
+ @Test(expected = NotAuthorizedException.class)
+ public void getAccountUsers_unauthorized() throws Exception {
+ unauthorizedFacebookAds.accountOperations().getAdAccountUsers("123456789");
+ }
+
+ @Test
+ public void addUserToAccount() throws Exception {
+ String requestBody = "uid=123456&role=1002";
+ mockServer.expect(requestTo("https://graph.facebook.com/v2.3/act_123456789/users/"))
+ .andExpect(method(POST))
+ .andExpect(content().string(requestBody))
+ .andExpect(header("Authorization", "OAuth someAccessToken"))
+ .andRespond(withSuccess("{\"success\":true}", MediaType.APPLICATION_JSON));
+ facebookAds.accountOperations().addUserToAdAccount("123456789", "123456", AdUserRole.ADVERTISER);
+ mockServer.verify();
+ }
+
+ @Test(expected = NotAuthorizedException.class)
+ public void addUserToAccount_unauthorized() throws Exception {
+ unauthorizedFacebookAds.accountOperations().addUserToAdAccount("123456789", "123456", AdUserRole.ADVERTISER);
+ }
+
+ @Test
+ public void deleteUserFromAccount() throws Exception {
+ String requestBody = "method=delete";
+ mockServer.expect(requestTo("https://graph.facebook.com/v2.3/act_123456789/users/123456"))
+ .andExpect(method(POST))
+ .andExpect(header("Authorization", "OAuth someAccessToken"))
+ .andExpect(content().string(requestBody))
+ .andRespond(withSuccess("{\"success\":true}", MediaType.APPLICATION_JSON));
+ facebookAds.accountOperations().deleteUserFromAdAccount("123456789", "123456");
+ mockServer.verify();
+ }
+
+ @Test(expected = NotAuthorizedException.class)
+ public void deleteUserFromAccount_unauthorized() throws Exception {
+ unauthorizedFacebookAds.accountOperations().deleteUserFromAdAccount("123456789", "123456");
+ }
+
+ @Test
+ public void getAccountInsight() throws Exception {
+ mockServer.expect(requestTo(GET_ADACCOUNT_INSIGHT))
+ .andExpect(method(GET))
+ .andExpect(header("Authorization", "OAuth someAccessToken"))
+ .andRespond(withSuccess(jsonResource("ad-account-insights"), MediaType.APPLICATION_JSON));
+
+ AdInsight insight = facebookAds.accountOperations().getAdAccountInsight("123456789");
+ assertEquals("123456789", insight.getAccountId());
+ assertEquals("Account Test Name #1", insight.getAccountName());
+ assertEquals(0.016042780748663, insight.getActionsPerImpression(), EPSILON);
+ assertEquals(8, insight.getClicks());
+ assertEquals(5, insight.getUniqueClicks());
+ assertEquals(0.66666666666667, insight.getCostPerResult(), EPSILON);
+ assertEquals(0.66666666666667, insight.getCostPerTotalAction(), EPSILON);
+ assertEquals(0.25, insight.getCostPerClick(), EPSILON);
+ assertEquals(0.4, insight.getCostPerUniqueClick(), EPSILON);
+ assertEquals(10.695187165775, insight.getCpm(), EPSILON);
+ assertEquals(10.869565217391, insight.getCpp(), EPSILON);
+ assertEquals(4.2780748663102, insight.getCtr(), EPSILON);
+ assertEquals(2.7173913043478, insight.getUniqueCtr(), EPSILON);
+ assertEquals(1.0163043478261, insight.getFrequency(), EPSILON);
+ assertEquals(187, insight.getImpressions());
+ assertEquals(184, insight.getUniqueImpressions());
+ assertEquals(184, insight.getReach());
+ assertEquals(1.6042780748663, insight.getResultRate(), EPSILON);
+ assertEquals(3, insight.getResults());
+ assertEquals(1, insight.getRoas());
+ assertEquals(2, insight.getSocialClicks());
+ assertEquals(3, insight.getUniqueSocialClicks());
+ assertEquals(4, insight.getSocialImpressions());
+ assertEquals(5, insight.getUniqueSocialImpressions());
+ assertEquals(6, insight.getSocialReach());
+ assertEquals(2, insight.getSpend());
+ assertEquals(0, insight.getTodaySpend());
+ assertEquals(0, insight.getTotalActionValue());
+ assertEquals(3, insight.getTotalActions());
+ assertEquals(2, insight.getTotalUniqueActions());
+ assertEquals(4, insight.getActions().size());
+ assertEquals("comment", insight.getActions().get(0).getActionType());
+ assertEquals(2, insight.getActions().get(0).getValue(), EPSILON);
+ assertEquals("post_like", insight.getActions().get(1).getActionType());
+ assertEquals(1, insight.getActions().get(1).getValue(), EPSILON);
+ assertEquals("page_engagement", insight.getActions().get(2).getActionType());
+ assertEquals(3, insight.getActions().get(2).getValue(), EPSILON);
+ assertEquals("post_engagement", insight.getActions().get(3).getActionType());
+ assertEquals(3, insight.getActions().get(3).getValue(), EPSILON);
+ assertEquals(4, insight.getUniqueActions().size());
+ assertEquals("comment", insight.getUniqueActions().get(0).getActionType());
+ assertEquals(1, insight.getUniqueActions().get(0).getValue(), EPSILON);
+ assertEquals("post_like", insight.getUniqueActions().get(1).getActionType());
+ assertEquals(1, insight.getUniqueActions().get(1).getValue(), EPSILON);
+ assertEquals("page_engagement", insight.getUniqueActions().get(2).getActionType());
+ assertEquals(2, insight.getUniqueActions().get(2).getValue(), EPSILON);
+ assertEquals("post_engagement", insight.getUniqueActions().get(3).getActionType());
+ assertEquals(2, insight.getUniqueActions().get(3).getValue(), EPSILON);
+ assertEquals(4, insight.getCostPerActionType().size());
+ assertEquals("comment", insight.getCostPerActionType().get(0).getActionType());
+ assertEquals(1, insight.getCostPerActionType().get(0).getValue(), EPSILON);
+ assertEquals("post_like", insight.getCostPerActionType().get(1).getActionType());
+ assertEquals(2, insight.getCostPerActionType().get(1).getValue(), EPSILON);
+ assertEquals("page_engagement", insight.getCostPerActionType().get(2).getActionType());
+ assertEquals(0.66666666666667, insight.getCostPerActionType().get(2).getValue(), EPSILON);
+ assertEquals("post_engagement", insight.getCostPerActionType().get(3).getActionType());
+ assertEquals(0.66666666666667, insight.getCostPerActionType().get(3).getValue(), EPSILON);
+ assertEquals(1, insight.getVideoStartActions().size());
+ assertEquals("video_view", insight.getVideoStartActions().get(0).getActionType());
+ assertEquals(0, insight.getVideoStartActions().get(0).getValue(), EPSILON);
+ }
+
+ @Test(expected = NotAuthorizedException.class)
+ public void getAccountInsight_unauthorized() throws Exception {
+ unauthorizedFacebookAds.accountOperations().getAdAccountInsight("123456789");
+ }
+
+ @Test
+ public void updateAdAccount_nameOnly() throws Exception {
+ String requestBody = "name=New+Test+Name";
+ mockServer.expect(requestTo("https://graph.facebook.com/v2.3/act_123456789"))
+ .andExpect(method(POST))
+ .andExpect(header("Authorization", "OAuth someAccessToken"))
+ .andExpect(content().string(requestBody))
+ .andRespond(withSuccess("{\"success\":true}", MediaType.APPLICATION_JSON));
+ AdAccount adAccount = new AdAccount();
+ adAccount.setName("New Test Name");
+ boolean updateStatus = facebookAds.accountOperations().updateAdAccount("123456789", adAccount);
+ assertTrue(updateStatus);
+ mockServer.verify();
+ }
+
+ @Test
+ public void updateAdAccount_spendCapOnly() throws Exception {
+ String requestBody = "spend_cap=10000";
+ mockServer.expect(requestTo("https://graph.facebook.com/v2.3/act_123456789"))
+ .andExpect(method(POST))
+ .andExpect(header("Authorization", "OAuth someAccessToken"))
+ .andExpect(content().string(requestBody))
+ .andRespond(withSuccess("{\"success\":true}", MediaType.APPLICATION_JSON));
+ AdAccount adAccount = new AdAccount();
+ adAccount.setSpendCap("10000");
+ boolean updateStatus = facebookAds.accountOperations().updateAdAccount("123456789", adAccount);
+ assertTrue(updateStatus);
+ mockServer.verify();
+ }
+
+ @Test
+ public void updateAdAccount_bothNameAndSpendCap() throws Exception {
+ String requestBody = "name=Super+cool+name&spend_cap=11111";
+ mockServer.expect(requestTo("https://graph.facebook.com/v2.3/act_123456789"))
+ .andExpect(method(POST))
+ .andExpect(header("Authorization", "OAuth someAccessToken"))
+ .andExpect(content().string(requestBody))
+ .andRespond(withSuccess("{\"success\":true}", MediaType.APPLICATION_JSON));
+ AdAccount adAccount = new AdAccount();
+ adAccount.setName("Super cool name");
+ adAccount.setSpendCap("11111");
+ boolean updateStatus = facebookAds.accountOperations().updateAdAccount("123456789", adAccount);
+ assertTrue(updateStatus);
+ mockServer.verify();
+ }
+
+ @Test(expected = NotAuthorizedException.class)
+ public void updateAdAccount_unauthorized() throws Exception {
+ AdAccount adAccount = new AdAccount();
+ adAccount.setName("abc");
+ unauthorizedFacebookAds.accountOperations().updateAdAccount("123456789", adAccount);
+ }
+
+ private void assertAdAccountsFields(List adAccounts) {
+ assertAdAccountFields(adAccounts.get(0));
+ assertAdAccountFields(adAccounts.get(0));
+ assertEquals("act_77777777", adAccounts.get(1).getId());
+ assertEquals(2, adAccounts.get(1).getAccountGroups().size());
+ assertEquals("987654321", adAccounts.get(1).getAccountGroups().get(0).getId());
+ assertEquals("Test group name", adAccounts.get(1).getAccountGroups().get(0).getName());
+ assertEquals(1, adAccounts.get(1).getAccountGroups().get(0).getStatus());
+ assertEquals("11223344", adAccounts.get(1).getAccountGroups().get(1).getId());
+ assertEquals("Test group name 2", adAccounts.get(1).getAccountGroups().get(1).getName());
+ assertEquals(1, adAccounts.get(1).getAccountGroups().get(1).getStatus());
+ assertEquals(77777777, adAccounts.get(1).getAccountId());
+ assertEquals(AdAccount.AccountStatus.DISABLED, adAccounts.get(1).getStatus());
+ assertEquals(777, adAccounts.get(1).getAge());
+ assertEquals("7777", adAccounts.get(1).getAmountSpent());
+ assertEquals("77777", adAccounts.get(1).getBalance());
+ assertEquals("Warsaw", adAccounts.get(1).getBusinessCity());
+ assertEquals("PL", adAccounts.get(1).getBusinessCountryCode());
+ assertEquals("Some business name for the account 2", adAccounts.get(1).getBusinessName());
+ assertEquals("mazowieckie", adAccounts.get(1).getBusinessState());
+ assertEquals("Some street 2", adAccounts.get(1).getBusinessStreet());
+ assertEquals(null, adAccounts.get(1).getBusinessStreet2());
+ assertEquals("77-777", adAccounts.get(1).getBusinessZip());
+ assertEquals(2, adAccounts.get(1).getCapabilities().size());
+ assertEquals(Capability.DIRECT_SALES, adAccounts.get(1).getCapabilities().get(0));
+ assertEquals(Capability.VIEW_TAGS, adAccounts.get(1).getCapabilities().get(1));
+ assertEquals(toDate("2015-04-20T00:31:33+0100"), adAccounts.get(1).getCreatedTime());
+ assertEquals("PLN", adAccounts.get(1).getCurrency());
+ assertEquals("77", adAccounts.get(1).getDailySpendLimit());
+ assertEquals(987654321, adAccounts.get(1).getEndAdvertiser());
+ assertEquals("77", adAccounts.get(1).getFundingSource());
+ assertEquals(1, adAccounts.get(1).getIsPersonal());
+ assertEquals(54321, adAccounts.get(1).getMediaAgency());
+ assertEquals("This is a test account 2", adAccounts.get(1).getName());
+ assertEquals(false, adAccounts.get(1).isOffsitePixelsTOSAccepted());
+ assertEquals(111222333, adAccounts.get(1).getPartner());
+ assertEquals("0", adAccounts.get(1).getSpendCap());
+ assertEquals(106, adAccounts.get(1).getTimezoneId());
+ assertEquals("Europe/Warsaw", adAccounts.get(1).getTimezoneName());
+ assertEquals(2, adAccounts.get(1).getTimezoneOffsetHoursUTC());
+ assertEquals(TaxStatus.ACCOUNT_IS_PERSONAL_ACCOUNT, adAccounts.get(1).getTaxStatus());
+ }
+
+
+ private void assertAdAccountFields(AdAccount adAccount) {
+ assertEquals("act_123456789", adAccount.getId());
+ assertEquals(1, adAccount.getAccountGroups().size());
+ assertEquals("987654321", adAccount.getAccountGroups().get(0).getId());
+ assertEquals("Test group name", adAccount.getAccountGroups().get(0).getName());
+ assertEquals(1, adAccount.getAccountGroups().get(0).getStatus());
+ assertEquals(123456789, adAccount.getAccountId());
+ assertEquals(10, adAccount.getAge());
+ assertEquals("1234", adAccount.getAmountSpent());
+ assertEquals("10000", adAccount.getBalance());
+ assertEquals("Poznan", adAccount.getBusinessCity());
+ assertEquals("PL", adAccount.getBusinessCountryCode());
+ assertEquals("Some business name for the account", adAccount.getBusinessName());
+ assertEquals("wielkopolska", adAccount.getBusinessState());
+ assertEquals("Some street 79A", adAccount.getBusinessStreet());
+ assertEquals(null, adAccount.getBusinessStreet2());
+ assertEquals("66-777", adAccount.getBusinessZip());
+ assertEquals(2, adAccount.getCapabilities().size());
+ assertEquals(Capability.DIRECT_SALES, adAccount.getCapabilities().get(0));
+ assertEquals(Capability.VIEW_TAGS, adAccount.getCapabilities().get(1));
+ assertEquals(toDate("2015-02-19T00:31:33+0100"), adAccount.getCreatedTime());
+ assertEquals("PLN", adAccount.getCurrency());
+ assertEquals("93263", adAccount.getDailySpendLimit());
+ assertEquals(987654321, adAccount.getEndAdvertiser());
+ assertEquals("1122334455", adAccount.getFundingSource());
+ assertEquals(1, adAccount.getIsPersonal());
+ assertEquals(54321, adAccount.getMediaAgency());
+ assertEquals("This is a test account", adAccount.getName());
+ assertEquals(false, adAccount.isOffsitePixelsTOSAccepted());
+ assertEquals(111222333, adAccount.getPartner());
+ assertEquals("0", adAccount.getSpendCap());
+ assertEquals(106, adAccount.getTimezoneId());
+ assertEquals("Europe/Warsaw", adAccount.getTimezoneName());
+ assertEquals(2, adAccount.getTimezoneOffsetHoursUTC());
+ assertEquals(TaxStatus.VAT_INFORMATION_SUBMITTED, adAccount.getTaxStatus());
+ }
+}
diff --git a/spring-social-facebook/src/test/java/org/springframework/social/facebook/api/ads/AdSetTemplateTest.java b/spring-social-facebook/src/test/java/org/springframework/social/facebook/api/ads/AdSetTemplateTest.java
new file mode 100644
index 000000000..fcfd08c90
--- /dev/null
+++ b/spring-social-facebook/src/test/java/org/springframework/social/facebook/api/ads/AdSetTemplateTest.java
@@ -0,0 +1,489 @@
+package org.springframework.social.facebook.api.ads;
+
+import org.junit.Test;
+import org.springframework.http.MediaType;
+import org.springframework.social.NotAuthorizedException;
+import org.springframework.social.facebook.api.PagedList;
+import org.springframework.social.facebook.api.ads.AdSet.AdSetStatus;
+
+import java.text.DateFormat;
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+import java.util.Arrays;
+
+import static org.junit.Assert.*;
+import static org.springframework.http.HttpMethod.GET;
+import static org.springframework.http.HttpMethod.POST;
+import static org.springframework.test.web.client.match.MockRestRequestMatchers.*;
+import static org.springframework.test.web.client.response.MockRestResponseCreators.withSuccess;
+
+/**
+ * @author Sebastian Górecki
+ */
+public class AdSetTemplateTest extends AbstractFacebookAdsApiTest {
+
+ @Test
+ public void getAdSets() throws Exception {
+ mockServer.expect(requestTo("https://graph.facebook.com/v2.3/act_123456789/adcampaigns?fields=account_id%2Cbid_info%2Cbid_type%2Cbudget_remaining%2Ccampaign_group_id%2Ccampaign_status%2Ccreated_time%2Ccreative_sequence%2Cdaily_budget%2Cend_time%2Cid%2Cis_autobid%2Clifetime_budget%2Cname%2Cpromoted_object%2Cstart_time%2Ctargeting%2Cupdated_time"))
+ .andExpect(method(GET))
+ .andExpect(header("Authorization", "OAuth someAccessToken"))
+ .andRespond(withSuccess(jsonResource("ad-sets"), MediaType.APPLICATION_JSON));
+
+ PagedList adSets = facebookAds.adSetOperations().getAccountAdSets("123456789");
+ verifyAdSets(adSets);
+ mockServer.verify();
+ }
+
+ @Test(expected = NotAuthorizedException.class)
+ public void getAdSets_unauthorized() throws Exception {
+ unauthorizedFacebookAds.adSetOperations().getAccountAdSets("123456789");
+ }
+
+ @Test
+ public void getCampaignAdsets() throws Exception {
+ mockServer.expect(requestTo("https://graph.facebook.com/v2.3/600123456789/adcampaigns?fields=account_id%2Cbid_info%2Cbid_type%2Cbudget_remaining%2Ccampaign_group_id%2Ccampaign_status%2Ccreated_time%2Ccreative_sequence%2Cdaily_budget%2Cend_time%2Cid%2Cis_autobid%2Clifetime_budget%2Cname%2Cpromoted_object%2Cstart_time%2Ctargeting%2Cupdated_time"))
+ .andExpect(method(GET))
+ .andExpect(header("Authorization", "OAuth someAccessToken"))
+ .andRespond(withSuccess(jsonResource("ad-sets"), MediaType.APPLICATION_JSON));
+
+ PagedList adSets = facebookAds.adSetOperations().getCampaignAdSets("600123456789");
+ verifyAdSets(adSets);
+ mockServer.verify();
+ }
+
+ @Test(expected = NotAuthorizedException.class)
+ public void getCampaignAdSets_unauthorized() throws Exception {
+ unauthorizedFacebookAds.adSetOperations().getCampaignAdSets("600123456789");
+ }
+
+ @Test
+ public void getAdSet() throws Exception {
+ mockServer.expect(requestTo("https://graph.facebook.com/v2.3/700123456789?fields=account_id%2Cbid_info%2Cbid_type%2Cbudget_remaining%2Ccampaign_group_id%2Ccampaign_status%2Ccreated_time%2Ccreative_sequence%2Cdaily_budget%2Cend_time%2Cid%2Cis_autobid%2Clifetime_budget%2Cname%2Cpromoted_object%2Cstart_time%2Ctargeting%2Cupdated_time"))
+ .andExpect(method(GET))
+ .andExpect(header("Authorization", "OAuth someAccessToken"))
+ .andRespond(withSuccess(jsonResource("ad-set"), MediaType.APPLICATION_JSON));
+ AdSet adSet = facebookAds.adSetOperations().getAdSet("700123456789");
+ assertEquals("700123456789", adSet.getId());
+ assertEquals("123456789", adSet.getAccountId());
+ assertEquals("600123456789", adSet.getCampaignId());
+ assertEquals("Test AdSet", adSet.getName());
+ assertEquals(AdSetStatus.ACTIVE, adSet.getStatus());
+ assertTrue(adSet.isAutobid());
+ assertEquals(BidType.ABSOLUTE_OCPM, adSet.getBidType());
+ assertEquals(50, adSet.getBudgetRemaining());
+ assertEquals(0, adSet.getDailyBudget());
+ assertEquals(200, adSet.getLifetimeBudget());
+ // targeting
+ assertEquals(Integer.valueOf(20), adSet.getTargeting().getAgeMax());
+ assertEquals(Integer.valueOf(18), adSet.getTargeting().getAgeMin());
+ assertEquals(1, adSet.getTargeting().getBehaviors().size());
+ assertEquals(6004854404172L, adSet.getTargeting().getBehaviors().get(0).getId());
+ assertEquals("Technology late adopters", adSet.getTargeting().getBehaviors().get(0).getName());
+ assertEquals(1, adSet.getTargeting().getGenders().size());
+ assertEquals(Targeting.Gender.MALE, adSet.getTargeting().getGenders().get(0));
+ assertEquals(1, adSet.getTargeting().getGeoLocations().getCountries().size());
+ assertEquals("PL", adSet.getTargeting().getGeoLocations().getCountries().get(0));
+ assertEquals(2, adSet.getTargeting().getGeoLocations().getRegions().size());
+ assertEquals("3847", adSet.getTargeting().getGeoLocations().getRegions().get(0));
+ assertEquals("1122", adSet.getTargeting().getGeoLocations().getRegions().get(1));
+ assertEquals(2, adSet.getTargeting().getGeoLocations().getCities().size());
+ assertEquals("2430536", adSet.getTargeting().getGeoLocations().getCities().get(0).getKey());
+ assertEquals(12, adSet.getTargeting().getGeoLocations().getCities().get(0).getRadius());
+ assertEquals("mile", adSet.getTargeting().getGeoLocations().getCities().get(0).getDistanceUnit());
+ assertEquals("11223344", adSet.getTargeting().getGeoLocations().getCities().get(1).getKey());
+ assertEquals(55, adSet.getTargeting().getGeoLocations().getCities().get(1).getRadius());
+ assertEquals("kilometer", adSet.getTargeting().getGeoLocations().getCities().get(1).getDistanceUnit());
+ assertEquals(2, adSet.getTargeting().getGeoLocations().getZips().size());
+ assertEquals("US:94304", adSet.getTargeting().getGeoLocations().getZips().get(0));
+ assertEquals("US:00501", adSet.getTargeting().getGeoLocations().getZips().get(1));
+ assertEquals(2, adSet.getTargeting().getGeoLocations().getLocationTypes().size());
+ assertEquals(TargetingLocation.LocationType.HOME, adSet.getTargeting().getGeoLocations().getLocationTypes().get(0));
+ assertEquals(TargetingLocation.LocationType.RECENT, adSet.getTargeting().getGeoLocations().getLocationTypes().get(1));
+ assertEquals(1, adSet.getTargeting().getInterests().size());
+ assertEquals(6003629266583L, adSet.getTargeting().getInterests().get(0).getId());
+ assertEquals("Hard drives", adSet.getTargeting().getInterests().get(0).getName());
+ assertEquals(2, adSet.getTargeting().getPageTypes().size());
+ assertEquals(Targeting.PageType.FEED, adSet.getTargeting().getPageTypes().get(0));
+ assertEquals(Targeting.PageType.DESKTOP_AND_MOBILE_AND_EXTERNAL, adSet.getTargeting().getPageTypes().get(1));
+ assertEquals(2, adSet.getTargeting().getRelationshipStatuses().size());
+ assertEquals(Targeting.RelationshipStatus.IN_RELATIONSHIP, adSet.getTargeting().getRelationshipStatuses().get(0));
+ assertEquals(Targeting.RelationshipStatus.IN_OPEN_RELATIONSHIP, adSet.getTargeting().getRelationshipStatuses().get(1));
+ assertEquals(1, adSet.getTargeting().getInterestedIn().size());
+ assertEquals(Targeting.InterestedIn.WOMEN, adSet.getTargeting().getInterestedIn().get(0));
+ assertEquals(1, adSet.getTargeting().getEducationSchools().size());
+ assertEquals(105930651606L, adSet.getTargeting().getEducationSchools().get(0).getId());
+ assertEquals("Harvard University", adSet.getTargeting().getEducationSchools().get(0).getName());
+ assertEquals(3, adSet.getTargeting().getEducationStatuses().size());
+ assertEquals(Targeting.EducationStatus.HIGH_SCHOOL, adSet.getTargeting().getEducationStatuses().get(0));
+ assertEquals(Targeting.EducationStatus.MASTER_DEGREE, adSet.getTargeting().getEducationStatuses().get(1));
+ assertEquals(Targeting.EducationStatus.SOME_HIGH_SCHOOL, adSet.getTargeting().getEducationStatuses().get(2));
+ assertEquals(1, adSet.getTargeting().getWorkEmployers().size());
+ assertEquals(50431654L, adSet.getTargeting().getWorkEmployers().get(0).getId());
+ assertEquals("Microsoft", adSet.getTargeting().getWorkEmployers().get(0).getName());
+ assertEquals(1, adSet.getTargeting().getWorkPositions().size());
+ assertEquals(105763692790962L, adSet.getTargeting().getWorkPositions().get(0).getId());
+ assertEquals("Business Analyst", adSet.getTargeting().getWorkPositions().get(0).getName());
+
+ assertEquals(toDate("2015-04-12T09:19:00+0200"), adSet.getStartTime());
+ assertEquals(toDate("2015-04-13T09:19:00+0200"), adSet.getEndTime());
+ assertEquals(toDate("2015-04-10T09:28:54+0200"), adSet.getCreatedTime());
+ assertEquals(toDate("2015-04-10T13:32:09+0200"), adSet.getUpdatedTime());
+ }
+
+ @Test
+ public void getAdSet_wrongStatus() throws Exception {
+ mockServer.expect(requestTo("https://graph.facebook.com/v2.3/709123456789?fields=account_id%2Cbid_info%2Cbid_type%2Cbudget_remaining%2Ccampaign_group_id%2Ccampaign_status%2Ccreated_time%2Ccreative_sequence%2Cdaily_budget%2Cend_time%2Cid%2Cis_autobid%2Clifetime_budget%2Cname%2Cpromoted_object%2Cstart_time%2Ctargeting%2Cupdated_time"))
+ .andExpect(method(GET))
+ .andExpect(header("Authorization", "OAuth someAccessToken"))
+ .andRespond(withSuccess(jsonResource("ad-set-wrong-status"), MediaType.APPLICATION_JSON));
+ AdSet adSet = facebookAds.adSetOperations().getAdSet("709123456789");
+ assertEquals("709123456789", adSet.getId());
+ assertEquals("123456789", adSet.getAccountId());
+ assertEquals("600123456789", adSet.getCampaignId());
+ assertEquals("Test AdSet", adSet.getName());
+ assertEquals(AdSetStatus.UNKNOWN, adSet.getStatus());
+ mockServer.verify();
+ }
+
+ @Test
+ public void getAdSet_wrongBidType() throws Exception {
+ mockServer.expect(requestTo("https://graph.facebook.com/v2.3/710123456789?fields=account_id%2Cbid_info%2Cbid_type%2Cbudget_remaining%2Ccampaign_group_id%2Ccampaign_status%2Ccreated_time%2Ccreative_sequence%2Cdaily_budget%2Cend_time%2Cid%2Cis_autobid%2Clifetime_budget%2Cname%2Cpromoted_object%2Cstart_time%2Ctargeting%2Cupdated_time"))
+ .andExpect(method(GET))
+ .andExpect(header("Authorization", "OAuth someAccessToken"))
+ .andRespond(withSuccess(jsonResource("ad-set-wrong-bid-type"), MediaType.APPLICATION_JSON));
+ AdSet adSet = facebookAds.adSetOperations().getAdSet("710123456789");
+ assertEquals("710123456789", adSet.getId());
+ assertEquals("123456789", adSet.getAccountId());
+ assertEquals("600123456789", adSet.getCampaignId());
+ assertEquals("Test AdSet", adSet.getName());
+ assertEquals(AdSetStatus.ACTIVE, adSet.getStatus());
+ assertEquals(BidType.UNKNOWN, adSet.getBidType());
+ mockServer.verify();
+ }
+
+ @Test
+ public void getAdSet_withPromotedObject() throws Exception {
+ mockServer.expect(requestTo("https://graph.facebook.com/v2.3/705123456789?fields=account_id%2Cbid_info%2Cbid_type%2Cbudget_remaining%2Ccampaign_group_id%2Ccampaign_status%2Ccreated_time%2Ccreative_sequence%2Cdaily_budget%2Cend_time%2Cid%2Cis_autobid%2Clifetime_budget%2Cname%2Cpromoted_object%2Cstart_time%2Ctargeting%2Cupdated_time"))
+ .andExpect(method(GET))
+ .andExpect(header("Authorization", "OAuth someAccessToken"))
+ .andRespond(withSuccess(jsonResource("ad-set-promoted-object"), MediaType.APPLICATION_JSON));
+ AdSet adSet = facebookAds.adSetOperations().getAdSet("705123456789");
+ assertEquals("705123456789", adSet.getId());
+ assertEquals("123456789", adSet.getAccountId());
+ assertEquals(Integer.valueOf(500), adSet.getBidInfo().get("CLICKS"));
+ assertEquals(BidType.ABSOLUTE_OCPM, adSet.getBidType());
+ assertEquals(807, adSet.getBudgetRemaining());
+ assertEquals("600123456789", adSet.getCampaignId());
+ assertEquals(AdSetStatus.PAUSED, adSet.getStatus());
+ assertEquals(2000, adSet.getDailyBudget());
+ assertFalse(adSet.isAutobid());
+ assertEquals(0, adSet.getLifetimeBudget());
+ assertEquals("Test promoted object", adSet.getName());
+ assertEquals("999888777666555", adSet.getPromotedObject().get("page_id"));
+ assertEquals(toDate("2015-07-06T14:18:55+0200"), adSet.getCreatedTime());
+ assertEquals(toDate("2015-07-06T14:18:55+0200"), adSet.getStartTime());
+ assertEquals(toDate("2015-07-06T14:18:55+0200"), adSet.getUpdatedTime());
+ mockServer.verify();
+ }
+
+ @Test(expected = NotAuthorizedException.class)
+ public void getAdSet_unauthorized() throws Exception {
+ unauthorizedFacebookAds.adSetOperations().getAdSet("700123456789");
+ }
+
+ @Test
+ public void getAdSetInsights() throws Exception {
+ mockServer.expect(requestTo("https://graph.facebook.com/v2.3/700123456789/insights?fields=account_id%2Caccount_name%2Cdate_start%2Cdate_stop%2Cactions_per_impression%2Cclicks%2Cunique_clicks%2Ccost_per_result%2Ccost_per_total_action%2Ccpc%2Ccost_per_unique_click%2Ccpm%2Ccpp%2Cctr%2Cunique_ctr%2Cfrequency%2Cimpressions%2Cunique_impressions%2Cobjective%2Creach%2Cresult_rate%2Cresults%2Croas%2Csocial_clicks%2Cunique_social_clicks%2Csocial_impressions%2Cunique_social_impressions%2Csocial_reach%2Cspend%2Ctoday_spend%2Ctotal_action_value%2Ctotal_actions%2Ctotal_unique_actions%2Cactions%2Cunique_actions%2Ccost_per_action_type%2Cvideo_start_actions"))
+ .andExpect(method(GET))
+ .andExpect(header("Authorization", "OAuth someAccessToken"))
+ .andRespond(withSuccess(jsonResource("ad-set-insights"), MediaType.APPLICATION_JSON));
+
+ AdInsight insight = facebookAds.adSetOperations().getAdSetInsight("700123456789");
+
+ assertEquals("123456789", insight.getAccountId());
+ assertEquals("Test account name", insight.getAccountName());
+ assertEquals(0.016042780748663, insight.getActionsPerImpression(), EPSILON);
+ assertEquals(8, insight.getClicks());
+ assertEquals(5, insight.getUniqueClicks());
+ assertEquals(0.66666666666667, insight.getCostPerResult(), EPSILON);
+ assertEquals(0.66666666666667, insight.getCostPerTotalAction(), EPSILON);
+ assertEquals(0.25, insight.getCostPerClick(), EPSILON);
+ assertEquals(0.4, insight.getCostPerUniqueClick(), EPSILON);
+ assertEquals(10.695187165775, insight.getCpm(), EPSILON);
+ assertEquals(10.869565217391, insight.getCpp(), EPSILON);
+ assertEquals(4.2780748663102, insight.getCtr(), EPSILON);
+ assertEquals(2.7173913043478, insight.getUniqueCtr(), EPSILON);
+ assertEquals(1.0163043478261, insight.getFrequency(), EPSILON);
+ assertEquals(187, insight.getImpressions());
+ assertEquals(184, insight.getUniqueImpressions());
+ assertEquals(184, insight.getReach());
+ assertEquals(1.6042780748663, insight.getResultRate(), EPSILON);
+ assertEquals(3, insight.getResults());
+ assertEquals(0, insight.getRoas());
+ assertEquals(0, insight.getSocialClicks());
+ assertEquals(0, insight.getUniqueSocialClicks());
+ assertEquals(0, insight.getSocialImpressions());
+ assertEquals(0, insight.getUniqueSocialImpressions());
+ assertEquals(0, insight.getSocialReach());
+ assertEquals(2, insight.getSpend());
+ assertEquals(0, insight.getTodaySpend());
+ assertEquals(0, insight.getTotalActionValue());
+ assertEquals(3, insight.getTotalActions());
+ assertEquals(2, insight.getTotalUniqueActions());
+ assertEquals(4, insight.getActions().size());
+ assertEquals("comment", insight.getActions().get(0).getActionType());
+ assertEquals(2, insight.getActions().get(0).getValue(), EPSILON);
+ assertEquals("post_like", insight.getActions().get(1).getActionType());
+ assertEquals(1, insight.getActions().get(1).getValue(), EPSILON);
+ assertEquals("page_engagement", insight.getActions().get(2).getActionType());
+ assertEquals(3, insight.getActions().get(2).getValue(), EPSILON);
+ assertEquals("post_engagement", insight.getActions().get(3).getActionType());
+ assertEquals(3, insight.getActions().get(3).getValue(), EPSILON);
+ assertEquals(4, insight.getUniqueActions().size());
+ assertEquals("comment", insight.getUniqueActions().get(0).getActionType());
+ assertEquals(1, insight.getUniqueActions().get(0).getValue(), EPSILON);
+ assertEquals("post_like", insight.getUniqueActions().get(1).getActionType());
+ assertEquals(1, insight.getUniqueActions().get(1).getValue(), EPSILON);
+ assertEquals("page_engagement", insight.getUniqueActions().get(2).getActionType());
+ assertEquals(2, insight.getUniqueActions().get(2).getValue(), EPSILON);
+ assertEquals("post_engagement", insight.getUniqueActions().get(3).getActionType());
+ assertEquals(2, insight.getUniqueActions().get(3).getValue(), EPSILON);
+ assertEquals(4, insight.getCostPerActionType().size());
+ assertEquals("comment", insight.getCostPerActionType().get(0).getActionType());
+ assertEquals(1, insight.getCostPerActionType().get(0).getValue(), EPSILON);
+ assertEquals("post_like", insight.getCostPerActionType().get(1).getActionType());
+ assertEquals(2, insight.getCostPerActionType().get(1).getValue(), EPSILON);
+ assertEquals("page_engagement", insight.getCostPerActionType().get(2).getActionType());
+ assertEquals(0.66666666666667, insight.getCostPerActionType().get(2).getValue(), EPSILON);
+ assertEquals("post_engagement", insight.getCostPerActionType().get(3).getActionType());
+ assertEquals(0.66666666666667, insight.getCostPerActionType().get(3).getValue(), EPSILON);
+ assertEquals(1, insight.getVideoStartActions().size());
+ assertEquals("video_view", insight.getVideoStartActions().get(0).getActionType());
+ assertEquals(0, insight.getVideoStartActions().get(0).getValue(), EPSILON);
+
+ mockServer.verify();
+ }
+
+ @Test(expected = NotAuthorizedException.class)
+ public void getAdSetInsights_unauthorized() throws Exception {
+ unauthorizedFacebookAds.adSetOperations().getAdSetInsight("700123456789");
+ }
+
+ @Test
+ public void createAdSet() throws Exception {
+ String requestBody = "date_format=U&name=Test+AdSet&campaign_status=PAUSED&is_autobid=true&bid_type=ABSOLUTE_OCPM&daily_budget=0&lifetime_budget=200&targeting=%7B%22geo_locations%22%3A%7B%22countries%22%3A%5B%22PL%22%5D%7D%7D&end_time=1432231200&campaign_group_id=600123456789";
+ mockServer.expect(requestTo("https://graph.facebook.com/v2.3/act_123456789/adcampaigns"))
+ .andExpect(method(POST))
+ .andExpect(header("Authorization", "OAuth someAccessToken"))
+ .andExpect(content().string(requestBody))
+ .andRespond(withSuccess("{\"id\": \"701123456789\"}", MediaType.APPLICATION_JSON));
+ AdSet adSet = createSampleAdSet();
+
+ assertEquals("701123456789", facebookAds.adSetOperations().createAdSet("123456789", adSet));
+ mockServer.verify();
+ }
+
+ @Test
+ public void createAdSet_withAllFields() throws Exception {
+ String requestBody = "date_format=U&name=Test+AdSet+2&campaign_status=ACTIVE&is_autobid=false&" +
+ "bid_info=%7B%22REACH%22%3A1000%2C%22ACTIONS%22%3A200%2C%22SOCIAL%22%3A110%2C%22CLICKS%22%3A500%7D&" +
+ "bid_type=ABSOLUTE_OCPM&daily_budget=4000&lifetime_budget=0&" +
+ "targeting=%7B%22genders%22%3A%5B1%2C2%5D%2C%22age_min%22%3A45%2C%22age_max%22%3A55%2C%22relationship_statuses%22%3A%5B10%2C12%5D%2C%22interested_in%22%3A%5B1%2C2%5D%2C%22geo_locations%22%3A%7B%22countries%22%3A%5B%22PL%22%2C%22DE%22%2C%22US%22%2C%22FR%22%5D%2C%22regions%22%3A%5B%7B%22key%22%3A%223847%22%7D%2C%7B%22key%22%3A%221111%22%7D%2C%7B%22key%22%3A%221234%22%7D%2C%7B%22key%22%3A%229888%22%7D%5D%2C%22cities%22%3A%5B%7B%22key%22%3A%222430536%22%2C%22radius%22%3A12%2C%22distance_unit%22%3A%22mile%22%7D%2C%7B%22key%22%3A%22777555%22%2C%22radius%22%3A1024%2C%22distance_unit%22%3A%22kilometer%22%7D%5D%2C%22zips%22%3A%5B%7B%22key%22%3A%22PL%3A62030%22%7D%2C%7B%22key%22%3A%22US%3A88123%22%7D%2C%7B%22key%22%3A%22FR%3A33144%22%7D%5D%2C%22location_types%22%3A%5B%22home%22%2C%22recent%22%5D%7D%2C%22excluded_geo_locations%22%3A%7B%22countries%22%3A%5B%22HU%22%2C%22JP%22%5D%2C%22regions%22%3A%5B%7B%22key%22%3A%221122%22%7D%2C%7B%22key%22%3A%2231415%22%7D%5D%2C%22cities%22%3A%5B%7B%22key%22%3A%2288997766%22%2C%22radius%22%3A12345%2C%22distance_unit%22%3A%22mile%22%7D%5D%2C%22zips%22%3A%5B%7B%22key%22%3A%22JP%3A44552%22%7D%5D%2C%22location_types%22%3A%5B%22home%22%5D%7D%2C%22page_types%22%3A%5B%22desktopfeed%22%2C%22mobilefeed-and-external%22%5D%2C%22connections%22%3A%5B%22123456789%22%2C%2255442211%22%5D%2C%22excluded_connections%22%3A%5B%2233441122%22%5D%2C%22friends_of_connections%22%3A%5B%22987654321%22%5D%2C%22interests%22%3A%5B%7B%22id%22%3A986123123123%2C%22name%22%3A%22Football%22%7D%5D%2C%22behaviors%22%3A%5B%7B%22id%22%3A1%2C%22name%22%3A%22Some+behavior%22%7D%5D%2C%22education_schools%22%3A%5B%7B%22id%22%3A10593123549%2C%22name%22%3A%22Poznan+University+of+Technology%22%7D%5D%2C%22education_statuses%22%3A%5B9%5D%2C%22college_years%22%3A%5B8%5D%2C%22education_majors%22%3A%5B%7B%22id%22%3A12%2C%22name%22%3A%22Some+major%22%7D%5D%2C%22work_employers%22%3A%5B%7B%22id%22%3A43125%2C%22name%22%3A%22Super+company%22%7D%5D%2C%22work_positions%22%3A%5B%7B%22id%22%3A11111%2C%22name%22%3A%22Developer%22%7D%5D%7D&" +
+ "start_time=1432742400&end_time=1435420799&campaign_group_id=601123456789";
+ mockServer.expect(requestTo("https://graph.facebook.com/v2.3/act_123456789/adcampaigns"))
+ .andExpect(method(POST))
+ .andExpect(header("Authorization", "OAuth someAccessToken"))
+ .andExpect(content().string(requestBody))
+ .andRespond(withSuccess("{\"id\": \"702123456789\"}", MediaType.APPLICATION_JSON));
+ AdSet adSet = new AdSet();
+ adSet.setCampaignId("601123456789");
+ adSet.setName("Test AdSet 2");
+ adSet.setStatus(AdSetStatus.ACTIVE);
+ adSet.setAutobid(false);
+ BidInfo bidInfo = new BidInfo();
+ bidInfo.put("ACTIONS", 200);
+ bidInfo.put("REACH", 1000);
+ bidInfo.put("CLICKS", 500);
+ bidInfo.put("SOCIAL", 110);
+ adSet.setBidInfo(bidInfo);
+ adSet.setBidType(BidType.ABSOLUTE_OCPM);
+ adSet.setDailyBudget(4000);
+ adSet.setLifetimeBudget(0);
+ // targeting
+ Targeting targeting = new Targeting();
+ targeting.setGenders(Arrays.asList(Targeting.Gender.MALE, Targeting.Gender.FEMALE));
+ targeting.setAgeMin(45);
+ targeting.setAgeMax(55);
+ targeting.setRelationshipStatuses(Arrays.asList(Targeting.RelationshipStatus.ITS_COMPLICATED, Targeting.RelationshipStatus.DIVORCED));
+ targeting.setInterestedIn(Arrays.asList(Targeting.InterestedIn.MEN, Targeting.InterestedIn.WOMEN));
+ // targeting - geoLocations
+ TargetingLocation geoLocation = new TargetingLocation();
+ geoLocation.setCountries(Arrays.asList("PL", "DE", "US", "FR"));
+ geoLocation.setRegions(Arrays.asList("3847", "1111", "1234", "9888"));
+ geoLocation.setCities(Arrays.asList(new TargetingCityEntry("2430536", 12, "mile"), new TargetingCityEntry("777555", 1024, "kilometer")));
+ geoLocation.setZips(Arrays.asList("PL:62030", "US:88123", "FR:33144"));
+ geoLocation.setLocationTypes(Arrays.asList(TargetingLocation.LocationType.HOME, TargetingLocation.LocationType.RECENT));
+ targeting.setGeoLocations(geoLocation);
+ // targeting - excludedGeoLocations
+ TargetingLocation excludedGeoLocations = new TargetingLocation();
+ excludedGeoLocations.setCountries(Arrays.asList("HU", "JP"));
+ excludedGeoLocations.setRegions(Arrays.asList("1122", "31415"));
+ excludedGeoLocations.setCities(Arrays.asList(new TargetingCityEntry("88997766", 12345, "mile")));
+ excludedGeoLocations.setZips(Arrays.asList("JP:44552"));
+ excludedGeoLocations.setLocationTypes(Arrays.asList(TargetingLocation.LocationType.HOME));
+ targeting.setExcludedGeoLocations(excludedGeoLocations);
+ // targeting cd.
+ targeting.setPageTypes(Arrays.asList(Targeting.PageType.DESKTOP_FEED, Targeting.PageType.MOBILE_FEED_AND_EXTERNAL));
+ targeting.setConnections(Arrays.asList("123456789", "55442211"));
+ targeting.setExcludedConnections(Arrays.asList("33441122"));
+ targeting.setFriendsOfConnections(Arrays.asList("987654321"));
+ targeting.setInterests(Arrays.asList(new TargetingEntry(986123123123L, "Football")));
+ targeting.setBehaviors(Arrays.asList(new TargetingEntry(1L, "Some behavior")));
+ targeting.setEducationSchools(Arrays.asList(new TargetingEntry(10593123549L, "Poznan University of Technology")));
+ targeting.setEducationStatuses(Arrays.asList(Targeting.EducationStatus.MASTER_DEGREE));
+ targeting.setCollegeYears(Arrays.asList(Integer.valueOf(8)));
+ targeting.setEducationMajors(Arrays.asList(new TargetingEntry(12L, "Some major")));
+ targeting.setWorkEmployers(Arrays.asList(new TargetingEntry(43125L, "Super company")));
+ targeting.setWorkPositions(Arrays.asList(new TargetingEntry(11111L, "Developer")));
+ adSet.setTargeting(targeting);
+
+ DateFormat formatter = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
+ adSet.setStartTime(formatter.parse("2015-05-27 18:00:00"));
+ adSet.setEndTime(formatter.parse("2015-06-27 17:59:59"));
+
+ assertEquals("702123456789", facebookAds.adSetOperations().createAdSet("123456789", adSet));
+ mockServer.verify();
+ }
+
+ @Test
+ public void createAdSet_withPromotedObject() throws Exception {
+ String requestBody = "date_format=U&name=Test+AdSet&campaign_status=PAUSED&is_autobid=true&bid_type=ABSOLUTE_OCPM&daily_budget=0&lifetime_budget=200&promoted_object=%7B%22page_id%22%3A%22111222333444555%22%7D&targeting=%7B%22geo_locations%22%3A%7B%22countries%22%3A%5B%22PL%22%5D%7D%7D&end_time=1432231200&campaign_group_id=600123456789";
+ mockServer.expect(requestTo("https://graph.facebook.com/v2.3/act_123456789/adcampaigns"))
+ .andExpect(method(POST))
+ .andExpect(header("Authorization", "OAuth someAccessToken"))
+ .andExpect(content().string(requestBody))
+ .andRespond(withSuccess("{\"id\": \"702123456789\"}", MediaType.APPLICATION_JSON));
+
+ AdSet adSet = createSampleAdSet();
+ PromotedObject promotedObject = new PromotedObject();
+ promotedObject.put("page_id", "111222333444555");
+ adSet.setPromotedObject(promotedObject);
+
+ assertEquals("702123456789", facebookAds.adSetOperations().createAdSet("123456789", adSet));
+ mockServer.verify();
+ }
+
+ @Test(expected = NotAuthorizedException.class)
+ public void createAdSet_unauthorized() throws Exception {
+ unauthorizedFacebookAds.adSetOperations().createAdSet("123456789", new AdSet());
+ }
+
+ @Test
+ public void updateAdSet() throws Exception {
+ String requestBody = "date_format=U&name=New+AdSet+name&campaign_status=ARCHIVED&is_autobid=true&daily_budget=0&lifetime_budget=50000&start_time=1432833720";
+ mockServer.expect(requestTo("https://graph.facebook.com/v2.3/700123456789"))
+ .andExpect(method(POST))
+ .andExpect(header("Authorization", "OAuth someAccessToken"))
+ .andExpect(content().string(requestBody))
+ .andRespond(withSuccess("{\"success\": true}", MediaType.APPLICATION_JSON));
+
+ AdSet adSet = new AdSet();
+ adSet.setName("New AdSet name");
+ adSet.setStatus(AdSetStatus.ARCHIVED);
+ adSet.setLifetimeBudget(50000);
+ adSet.setAutobid(true);
+ DateFormat formatter = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
+ adSet.setStartTime(formatter.parse("2015-05-28 19:22:00"));
+
+ assertTrue(facebookAds.adSetOperations().updateAdSet("700123456789", adSet));
+ mockServer.verify();
+ }
+
+ @Test(expected = NotAuthorizedException.class)
+ public void updateAdSet_unauthorized() throws Exception {
+ unauthorizedFacebookAds.adSetOperations().updateAdSet("700123456789", new AdSet());
+ }
+
+ @Test
+ public void deleteAdSet() throws Exception {
+ String requestBody = "campaign_status=DELETED";
+ mockServer.expect(requestTo("https://graph.facebook.com/v2.3/700123456789"))
+ .andExpect(method(POST))
+ .andExpect(header("Authorization", "OAuth someAccessToken"))
+ .andExpect(content().string(requestBody))
+ .andRespond(withSuccess("{\"status\": \"true\"}", MediaType.APPLICATION_JSON));
+ facebookAds.adSetOperations().deleteAdSet("700123456789");
+ mockServer.verify();
+ }
+
+ @Test(expected = NotAuthorizedException.class)
+ public void deleteAdSet_unauthorized() throws Exception {
+ unauthorizedFacebookAds.adSetOperations().deleteAdSet("700123456789");
+ }
+
+ private void verifyAdSets(PagedList adSets) {
+ assertEquals(2, adSets.size());
+ assertEquals("123456789", adSets.get(0).getAccountId());
+ assertEquals(BidType.ABSOLUTE_OCPM, adSets.get(0).getBidType());
+ assertEquals(37407, adSets.get(0).getBudgetRemaining());
+ assertEquals("600123456789", adSets.get(0).getCampaignId());
+ assertEquals(AdSetStatus.PAUSED, adSets.get(0).getStatus());
+ assertEquals(toDate("2015-05-27T11:58:34+0200"), adSets.get(0).getCreatedTime());
+ assertEquals(40000, adSets.get(0).getDailyBudget());
+ assertEquals(toDate("2015-05-29T22:26:40+0200"), adSets.get(0).getEndTime());
+ assertEquals("700123456789", adSets.get(0).getId());
+ assertTrue(adSets.get(0).isAutobid());
+ assertEquals(0, adSets.get(0).getLifetimeBudget());
+ assertEquals("Test AdSet", adSets.get(0).getName());
+ assertEquals(toDate("2015-05-27T11:58:34+0200"), adSets.get(0).getStartTime());
+ assertEquals(Integer.valueOf(65), adSets.get(0).getTargeting().getAgeMax());
+ assertEquals(Integer.valueOf(18), adSets.get(0).getTargeting().getAgeMin());
+ assertEquals("BR", adSets.get(0).getTargeting().getGeoLocations().getCountries().get(0));
+ assertEquals(TargetingLocation.LocationType.HOME, adSets.get(0).getTargeting().getGeoLocations().getLocationTypes().get(0));
+ assertEquals(toDate("2015-05-27T11:58:34+0200"), adSets.get(0).getUpdatedTime());
+
+ assertEquals("123456789", adSets.get(1).getAccountId());
+ assertEquals(BidType.ABSOLUTE_OCPM, adSets.get(1).getBidType());
+ assertEquals(0, adSets.get(1).getBudgetRemaining());
+ assertEquals("600123456789", adSets.get(1).getCampaignId());
+ assertEquals(AdSetStatus.ACTIVE, adSets.get(1).getStatus());
+ assertEquals(toDate("2015-04-10T09:28:54+0200"), adSets.get(1).getCreatedTime());
+ assertEquals(0, adSets.get(1).getDailyBudget());
+ assertEquals(toDate("2015-04-13T09:19:00+0200"), adSets.get(1).getEndTime());
+ assertEquals("701123456789", adSets.get(1).getId());
+ assertTrue(adSets.get(1).isAutobid());
+ assertEquals(200, adSets.get(1).getLifetimeBudget());
+ assertEquals("Real ad set", adSets.get(1).getName());
+ assertEquals(toDate("2015-04-12T09:19:00+0200"), adSets.get(1).getStartTime());
+ assertEquals(Integer.valueOf(20), adSets.get(1).getTargeting().getAgeMax());
+ assertEquals(Integer.valueOf(18), adSets.get(1).getTargeting().getAgeMin());
+ assertEquals(6004854404172L, adSets.get(1).getTargeting().getBehaviors().get(0).getId());
+ assertEquals("Technology late adopters", adSets.get(1).getTargeting().getBehaviors().get(0).getName());
+ assertEquals(Targeting.Gender.MALE, adSets.get(1).getTargeting().getGenders().get(0));
+ assertEquals("PL", adSets.get(1).getTargeting().getGeoLocations().getCountries().get(0));
+ assertEquals(TargetingLocation.LocationType.HOME, adSets.get(1).getTargeting().getGeoLocations().getLocationTypes().get(0));
+ assertEquals(TargetingLocation.LocationType.RECENT, adSets.get(1).getTargeting().getGeoLocations().getLocationTypes().get(1));
+ assertEquals(6003629266583L, adSets.get(1).getTargeting().getInterests().get(0).getId());
+ assertEquals("Hard drives", adSets.get(1).getTargeting().getInterests().get(0).getName());
+ assertEquals(Targeting.PageType.FEED, adSets.get(1).getTargeting().getPageTypes().get(0));
+ assertEquals(toDate("2015-04-10T13:32:09+0200"), adSets.get(1).getUpdatedTime());
+ }
+
+ private AdSet createSampleAdSet() throws ParseException {
+ AdSet adSet = new AdSet();
+ adSet.setAutobid(true);
+ adSet.setBidType(BidType.ABSOLUTE_OCPM);
+ adSet.setCampaignId("600123456789");
+ adSet.setStatus(AdSetStatus.PAUSED);
+ DateFormat formatter = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
+ adSet.setEndTime(formatter.parse("2015-05-21 20:00:00"));
+ adSet.setName("Test AdSet");
+ TargetingLocation location = new TargetingLocation();
+ location.setCountries(Arrays.asList("PL"));
+ Targeting targeting = new Targeting();
+ targeting.setGeoLocations(location);
+ adSet.setTargeting(targeting);
+ adSet.setLifetimeBudget(200);
+ return adSet;
+ }
+}
diff --git a/spring-social-facebook/src/test/java/org/springframework/social/facebook/api/ads/AdTemplateTest.java b/spring-social-facebook/src/test/java/org/springframework/social/facebook/api/ads/AdTemplateTest.java
new file mode 100644
index 000000000..e97b62aa2
--- /dev/null
+++ b/spring-social-facebook/src/test/java/org/springframework/social/facebook/api/ads/AdTemplateTest.java
@@ -0,0 +1,330 @@
+package org.springframework.social.facebook.api.ads;
+
+import org.junit.Assert;
+import org.junit.Test;
+import org.springframework.http.MediaType;
+import org.springframework.social.NotAuthorizedException;
+import org.springframework.social.facebook.api.PagedList;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+import static org.springframework.http.HttpMethod.*;
+import static org.springframework.test.web.client.match.MockRestRequestMatchers.*;
+import static org.springframework.test.web.client.response.MockRestResponseCreators.withSuccess;
+
+/**
+ * @author Sebastian Górecki
+ */
+public class AdTemplateTest extends AbstractFacebookAdsApiTest {
+
+ @Test
+ public void getAccountAds() throws Exception {
+ mockServer.expect(requestTo("https://graph.facebook.com/v2.3/act_123456789/adgroups?fields=id%2Caccount_id%2Cadgroup_status%2Cbid_type%2Cbid_info%2Ccampaign_id%2Ccampaign_group_id%2Ccreated_time%2Ccreative%2Cname%2Ctargeting%2Cupdated_time"))
+ .andExpect(method(GET))
+ .andExpect(header("Authorization", "OAuth someAccessToken"))
+ .andRespond(withSuccess(jsonResource("ad-same-account"), MediaType.APPLICATION_JSON));
+
+ PagedList accountAds = facebookAds.adOperations().getAccountAds("123456789");
+ assertEquals(3, accountAds.size());
+ assertEquals("101123456789", accountAds.get(0).getId());
+ assertEquals("801123456789", accountAds.get(0).getAdSetId());
+ assertEquals("701123456789", accountAds.get(0).getCampaignId());
+ assertEquals("123456789", accountAds.get(0).getAccountId());
+ assertEquals("102123456789", accountAds.get(1).getId());
+ assertEquals("802123456789", accountAds.get(1).getAdSetId());
+ assertEquals("702123456789", accountAds.get(1).getCampaignId());
+ assertEquals("123456789", accountAds.get(1).getAccountId());
+ assertEquals("103123456789", accountAds.get(2).getId());
+ assertEquals("803123456789", accountAds.get(2).getAdSetId());
+ assertEquals("703123456789", accountAds.get(2).getCampaignId());
+ assertEquals("123456789", accountAds.get(2).getAccountId());
+ mockServer.verify();
+ }
+
+ @Test(expected = NotAuthorizedException.class)
+ public void getAccountAds_unauthorized() throws Exception {
+ unauthorizedFacebookAds.adOperations().getAccountAds("123456789");
+ }
+
+ @Test
+ public void getCampaignAds() throws Exception {
+ mockServer.expect(requestTo("https://graph.facebook.com/v2.3/701123456789/adgroups?fields=id%2Caccount_id%2Cadgroup_status%2Cbid_type%2Cbid_info%2Ccampaign_id%2Ccampaign_group_id%2Ccreated_time%2Ccreative%2Cname%2Ctargeting%2Cupdated_time"))
+ .andExpect(method(GET))
+ .andExpect(header("Authorization", "OAuth someAccessToken"))
+ .andRespond(withSuccess(jsonResource("ad-same-campaign"), MediaType.APPLICATION_JSON));
+
+ PagedList campaignAds = facebookAds.adOperations().getCampaignAds("701123456789");
+ assertEquals(3, campaignAds.size());
+ assertEquals("101123456789", campaignAds.get(0).getId());
+ assertEquals("801123456789", campaignAds.get(0).getAdSetId());
+ assertEquals("701123456789", campaignAds.get(0).getCampaignId());
+ assertEquals("102123456789", campaignAds.get(1).getId());
+ assertEquals("802123456789", campaignAds.get(1).getAdSetId());
+ assertEquals("701123456789", campaignAds.get(1).getCampaignId());
+ assertEquals("103123456789", campaignAds.get(2).getId());
+ assertEquals("803123456789", campaignAds.get(2).getAdSetId());
+ assertEquals("701123456789", campaignAds.get(2).getCampaignId());
+ mockServer.verify();
+ }
+
+ @Test(expected = NotAuthorizedException.class)
+ public void getCampaignAds_unauthorized() throws Exception {
+ unauthorizedFacebookAds.adOperations().getCampaignAds("701123456789");
+ }
+
+ @Test
+ public void getAdSetAds() throws Exception {
+ mockServer.expect(requestTo("https://graph.facebook.com/v2.3/800123456789/adgroups?fields=id%2Caccount_id%2Cadgroup_status%2Cbid_type%2Cbid_info%2Ccampaign_id%2Ccampaign_group_id%2Ccreated_time%2Ccreative%2Cname%2Ctargeting%2Cupdated_time"))
+ .andExpect(method(GET))
+ .andExpect(header("Authorization", "OAuth someAccessToken"))
+ .andRespond(withSuccess(jsonResource("ad-same-ad-set"), MediaType.APPLICATION_JSON));
+
+ PagedList adSetAds = facebookAds.adOperations().getAdSetAds("800123456789");
+ assertEquals(2, adSetAds.size());
+ assertEquals("101123456789", adSetAds.get(0).getId());
+ assertEquals("800123456789", adSetAds.get(0).getAdSetId());
+ assertEquals("700123456789", adSetAds.get(0).getCampaignId());
+
+ assertEquals("102123456789", adSetAds.get(1).getId());
+ assertEquals("800123456789", adSetAds.get(1).getAdSetId());
+ assertEquals("701123456789", adSetAds.get(1).getCampaignId());
+ mockServer.verify();
+ }
+
+ @Test(expected = NotAuthorizedException.class)
+ public void getAdSetAds_unauthorized() throws Exception {
+ unauthorizedFacebookAds.adOperations().getAdSetAds("800123456789");
+ }
+
+ @Test
+ public void getAd() throws Exception {
+ mockServer.expect(requestTo("https://graph.facebook.com/v2.3/100123456789?fields=id%2Caccount_id%2Cadgroup_status%2Cbid_type%2Cbid_info%2Ccampaign_id%2Ccampaign_group_id%2Ccreated_time%2Ccreative%2Cname%2Ctargeting%2Cupdated_time"))
+ .andExpect(method(GET))
+ .andExpect(header("Authorization", "OAuth someAccessToken"))
+ .andRespond(withSuccess(jsonResource("ad"), MediaType.APPLICATION_JSON));
+
+ Ad ad = facebookAds.adOperations().getAd("100123456789");
+ verifyAd(ad);
+ assertEquals(Ad.AdStatus.ACTIVE, ad.getStatus());
+ assertEquals(BidType.ABSOLUTE_OCPM, ad.getBidType());
+ mockServer.verify();
+ }
+
+ @Test
+ public void getAd_withWrongStatus() throws Exception {
+ mockServer.expect(requestTo("https://graph.facebook.com/v2.3/100123456789?fields=id%2Caccount_id%2Cadgroup_status%2Cbid_type%2Cbid_info%2Ccampaign_id%2Ccampaign_group_id%2Ccreated_time%2Ccreative%2Cname%2Ctargeting%2Cupdated_time"))
+ .andExpect(method(GET))
+ .andExpect(header("Authorization", "OAuth someAccessToken"))
+ .andRespond(withSuccess(jsonResource("ad-wrong-status"), MediaType.APPLICATION_JSON));
+
+ Ad ad = facebookAds.adOperations().getAd("100123456789");
+ verifyAd(ad);
+ assertEquals(Ad.AdStatus.UNKNOWN, ad.getStatus());
+ assertEquals(BidType.ABSOLUTE_OCPM, ad.getBidType());
+ mockServer.verify();
+ }
+
+ @Test
+ public void getAd_withWrongBidType() throws Exception {
+ mockServer.expect(requestTo("https://graph.facebook.com/v2.3/100123456789?fields=id%2Caccount_id%2Cadgroup_status%2Cbid_type%2Cbid_info%2Ccampaign_id%2Ccampaign_group_id%2Ccreated_time%2Ccreative%2Cname%2Ctargeting%2Cupdated_time"))
+ .andExpect(method(GET))
+ .andExpect(header("Authorization", "OAuth someAccessToken"))
+ .andRespond(withSuccess(jsonResource("ad-wrong-bid-type"), MediaType.APPLICATION_JSON));
+
+ Ad ad = facebookAds.adOperations().getAd("100123456789");
+ verifyAd(ad);
+ assertEquals(Ad.AdStatus.ACTIVE, ad.getStatus());
+ assertEquals(BidType.UNKNOWN, ad.getBidType());
+ mockServer.verify();
+ }
+
+ @Test(expected = NotAuthorizedException.class)
+ public void getAd_unauthorized() throws Exception {
+ unauthorizedFacebookAds.adOperations().getAd("100123456789");
+ }
+
+ @Test
+ public void getAdInsight() throws Exception {
+ mockServer.expect(requestTo("https://graph.facebook.com/v2.3/100123456789/insights?fields=account_id%2Caccount_name%2Cdate_start%2Cdate_stop%2Cactions_per_impression%2Cclicks%2Cunique_clicks%2Ccost_per_result%2Ccost_per_total_action%2Ccpc%2Ccost_per_unique_click%2Ccpm%2Ccpp%2Cctr%2Cunique_ctr%2Cfrequency%2Cimpressions%2Cunique_impressions%2Cobjective%2Creach%2Cresult_rate%2Cresults%2Croas%2Csocial_clicks%2Cunique_social_clicks%2Csocial_impressions%2Cunique_social_impressions%2Csocial_reach%2Cspend%2Ctoday_spend%2Ctotal_action_value%2Ctotal_actions%2Ctotal_unique_actions%2Cactions%2Cunique_actions%2Ccost_per_action_type%2Cvideo_start_actions"))
+ .andExpect(method(GET))
+ .andExpect(header("Authorization", "OAuth someAccessToken"))
+ .andRespond(withSuccess(jsonResource("ad-insights"), MediaType.APPLICATION_JSON));
+
+ AdInsight insight = facebookAds.adOperations().getAdInsight("100123456789");
+ Assert.assertEquals("123456789", insight.getAccountId());
+ Assert.assertEquals("Test account name", insight.getAccountName());
+ Assert.assertEquals(0.016042780748663, insight.getActionsPerImpression(), EPSILON);
+ Assert.assertEquals(8, insight.getClicks());
+ Assert.assertEquals(5, insight.getUniqueClicks());
+ Assert.assertEquals(0.66666666666667, insight.getCostPerResult(), EPSILON);
+ Assert.assertEquals(0.66666666666667, insight.getCostPerTotalAction(), EPSILON);
+ Assert.assertEquals(0.25, insight.getCostPerClick(), EPSILON);
+ Assert.assertEquals(0.4, insight.getCostPerUniqueClick(), EPSILON);
+ Assert.assertEquals(10.695187165775, insight.getCpm(), EPSILON);
+ Assert.assertEquals(10.869565217391, insight.getCpp(), EPSILON);
+ Assert.assertEquals(4.2780748663102, insight.getCtr(), EPSILON);
+ Assert.assertEquals(2.7173913043478, insight.getUniqueCtr(), EPSILON);
+ Assert.assertEquals(1.0163043478261, insight.getFrequency(), EPSILON);
+ Assert.assertEquals(187, insight.getImpressions());
+ Assert.assertEquals(184, insight.getUniqueImpressions());
+ Assert.assertEquals(184, insight.getReach());
+ Assert.assertEquals(1.6042780748663, insight.getResultRate(), EPSILON);
+ Assert.assertEquals(3, insight.getResults());
+ Assert.assertEquals(0, insight.getRoas());
+ Assert.assertEquals(0, insight.getSocialClicks());
+ Assert.assertEquals(0, insight.getUniqueSocialClicks());
+ Assert.assertEquals(0, insight.getSocialImpressions());
+ Assert.assertEquals(0, insight.getUniqueSocialImpressions());
+ Assert.assertEquals(0, insight.getSocialReach());
+ Assert.assertEquals(2, insight.getSpend());
+ Assert.assertEquals(0, insight.getTodaySpend());
+ Assert.assertEquals(0, insight.getTotalActionValue());
+ Assert.assertEquals(3, insight.getTotalActions());
+ Assert.assertEquals(2, insight.getTotalUniqueActions());
+ Assert.assertEquals(4, insight.getActions().size());
+ Assert.assertEquals("comment", insight.getActions().get(0).getActionType());
+ Assert.assertEquals(2, insight.getActions().get(0).getValue(), EPSILON);
+ Assert.assertEquals("post_like", insight.getActions().get(1).getActionType());
+ Assert.assertEquals(1, insight.getActions().get(1).getValue(), EPSILON);
+ Assert.assertEquals("page_engagement", insight.getActions().get(2).getActionType());
+ Assert.assertEquals(3, insight.getActions().get(2).getValue(), EPSILON);
+ Assert.assertEquals("post_engagement", insight.getActions().get(3).getActionType());
+ Assert.assertEquals(3, insight.getActions().get(3).getValue(), EPSILON);
+ Assert.assertEquals(4, insight.getUniqueActions().size());
+ Assert.assertEquals("comment", insight.getUniqueActions().get(0).getActionType());
+ Assert.assertEquals(1, insight.getUniqueActions().get(0).getValue(), EPSILON);
+ Assert.assertEquals("post_like", insight.getUniqueActions().get(1).getActionType());
+ Assert.assertEquals(1, insight.getUniqueActions().get(1).getValue(), EPSILON);
+ Assert.assertEquals("page_engagement", insight.getUniqueActions().get(2).getActionType());
+ Assert.assertEquals(2, insight.getUniqueActions().get(2).getValue(), EPSILON);
+ Assert.assertEquals("post_engagement", insight.getUniqueActions().get(3).getActionType());
+ Assert.assertEquals(2, insight.getUniqueActions().get(3).getValue(), EPSILON);
+ Assert.assertEquals(4, insight.getCostPerActionType().size());
+ Assert.assertEquals("comment", insight.getCostPerActionType().get(0).getActionType());
+ Assert.assertEquals(1, insight.getCostPerActionType().get(0).getValue(), EPSILON);
+ Assert.assertEquals("post_like", insight.getCostPerActionType().get(1).getActionType());
+ Assert.assertEquals(2, insight.getCostPerActionType().get(1).getValue(), EPSILON);
+ Assert.assertEquals("page_engagement", insight.getCostPerActionType().get(2).getActionType());
+ Assert.assertEquals(0.66666666666667, insight.getCostPerActionType().get(2).getValue(), EPSILON);
+ Assert.assertEquals("post_engagement", insight.getCostPerActionType().get(3).getActionType());
+ Assert.assertEquals(0.66666666666667, insight.getCostPerActionType().get(3).getValue(), EPSILON);
+ Assert.assertEquals(1, insight.getVideoStartActions().size());
+ Assert.assertEquals("video_view", insight.getVideoStartActions().get(0).getActionType());
+ Assert.assertEquals(0, insight.getVideoStartActions().get(0).getValue(), EPSILON);
+
+ mockServer.verify();
+ }
+
+ @Test(expected = NotAuthorizedException.class)
+ public void getAdInsight_unauthorized() throws Exception {
+ unauthorizedFacebookAds.adOperations().getAdInsight("100123456789");
+ }
+
+ @Test
+ public void createAd() throws Exception {
+ String requestBody = "name=Test+ad&adgroup_status=PAUSED&creative=%7B%22creative_id%22%3A+%22900123456789%22%7D&campaign_id=800123456789";
+ mockServer.expect(requestTo("https://graph.facebook.com/v2.3/act_123456789/adgroups"))
+ .andExpect(method(POST))
+ .andExpect(header("Authorization", "OAuth someAccessToken"))
+ .andExpect(content().string(requestBody))
+ .andRespond(withSuccess("{\"id\": \"100123456789\"}", MediaType.APPLICATION_JSON));
+
+ Ad ad = new Ad();
+ ad.setName("Test ad");
+ ad.setStatus(Ad.AdStatus.PAUSED);
+ ad.setAdSetId("800123456789");
+ ad.setCreativeId("900123456789");
+ assertEquals("100123456789", facebookAds.adOperations().createAd("123456789", ad));
+ mockServer.verify();
+ }
+
+ @Test
+ public void createAd_withBidInfo() throws Exception {
+ String requestBody = "name=Test+ad&adgroup_status=PAUSED&bid_info=%7B%22REACH%22%3A11%2C%22ACTIONS%22%3A10%2C%22SOCIAL%22%3A50%2C%22CLICKS%22%3A12%7D&creative=%7B%22creative_id%22%3A+%22900123456789%22%7D&campaign_id=800123456789";
+ mockServer.expect(requestTo("https://graph.facebook.com/v2.3/act_123456789/adgroups"))
+ .andExpect(method(POST))
+ .andExpect(header("Authorization", "OAuth someAccessToken"))
+ .andExpect(content().string(requestBody))
+ .andRespond(withSuccess("{\"id\": \"100123456789\"}", MediaType.APPLICATION_JSON));
+
+ Ad ad = new Ad();
+ ad.setName("Test ad");
+ ad.setStatus(Ad.AdStatus.PAUSED);
+ ad.setAdSetId("800123456789");
+ ad.setCreativeId("900123456789");
+ BidInfo bidInfo = new BidInfo();
+ bidInfo.put("ACTIONS", 10);
+ bidInfo.put("REACH", 11);
+ bidInfo.put("CLICKS", 12);
+ bidInfo.put("SOCIAL", 50);
+ ad.setBidInfo(bidInfo);
+ assertEquals("100123456789", facebookAds.adOperations().createAd("123456789", ad));
+ mockServer.verify();
+ }
+
+ @Test(expected = NotAuthorizedException.class)
+ public void createAd_unauthorized() throws Exception {
+ unauthorizedFacebookAds.adOperations().createAd("123456789", new Ad());
+ }
+
+ @Test
+ public void updateAd() throws Exception {
+ String requestBody = "name=Updated+Ad&adgroup_status=ARCHIVED&bid_info=%7B%22CLICKS%22%3A500%7D";
+ mockServer.expect(requestTo("https://graph.facebook.com/v2.3/100123456789"))
+ .andExpect(method(POST))
+ .andExpect(header("Authorization", "OAuth someAccessToken"))
+ .andExpect(content().string(requestBody))
+ .andRespond(withSuccess("{\"success\": true}", MediaType.APPLICATION_JSON));
+
+ Ad ad = new Ad();
+ ad.setStatus(Ad.AdStatus.ARCHIVED);
+ ad.setName("Updated Ad");
+ BidInfo bidInfo = new BidInfo();
+ bidInfo.put("CLICKS", 500);
+ ad.setBidInfo(bidInfo);
+ assertTrue(facebookAds.adOperations().updateAd("100123456789", ad));
+ mockServer.verify();
+ }
+
+ @Test(expected = NotAuthorizedException.class)
+ public void updateAd_unauthorized() throws Exception {
+ unauthorizedFacebookAds.adOperations().updateAd("100123456789", new Ad());
+ }
+
+ @Test
+ public void deleteAd() throws Exception {
+ mockServer.expect(requestTo("https://graph.facebook.com/v2.3/100123456789"))
+ .andExpect(method(DELETE))
+ .andExpect(header("Authorization", "OAuth someAccessToken"))
+ .andRespond(withSuccess("{\"success\": true}", MediaType.APPLICATION_JSON));
+ facebookAds.adOperations().deleteAd("100123456789");
+ mockServer.verify();
+ }
+
+ @Test(expected = NotAuthorizedException.class)
+ public void deleteAd_unauthorized() throws Exception {
+ unauthorizedFacebookAds.adOperations().deleteAd("100123456789");
+ }
+
+ private void verifyAd(Ad ad) {
+ assertEquals("100123456789", ad.getId());
+ assertEquals("123456789", ad.getAccountId());
+ assertEquals("800123456789", ad.getAdSetId());
+ assertEquals("700123456789", ad.getCampaignId());
+ assertEquals(toDate("2015-04-10T09:28:54+0200"), ad.getCreatedTime());
+ assertEquals("900123456789", ad.getCreativeId());
+ assertEquals("Test ad name", ad.getName());
+ assertEquals(Integer.valueOf(18), ad.getTargeting().getAgeMin());
+ assertEquals(Integer.valueOf(20), ad.getTargeting().getAgeMax());
+ assertEquals(6004854404172L, ad.getTargeting().getBehaviors().get(0).getId());
+ assertEquals("Technology late adopters", ad.getTargeting().getBehaviors().get(0).getName());
+ assertEquals(Targeting.Gender.MALE, ad.getTargeting().getGenders().get(0));
+ assertEquals("PL", ad.getTargeting().getGeoLocations().getCountries().get(0));
+ assertEquals(TargetingLocation.LocationType.HOME, ad.getTargeting().getGeoLocations().getLocationTypes().get(0));
+ assertEquals(TargetingLocation.LocationType.RECENT, ad.getTargeting().getGeoLocations().getLocationTypes().get(1));
+ assertEquals(6003629266583L, ad.getTargeting().getInterests().get(0).getId());
+ assertEquals("Hard drives", ad.getTargeting().getInterests().get(0).getName());
+ assertEquals(Targeting.PageType.FEED, ad.getTargeting().getPageTypes().get(0));
+ assertEquals(toDate("2015-04-10T13:32:09+0200"), ad.getUpdatedTime());
+ }
+}
diff --git a/spring-social-facebook/src/test/java/org/springframework/social/facebook/api/ads/CampaignTemplateTest.java b/spring-social-facebook/src/test/java/org/springframework/social/facebook/api/ads/CampaignTemplateTest.java
new file mode 100644
index 000000000..1b301c51d
--- /dev/null
+++ b/spring-social-facebook/src/test/java/org/springframework/social/facebook/api/ads/CampaignTemplateTest.java
@@ -0,0 +1,466 @@
+package org.springframework.social.facebook.api.ads;
+
+import org.junit.Test;
+import org.springframework.http.MediaType;
+
+import org.springframework.social.NotAuthorizedException;
+import org.springframework.social.facebook.api.PagedList;
+import org.springframework.social.facebook.api.ads.AdCampaign.BuyingType;
+import org.springframework.social.facebook.api.ads.AdCampaign.CampaignObjective;
+import org.springframework.social.facebook.api.ads.AdCampaign.CampaignStatus;
+
+import static org.junit.Assert.*;
+import static org.springframework.http.HttpMethod.GET;
+import static org.springframework.http.HttpMethod.POST;
+import static org.springframework.test.web.client.match.MockRestRequestMatchers.*;
+import static org.springframework.test.web.client.response.MockRestResponseCreators.withBadRequest;
+import static org.springframework.test.web.client.response.MockRestResponseCreators.withSuccess;
+
+/**
+ * @author Sebastian Górecki
+ */
+public class CampaignTemplateTest extends AbstractFacebookAdsApiTest {
+
+ @Test
+ public void getAdCampaigns() throws Exception {
+ mockServer.expect(requestTo("https://graph.facebook.com/v2.3/act_123456789/adcampaign_groups?fields=id%2Caccount_id%2Cbuying_type%2Ccampaign_group_status%2Cname%2Cobjective%2Cspend_cap"))
+ .andExpect(method(GET))
+ .andExpect(header("Authorization", "OAuth someAccessToken"))
+ .andRespond(withSuccess(jsonResource("ad-account-campaigns"), MediaType.APPLICATION_JSON));
+ PagedList campaigns = facebookAds.campaignOperations().getAdCampaigns("123456789");
+ assertEquals(3, campaigns.size());
+ assertEquals("601123456789", campaigns.get(0).getId());
+ assertEquals("123456789", campaigns.get(0).getAccountId());
+ assertEquals(BuyingType.AUCTION, campaigns.get(0).getBuyingType());
+ assertEquals(CampaignStatus.ACTIVE, campaigns.get(0).getStatus());
+ assertEquals("Campaign #1", campaigns.get(0).getName());
+ assertEquals(CampaignObjective.POST_ENGAGEMENT, campaigns.get(0).getObjective());
+ assertEquals(0, campaigns.get(0).getSpendCap());
+ assertEquals("602123456789", campaigns.get(1).getId());
+ assertEquals("123456789", campaigns.get(1).getAccountId());
+ assertEquals(BuyingType.FIXED_CPM, campaigns.get(1).getBuyingType());
+ assertEquals(CampaignStatus.PAUSED, campaigns.get(1).getStatus());
+ assertEquals("Campaign #2", campaigns.get(1).getName());
+ assertEquals(CampaignObjective.NONE, campaigns.get(1).getObjective());
+ assertEquals(0, campaigns.get(1).getSpendCap());
+ assertEquals("603123456789", campaigns.get(2).getId());
+ assertEquals("123456789", campaigns.get(2).getAccountId());
+ assertEquals(BuyingType.RESERVED, campaigns.get(2).getBuyingType());
+ assertEquals(CampaignStatus.ARCHIVED, campaigns.get(2).getStatus());
+ assertEquals("Campaign #3", campaigns.get(2).getName());
+ assertEquals(CampaignObjective.WEBSITE_CONVERSIONS, campaigns.get(2).getObjective());
+ assertEquals(50000, campaigns.get(2).getSpendCap());
+ }
+
+ @Test(expected = NotAuthorizedException.class)
+ public void getAdCampaigns_unauthorized() throws Exception {
+ unauthorizedFacebookAds.campaignOperations().getAdCampaigns("123456789");
+ }
+
+ @Test
+ public void getCampaign() throws Exception {
+ mockServer.expect(requestTo("https://graph.facebook.com/v2.3/600123456789?fields=id%2Caccount_id%2Cbuying_type%2Ccampaign_group_status%2Cname%2Cobjective%2Cspend_cap"))
+ .andExpect(method(GET))
+ .andExpect(header("Authorization", "OAuth someAccessToken"))
+ .andRespond(withSuccess(jsonResource("ad-campaign"), MediaType.APPLICATION_JSON));
+
+ AdCampaign campaign = facebookAds.campaignOperations().getAdCampaign("600123456789");
+ assertEquals("600123456789", campaign.getId());
+ assertEquals("123456789", campaign.getAccountId());
+ assertEquals(BuyingType.AUCTION, campaign.getBuyingType());
+ assertEquals(CampaignStatus.ACTIVE, campaign.getStatus());
+ assertEquals("The test campaign name", campaign.getName());
+ assertEquals(CampaignObjective.POST_ENGAGEMENT, campaign.getObjective());
+ assertEquals(1000, campaign.getSpendCap());
+ }
+
+ @Test
+ public void getCampaign_withUnknownEnums() throws Exception {
+ mockServer.expect(requestTo("https://graph.facebook.com/v2.3/600123456789?fields=id%2Caccount_id%2Cbuying_type%2Ccampaign_group_status%2Cname%2Cobjective%2Cspend_cap"))
+ .andExpect(method(GET))
+ .andExpect(header("Authorization", "OAuth someAccessToken"))
+ .andRespond(withSuccess(jsonResource("ad-campaign-with-unknown-enums"), MediaType.APPLICATION_JSON));
+
+ AdCampaign campaign = facebookAds.campaignOperations().getAdCampaign("600123456789");
+ assertEquals("600123456789", campaign.getId());
+ assertEquals("123456789", campaign.getAccountId());
+ assertEquals(BuyingType.UNKNOWN, campaign.getBuyingType());
+ assertEquals(CampaignStatus.UNKNOWN, campaign.getStatus());
+ assertEquals("The test campaign name", campaign.getName());
+ assertEquals(CampaignObjective.UNKNOWN, campaign.getObjective());
+ mockServer.verify();
+ }
+
+ @Test
+ public void getCampaign_withEmptyBuyingType() throws Exception {
+ mockServer.expect(requestTo("https://graph.facebook.com/v2.3/609123456789?fields=id%2Caccount_id%2Cbuying_type%2Ccampaign_group_status%2Cname%2Cobjective%2Cspend_cap"))
+ .andExpect(method(GET))
+ .andExpect(header("Authorization", "OAuth someAccessToken"))
+ .andRespond(withSuccess(jsonResource("ad-campaign-empty-buying-type"), MediaType.APPLICATION_JSON));
+
+ AdCampaign campaign = facebookAds.campaignOperations().getAdCampaign("609123456789");
+ assertEquals("609123456789", campaign.getId());
+ assertEquals("123456789", campaign.getAccountId());
+ assertNull(campaign.getBuyingType());
+ assertEquals(CampaignStatus.ACTIVE, campaign.getStatus());
+ assertEquals("The test campaign name", campaign.getName());
+ assertEquals(CampaignObjective.POST_ENGAGEMENT, campaign.getObjective());
+ assertEquals(1000, campaign.getSpendCap());
+ mockServer.verify();
+ }
+
+ @Test(expected = NotAuthorizedException.class)
+ public void getCampaign_unauthorized() throws Exception {
+ unauthorizedFacebookAds.campaignOperations().getAdCampaign("600123456789");
+ }
+
+ @Test
+ public void getAdCampaignSets() throws Exception {
+ mockServer.expect(requestTo("https://graph.facebook.com/v2.3/600123456789/adcampaigns?fields=account_id%2Cbid_info%2Cbid_type%2Cbudget_remaining%2Ccampaign_group_id%2Ccampaign_status%2Ccreated_time%2Ccreative_sequence%2Cdaily_budget%2Cend_time%2Cid%2Cis_autobid%2Clifetime_budget%2Cname%2Cpromoted_object%2Cstart_time%2Ctargeting%2Cupdated_time"))
+ .andExpect(method(GET))
+ .andExpect(header("Authorization", "OAuth someAccessToken"))
+ .andRespond(withSuccess(jsonResource("ad-sets"), MediaType.APPLICATION_JSON));
+
+ PagedList adSets = facebookAds.campaignOperations().getAdCampaignSets("600123456789");
+ assertEquals(2, adSets.size());
+ assertEquals("123456789", adSets.get(0).getAccountId());
+ assertEquals(BidType.ABSOLUTE_OCPM, adSets.get(0).getBidType());
+ assertEquals(37407, adSets.get(0).getBudgetRemaining());
+ assertEquals("600123456789", adSets.get(0).getCampaignId());
+ assertEquals(AdSet.AdSetStatus.PAUSED, adSets.get(0).getStatus());
+ assertEquals(toDate("2015-05-27T11:58:34+0200"), adSets.get(0).getCreatedTime());
+ assertEquals(40000, adSets.get(0).getDailyBudget());
+ assertEquals(toDate("2015-05-29T22:26:40+0200"), adSets.get(0).getEndTime());
+ assertEquals("700123456789", adSets.get(0).getId());
+ assertTrue(adSets.get(0).isAutobid());
+ assertEquals(0, adSets.get(0).getLifetimeBudget());
+ assertEquals("Test AdSet", adSets.get(0).getName());
+ assertEquals(toDate("2015-05-27T11:58:34+0200"), adSets.get(0).getStartTime());
+ assertEquals(Integer.valueOf(65), adSets.get(0).getTargeting().getAgeMax());
+ assertEquals(Integer.valueOf(18), adSets.get(0).getTargeting().getAgeMin());
+ assertEquals("BR", adSets.get(0).getTargeting().getGeoLocations().getCountries().get(0));
+ assertEquals(TargetingLocation.LocationType.HOME, adSets.get(0).getTargeting().getGeoLocations().getLocationTypes().get(0));
+ assertEquals(toDate("2015-05-27T11:58:34+0200"), adSets.get(0).getUpdatedTime());
+
+ assertEquals("123456789", adSets.get(1).getAccountId());
+ assertEquals(BidType.ABSOLUTE_OCPM, adSets.get(1).getBidType());
+ assertEquals(0, adSets.get(1).getBudgetRemaining());
+ assertEquals("600123456789", adSets.get(1).getCampaignId());
+ assertEquals(AdSet.AdSetStatus.ACTIVE, adSets.get(1).getStatus());
+ assertEquals(toDate("2015-04-10T09:28:54+0200"), adSets.get(1).getCreatedTime());
+ assertEquals(0, adSets.get(1).getDailyBudget());
+ assertEquals(toDate("2015-04-13T09:19:00+0200"), adSets.get(1).getEndTime());
+ assertEquals("701123456789", adSets.get(1).getId());
+ assertTrue(adSets.get(1).isAutobid());
+ assertEquals(200, adSets.get(1).getLifetimeBudget());
+ assertEquals("Real ad set", adSets.get(1).getName());
+ assertEquals(toDate("2015-04-12T09:19:00+0200"), adSets.get(1).getStartTime());
+ assertEquals(Integer.valueOf(20), adSets.get(1).getTargeting().getAgeMax());
+ assertEquals(Integer.valueOf(18), adSets.get(1).getTargeting().getAgeMin());
+ assertEquals(6004854404172L, adSets.get(1).getTargeting().getBehaviors().get(0).getId());
+ assertEquals("Technology late adopters", adSets.get(1).getTargeting().getBehaviors().get(0).getName());
+ assertEquals(Targeting.Gender.MALE, adSets.get(1).getTargeting().getGenders().get(0));
+ assertEquals("PL", adSets.get(1).getTargeting().getGeoLocations().getCountries().get(0));
+ assertEquals(TargetingLocation.LocationType.HOME, adSets.get(1).getTargeting().getGeoLocations().getLocationTypes().get(0));
+ assertEquals(TargetingLocation.LocationType.RECENT, adSets.get(1).getTargeting().getGeoLocations().getLocationTypes().get(1));
+ assertEquals(6003629266583L, adSets.get(1).getTargeting().getInterests().get(0).getId());
+ assertEquals("Hard drives", adSets.get(1).getTargeting().getInterests().get(0).getName());
+ assertEquals(Targeting.PageType.FEED, adSets.get(1).getTargeting().getPageTypes().get(0));
+ assertEquals(toDate("2015-04-10T13:32:09+0200"), adSets.get(1).getUpdatedTime());
+
+ mockServer.verify();
+ }
+
+ @Test(expected = NotAuthorizedException.class)
+ public void getAdCampaignSets_unauthorized() throws Exception {
+ unauthorizedFacebookAds.campaignOperations().getAdCampaignSets("600123456789");
+ }
+
+ @Test
+ public void createCampaign_withNameOnly() throws Exception {
+ String requestBody = "name=Campaign+created+by+SpringSocialFacebook";
+ mockServer.expect(requestTo("https://graph.facebook.com/v2.3/act_123456789/adcampaign_groups"))
+ .andExpect(method(POST))
+ .andExpect(header("Authorization", "OAuth someAccessToken"))
+ .andExpect(content().string(requestBody))
+ .andRespond(withSuccess("{\"id\": \"601123456789\"}", MediaType.APPLICATION_JSON));
+ AdCampaign campaign = new AdCampaign();
+ campaign.setName("Campaign created by SpringSocialFacebook");
+ assertEquals("601123456789", facebookAds.campaignOperations().createAdCampaign("123456789", campaign));
+ mockServer.verify();
+ }
+
+ @Test
+ public void createCampaign_withStatusOnly() throws Exception {
+ String requestBody = "campaign_group_status=PAUSED";
+ mockServer.expect(requestTo("https://graph.facebook.com/v2.3/act_123456789/adcampaign_groups"))
+ .andExpect(method(POST))
+ .andExpect(header("Authorization", "OAuth someAccessToken"))
+ .andExpect(content().string(requestBody))
+ .andRespond(withSuccess("{\"id\": \"601123456789\"}", MediaType.APPLICATION_JSON));
+ AdCampaign campaign = new AdCampaign();
+ campaign.setStatus(CampaignStatus.PAUSED);
+ assertEquals("601123456789", facebookAds.campaignOperations().createAdCampaign("123456789", campaign));
+ mockServer.verify();
+ }
+
+ @Test
+ public void createCampaign_withInvalidStatus() throws Exception {
+ // new campaigns can be created only with statuses ACTIVE or PAUSED
+ String requestBody = "name=Campaign+with+invalid+status&campaign_group_status=ARCHIVED";
+ mockServer.expect(requestTo("https://graph.facebook.com/v2.3/act_123456789/adcampaign_groups"))
+ .andExpect(method(POST))
+ .andExpect(header("Authorization", "OAuth someAccessToken"))
+ .andExpect(content().string(requestBody))
+ .andRespond(withBadRequest().body(jsonResource("error-invalid-update-campaign-status")).contentType(MediaType.APPLICATION_JSON));
+
+ AdCampaign campaign = new AdCampaign();
+ campaign.setName("Campaign with invalid status");
+ campaign.setStatus(CampaignStatus.ARCHIVED);
+ try {
+ facebookAds.campaignOperations().createAdCampaign("123456789", campaign);
+ fail();
+ } catch (InvalidCampaignStatusException e) {
+ assertEquals("New campaigns need to be either active or paused.", e.getMessage());
+ assertEquals("facebook", e.getProviderId());
+ }
+ }
+
+ @Test
+ public void createCampaign_withObjectiveOnly() throws Exception {
+ String requestBody = "objective=VIDEO_VIEWS";
+ mockServer.expect(requestTo("https://graph.facebook.com/v2.3/act_123456789/adcampaign_groups"))
+ .andExpect(method(POST))
+ .andExpect(header("Authorization", "OAuth someAccessToken"))
+ .andExpect(content().string(requestBody))
+ .andRespond(withSuccess("{\"id\": \"601123456789\"}", MediaType.APPLICATION_JSON));
+ AdCampaign campaign = new AdCampaign();
+ campaign.setObjective(CampaignObjective.VIDEO_VIEWS);
+ assertEquals("601123456789", facebookAds.campaignOperations().createAdCampaign("123456789", campaign));
+ mockServer.verify();
+ }
+
+ @Test
+ public void createCampaign_withSpendCapOnly() throws Exception {
+ String requestBody = "spend_cap=240000";
+ mockServer.expect(requestTo("https://graph.facebook.com/v2.3/act_123456789/adcampaign_groups"))
+ .andExpect(method(POST))
+ .andExpect(header("Authorization", "OAuth someAccessToken"))
+ .andExpect(content().string(requestBody))
+ .andRespond(withSuccess("{\"id\": \"601123456789\"}", MediaType.APPLICATION_JSON));
+ AdCampaign campaign = new AdCampaign();
+ campaign.setSpendCap(240000);
+ assertEquals("601123456789", facebookAds.campaignOperations().createAdCampaign("123456789", campaign));
+ mockServer.verify();
+ }
+
+ @Test
+ public void createCampaign_withBuyingTypeOnly() throws Exception {
+ String requestBody = "buying_type=AUCTION";
+ mockServer.expect(requestTo("https://graph.facebook.com/v2.3/act_123456789/adcampaign_groups"))
+ .andExpect(method(POST))
+ .andExpect(header("Authorization", "OAuth someAccessToken"))
+ .andExpect(content().string(requestBody))
+ .andRespond(withSuccess("{\"id\": \"601123456789\"}", MediaType.APPLICATION_JSON));
+ AdCampaign campaign = new AdCampaign();
+ campaign.setBuyingType(BuyingType.AUCTION);
+ assertEquals("601123456789", facebookAds.campaignOperations().createAdCampaign("123456789", campaign));
+ mockServer.verify();
+ }
+
+
+ @Test
+ public void createCampaign_withAllFields() throws Exception {
+ String requestBody = "name=Full+campaign&campaign_group_status=ACTIVE&objective=PAGE_LIKES&spend_cap=60000&buying_type=RESERVED";
+ mockServer.expect(requestTo("https://graph.facebook.com/v2.3/act_123456789/adcampaign_groups"))
+ .andExpect(method(POST))
+ .andExpect(header("Authorization", "OAuth someAccessToken"))
+ .andExpect(content().string(requestBody))
+ .andRespond(withSuccess("{\"id\": \"601123456789\"}", MediaType.APPLICATION_JSON));
+ AdCampaign campaign = new AdCampaign();
+ campaign.setName("Full campaign");
+ campaign.setStatus(CampaignStatus.ACTIVE);
+ campaign.setObjective(CampaignObjective.PAGE_LIKES);
+ campaign.setSpendCap(60000);
+ campaign.setBuyingType(BuyingType.RESERVED);
+ assertEquals("601123456789", facebookAds.campaignOperations().createAdCampaign("123456789", campaign));
+ mockServer.verify();
+ }
+
+ @Test(expected = NotAuthorizedException.class)
+ public void createCampaign_unauthorized() throws Exception {
+ unauthorizedFacebookAds.campaignOperations().createAdCampaign("123456789", new AdCampaign());
+ }
+
+ @Test
+ public void updateAdCampaign_withNameOnly() throws Exception {
+ String requestBody = "name=New+campaign+name";
+ mockServer.expect(requestTo("https://graph.facebook.com/v2.3/600123456789"))
+ .andExpect(method(POST))
+ .andExpect(header("Authorization", "OAuth someAccessToken"))
+ .andExpect(content().string(requestBody))
+ .andRespond(withSuccess("{\"success\": true}", MediaType.APPLICATION_JSON));
+ AdCampaign campaign = new AdCampaign();
+ campaign.setName("New campaign name");
+ assertTrue(facebookAds.campaignOperations().updateAdCampaign("600123456789", campaign));
+ mockServer.verify();
+ }
+
+ @Test
+ public void updateAdCampaign_withStatusOnly() throws Exception {
+ String requestBody = "campaign_group_status=ACTIVE";
+ mockServer.expect(requestTo("https://graph.facebook.com/v2.3/600123456789"))
+ .andExpect(method(POST))
+ .andExpect(header("Authorization", "OAuth someAccessToken"))
+ .andExpect(content().string(requestBody))
+ .andRespond(withSuccess("{\"success\": true}", MediaType.APPLICATION_JSON));
+ AdCampaign campaign = new AdCampaign();
+ campaign.setStatus(CampaignStatus.ACTIVE);
+ assertTrue(facebookAds.campaignOperations().updateAdCampaign("600123456789", campaign));
+ mockServer.verify();
+ }
+
+ @Test
+ public void updateAdCampaign_withObjectiveOnly() throws Exception {
+ String requestBody = "objective=POST_ENGAGEMENT";
+ mockServer.expect(requestTo("https://graph.facebook.com/v2.3/600123456789"))
+ .andExpect(method(POST))
+ .andExpect(header("Authorization", "OAuth someAccessToken"))
+ .andExpect(content().string(requestBody))
+ .andRespond(withSuccess("{\"success\": true}", MediaType.APPLICATION_JSON));
+ AdCampaign campaign = new AdCampaign();
+ campaign.setObjective(CampaignObjective.POST_ENGAGEMENT);
+ assertTrue(facebookAds.campaignOperations().updateAdCampaign("600123456789", campaign));
+ mockServer.verify();
+ }
+
+ @Test
+ public void updateAdCampaign_withSpendCapOnly() throws Exception {
+ String requestBody = "spend_cap=60000";
+ mockServer.expect(requestTo("https://graph.facebook.com/v2.3/600123456789"))
+ .andExpect(method(POST))
+ .andExpect(header("Authorization", "OAuth someAccessToken"))
+ .andExpect(content().string(requestBody))
+ .andRespond(withSuccess("{\"success\": true}", MediaType.APPLICATION_JSON));
+ AdCampaign campaign = new AdCampaign();
+ campaign.setSpendCap(60000);
+ assertTrue(facebookAds.campaignOperations().updateAdCampaign("600123456789", campaign));
+ mockServer.verify();
+ }
+
+ @Test
+ public void updateAdCampaign_withAllFields() throws Exception {
+ String requestBody = "name=Updated+campaign&campaign_group_status=ARCHIVED&objective=CANVAS_APP_ENGAGEMENT&spend_cap=60000";
+ mockServer.expect(requestTo("https://graph.facebook.com/v2.3/600123456789"))
+ .andExpect(method(POST))
+ .andExpect(header("Authorization", "OAuth someAccessToken"))
+ .andExpect(content().string(requestBody))
+ .andRespond(withSuccess("{\"success\": true}", MediaType.APPLICATION_JSON));
+ AdCampaign campaign = new AdCampaign();
+ campaign.setName("Updated campaign");
+ campaign.setStatus(CampaignStatus.ARCHIVED);
+ campaign.setObjective(CampaignObjective.CANVAS_APP_ENGAGEMENT);
+ campaign.setSpendCap(60000);
+ assertTrue(facebookAds.campaignOperations().updateAdCampaign("600123456789", campaign));
+ mockServer.verify();
+ }
+
+ @Test(expected = NotAuthorizedException.class)
+ public void updateAdCampaign_unauthorized() throws Exception {
+ unauthorizedFacebookAds.campaignOperations().updateAdCampaign("600123456789", new AdCampaign());
+ }
+
+ @Test
+ public void deleteAdCampaign() throws Exception {
+ String requestBody = "campaign_group_status=DELETED";
+ mockServer.expect(requestTo("https://graph.facebook.com/v2.3/600123456789"))
+ .andExpect(method(POST))
+ .andExpect(header("Authorization", "OAuth someAccessToken"))
+ .andExpect(content().string(requestBody))
+ .andRespond(withSuccess("{\"success\": true}", MediaType.APPLICATION_JSON));
+ facebookAds.campaignOperations().deleteAdCampaign("600123456789");
+ mockServer.verify();
+ }
+
+ @Test(expected = NotAuthorizedException.class)
+ public void deleteAdCampaign_unauthorized() throws Exception {
+ unauthorizedFacebookAds.campaignOperations().deleteAdCampaign("600123456789");
+ }
+
+ @Test
+ public void getAdCampaignInsights() throws Exception {
+ mockServer.expect(requestTo("https://graph.facebook.com/v2.3/600123456789/insights?fields=account_id%2Caccount_name%2Cdate_start%2Cdate_stop%2Cactions_per_impression%2Cclicks%2Cunique_clicks%2Ccost_per_result%2Ccost_per_total_action%2Ccpc%2Ccost_per_unique_click%2Ccpm%2Ccpp%2Cctr%2Cunique_ctr%2Cfrequency%2Cimpressions%2Cunique_impressions%2Cobjective%2Creach%2Cresult_rate%2Cresults%2Croas%2Csocial_clicks%2Cunique_social_clicks%2Csocial_impressions%2Cunique_social_impressions%2Csocial_reach%2Cspend%2Ctoday_spend%2Ctotal_action_value%2Ctotal_actions%2Ctotal_unique_actions%2Cactions%2Cunique_actions%2Ccost_per_action_type%2Cvideo_start_actions"))
+ .andExpect(method(GET))
+ .andExpect(header("Authorization", "OAuth someAccessToken"))
+ .andRespond(withSuccess(jsonResource("ad-campaign-insights"), MediaType.APPLICATION_JSON));
+
+ AdInsight insight = facebookAds.campaignOperations().getAdCampaignInsight("600123456789");
+ assertEquals("123456789", insight.getAccountId());
+ assertEquals("Test account name", insight.getAccountName());
+ assertEquals(0.016042780748663, insight.getActionsPerImpression(), EPSILON);
+ assertEquals(8, insight.getClicks());
+ assertEquals(5, insight.getUniqueClicks());
+ assertEquals(0.66666666666667, insight.getCostPerResult(), EPSILON);
+ assertEquals(0.66666666666667, insight.getCostPerTotalAction(), EPSILON);
+ assertEquals(0.25, insight.getCostPerClick(), EPSILON);
+ assertEquals(0.4, insight.getCostPerUniqueClick(), EPSILON);
+ assertEquals(10.695187165775, insight.getCpm(), EPSILON);
+ assertEquals(10.869565217391, insight.getCpp(), EPSILON);
+ assertEquals(4.2780748663102, insight.getCtr(), EPSILON);
+ assertEquals(2.7173913043478, insight.getUniqueCtr(), EPSILON);
+ assertEquals(1.0163043478261, insight.getFrequency(), EPSILON);
+ assertEquals(187, insight.getImpressions());
+ assertEquals(184, insight.getUniqueImpressions());
+ assertEquals(184, insight.getReach());
+ assertEquals(1.6042780748663, insight.getResultRate(), EPSILON);
+ assertEquals(3, insight.getResults());
+ assertEquals(0, insight.getRoas());
+ assertEquals(0, insight.getSocialClicks());
+ assertEquals(0, insight.getUniqueSocialClicks());
+ assertEquals(0, insight.getSocialImpressions());
+ assertEquals(0, insight.getUniqueSocialImpressions());
+ assertEquals(0, insight.getSocialReach());
+ assertEquals(2, insight.getSpend());
+ assertEquals(0, insight.getTodaySpend());
+ assertEquals(0, insight.getTotalActionValue());
+ assertEquals(3, insight.getTotalActions());
+ assertEquals(2, insight.getTotalUniqueActions());
+ assertEquals(4, insight.getActions().size());
+ assertEquals("comment", insight.getActions().get(0).getActionType());
+ assertEquals(2, insight.getActions().get(0).getValue(), EPSILON);
+ assertEquals("post_like", insight.getActions().get(1).getActionType());
+ assertEquals(1, insight.getActions().get(1).getValue(), EPSILON);
+ assertEquals("page_engagement", insight.getActions().get(2).getActionType());
+ assertEquals(3, insight.getActions().get(2).getValue(), EPSILON);
+ assertEquals("post_engagement", insight.getActions().get(3).getActionType());
+ assertEquals(3, insight.getActions().get(3).getValue(), EPSILON);
+ assertEquals(4, insight.getUniqueActions().size());
+ assertEquals("comment", insight.getUniqueActions().get(0).getActionType());
+ assertEquals(1, insight.getUniqueActions().get(0).getValue(), EPSILON);
+ assertEquals("post_like", insight.getUniqueActions().get(1).getActionType());
+ assertEquals(1, insight.getUniqueActions().get(1).getValue(), EPSILON);
+ assertEquals("page_engagement", insight.getUniqueActions().get(2).getActionType());
+ assertEquals(2, insight.getUniqueActions().get(2).getValue(), EPSILON);
+ assertEquals("post_engagement", insight.getUniqueActions().get(3).getActionType());
+ assertEquals(2, insight.getUniqueActions().get(3).getValue(), EPSILON);
+ assertEquals(4, insight.getCostPerActionType().size());
+ assertEquals("comment", insight.getCostPerActionType().get(0).getActionType());
+ assertEquals(1, insight.getCostPerActionType().get(0).getValue(), EPSILON);
+ assertEquals("post_like", insight.getCostPerActionType().get(1).getActionType());
+ assertEquals(2, insight.getCostPerActionType().get(1).getValue(), EPSILON);
+ assertEquals("page_engagement", insight.getCostPerActionType().get(2).getActionType());
+ assertEquals(0.66666666666667, insight.getCostPerActionType().get(2).getValue(), EPSILON);
+ assertEquals("post_engagement", insight.getCostPerActionType().get(3).getActionType());
+ assertEquals(0.66666666666667, insight.getCostPerActionType().get(3).getValue(), EPSILON);
+ assertEquals(1, insight.getVideoStartActions().size());
+ assertEquals("video_view", insight.getVideoStartActions().get(0).getActionType());
+ assertEquals(0, insight.getVideoStartActions().get(0).getValue(), EPSILON);
+
+ mockServer.verify();
+ }
+
+ @Test(expected = NotAuthorizedException.class)
+ public void getAdCampaignInsights_unauthorized() throws Exception {
+ unauthorizedFacebookAds.campaignOperations().getAdCampaignInsight("600123456789");
+ }
+}
diff --git a/spring-social-facebook/src/test/java/org/springframework/social/facebook/api/ads/CreativeTemplateTest.java b/spring-social-facebook/src/test/java/org/springframework/social/facebook/api/ads/CreativeTemplateTest.java
new file mode 100644
index 000000000..76743a57c
--- /dev/null
+++ b/spring-social-facebook/src/test/java/org/springframework/social/facebook/api/ads/CreativeTemplateTest.java
@@ -0,0 +1,220 @@
+package org.springframework.social.facebook.api.ads;
+
+import org.junit.Test;
+import org.springframework.http.MediaType;
+import org.springframework.social.NotAuthorizedException;
+import org.springframework.social.facebook.api.PagedList;
+
+import java.util.List;
+
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.fail;
+import static org.springframework.http.HttpMethod.DELETE;
+import static org.springframework.http.HttpMethod.GET;
+import static org.springframework.http.HttpMethod.POST;
+import static org.springframework.test.web.client.match.MockRestRequestMatchers.*;
+import static org.springframework.test.web.client.response.MockRestResponseCreators.withSuccess;
+
+/**
+ * @author Sebastian Górecki
+ */
+public class CreativeTemplateTest extends AbstractFacebookAdsApiTest {
+
+ @Test
+ public void getAccountCreatives() throws Exception {
+ mockServer.expect(requestTo("https://graph.facebook.com/v2.3/act_123456789/adcreatives?fields=actor_id%2Cbody%2Cimage_hash%2Cimage_url%2Clink_url%2Cname%2Cobject_id%2Cobject_story_id%2Cobject_url%2Crun_status%2Ctitle%2Curl_tags%2Cthumbnail_url%2Cobject_type%2Cid"))
+ .andExpect(method(GET))
+ .andExpect(header("Authorization", "OAuth someAccessToken"))
+ .andRespond(withSuccess(jsonResource("ad-creatives"), MediaType.APPLICATION_JSON));
+
+ List creatives = facebookAds.creativeOperations().getAccountCreatives("123456789");
+ verifyAdCreatives(creatives);
+ mockServer.verify();
+ }
+
+ @Test(expected = NotAuthorizedException.class)
+ public void getAccountCreatives_unauthorized() throws Exception {
+ unauthorizedFacebookAds.creativeOperations().getAccountCreatives("123456789");
+ }
+
+ @Test
+ public void getAdSetCreatives() throws Exception {
+ mockServer.expect(requestTo("https://graph.facebook.com/v2.3/700123456789/adcreatives?fields=actor_id%2Cbody%2Cimage_hash%2Cimage_url%2Clink_url%2Cname%2Cobject_id%2Cobject_story_id%2Cobject_url%2Crun_status%2Ctitle%2Curl_tags%2Cthumbnail_url%2Cobject_type%2Cid"))
+ .andExpect(method(GET))
+ .andExpect(header("Authorization", "OAuth someAccessToken"))
+ .andRespond(withSuccess(jsonResource("ad-creatives"), MediaType.APPLICATION_JSON));
+
+ List creatives = facebookAds.creativeOperations().getAdSetCreatives("700123456789");
+ verifyAdCreatives(creatives);
+ mockServer.verify();
+ }
+
+ @Test(expected = NotAuthorizedException.class)
+ public void getAdSetCreatives_unauthorized() throws Exception {
+ unauthorizedFacebookAds.creativeOperations().getAdSetCreatives("700123456789");
+ }
+
+ @Test
+ public void getAdCreative() throws Exception {
+ mockServer.expect(requestTo("https://graph.facebook.com/v2.3/800123456789?fields=actor_id%2Cbody%2Cimage_hash%2Cimage_url%2Clink_url%2Cname%2Cobject_id%2Cobject_story_id%2Cobject_url%2Crun_status%2Ctitle%2Curl_tags%2Cthumbnail_url%2Cobject_type%2Cid"))
+ .andExpect(method(GET))
+ .andExpect(header("Authorization", "OAuth someAccessToken"))
+ .andRespond(withSuccess(jsonResource("ad-creative"), MediaType.APPLICATION_JSON));
+
+ AdCreative creative = facebookAds.creativeOperations().getAdCreative("800123456789");
+ assertEquals("800123456789", creative.getId());
+ assertEquals(AdCreative.AdCreativeType.STATUS, creative.getType());
+ assertEquals("https://example.net/safe_image.php?d=123456789", creative.getThumbnailUrl());
+ assertEquals(AdCreative.AdCreativeStatus.ACTIVE, creative.getStatus());
+ assertEquals("123456789_987654321", creative.getObjectStoryId());
+ assertEquals("Creative test name", creative.getName());
+ mockServer.verify();
+ }
+
+ @Test
+ public void getAdCreative_withWrongType() throws Exception {
+ mockServer.expect(requestTo("https://graph.facebook.com/v2.3/801123456789?fields=actor_id%2Cbody%2Cimage_hash%2Cimage_url%2Clink_url%2Cname%2Cobject_id%2Cobject_story_id%2Cobject_url%2Crun_status%2Ctitle%2Curl_tags%2Cthumbnail_url%2Cobject_type%2Cid"))
+ .andExpect(method(GET))
+ .andExpect(header("Authorization", "OAuth someAccessToken"))
+ .andRespond(withSuccess(jsonResource("ad-creative-wrong-type"), MediaType.APPLICATION_JSON));
+ AdCreative creative = facebookAds.creativeOperations().getAdCreative("801123456789");
+ assertEquals("801123456789", creative.getId());
+ assertEquals(AdCreative.AdCreativeType.UNKNOWN, creative.getType());
+ verifyAdCreative(creative);
+ assertEquals(AdCreative.AdCreativeStatus.ACTIVE, creative.getStatus());
+ mockServer.verify();
+ }
+
+ @Test
+ public void getAdCreative_withWrongStatus() throws Exception {
+ mockServer.expect(requestTo("https://graph.facebook.com/v2.3/802123456789?fields=actor_id%2Cbody%2Cimage_hash%2Cimage_url%2Clink_url%2Cname%2Cobject_id%2Cobject_story_id%2Cobject_url%2Crun_status%2Ctitle%2Curl_tags%2Cthumbnail_url%2Cobject_type%2Cid"))
+ .andExpect(method(GET))
+ .andExpect(header("Authorization", "OAuth someAccessToken"))
+ .andRespond(withSuccess(jsonResource("ad-creative-wrong-status"), MediaType.APPLICATION_JSON));
+ AdCreative creative = facebookAds.creativeOperations().getAdCreative("802123456789");
+ assertEquals("802123456789", creative.getId());
+ assertEquals(AdCreative.AdCreativeType.DOMAIN, creative.getType());
+ verifyAdCreative(creative);
+ assertEquals(AdCreative.AdCreativeStatus.UNKNOWN, creative.getStatus());
+ mockServer.verify();
+ }
+
+ @Test(expected = NotAuthorizedException.class)
+ public void getAdCreative_unauthorized() throws Exception {
+ unauthorizedFacebookAds.creativeOperations().getAdCreative("800123456789");
+ }
+
+ @Test
+ public void createAdCreative_linkAd() throws Exception {
+ String requestBody = "title=Ad+creative+title&body=Over+the+past+few+years+REST+has+become+an+important&image_url=http%3A%2F%2Fexample.com%2Fstatics_s2_20150603-0041%2Fstyles%2Fi%2Flogo_bigger.jpg&object_url=http%3A%2F%2Fwww.example.com%2Fsome_object";
+ mockServer.expect(requestTo("https://graph.facebook.com/v2.3/act_123456789/adcreatives"))
+ .andExpect(method(POST))
+ .andExpect(header("Authorization", "OAuth someAccessToken"))
+ .andExpect(content().string(requestBody))
+ .andRespond(withSuccess("{\"id\": \"801123456789\"}", MediaType.APPLICATION_JSON));
+
+ AdCreative creative = new AdCreative();
+ creative.setTitle("Ad creative title");
+ creative.setObjectUrl("http://www.example.com/some_object");
+ creative.setBody("Over the past few years REST has become an important");
+ creative.setImageUrl("http://example.com/statics_s2_20150603-0041/styles/i/logo_bigger.jpg");
+ assertEquals("801123456789", facebookAds.creativeOperations().createAdCreative("123456789", creative));
+ mockServer.verify();
+ }
+
+ @Test
+ public void createAdCreative_postLikeAd() throws Exception {
+ String requestBody = "name=Post+Like+Ad+Creative&body=Some+body+here&object_id=600123456789";
+ mockServer.expect(requestTo("https://graph.facebook.com/v2.3/act_123456789/adcreatives"))
+ .andExpect(method(POST))
+ .andExpect(header("Authorization", "OAuth someAccessToken"))
+ .andExpect(content().string(requestBody))
+ .andRespond(withSuccess("{\"id\": \"802123456789\"}", MediaType.APPLICATION_JSON));
+
+ AdCreative creative = new AdCreative();
+ creative.setName("Post Like Ad Creative");
+ creative.setObjectId("600123456789");
+ creative.setBody("Some body here");
+ assertEquals("802123456789", facebookAds.creativeOperations().createAdCreative("123456789", creative));
+ mockServer.verify();
+ }
+
+ @Test(expected = NotAuthorizedException.class)
+ public void createAdCreative_unauthorized() throws Exception {
+ unauthorizedFacebookAds.creativeOperations().createAdCreative("123456789", new AdCreative());
+ }
+
+ @Test
+ public void renameAdCreative() throws Exception {
+ String requestBody = "name=New+creative+name";
+ mockServer.expect(requestTo("https://graph.facebook.com/v2.3/800123456789"))
+ .andExpect(method(POST))
+ .andExpect(header("Authorization", "OAuth someAccessToken"))
+ .andExpect(content().string(requestBody))
+ .andRespond(withSuccess("{\"success\": true}", MediaType.APPLICATION_JSON));
+
+ facebookAds.creativeOperations().renameAdCreative("800123456789", "New creative name");
+ mockServer.verify();
+ }
+
+ @Test(expected = NotAuthorizedException.class)
+ public void renameAdCreative_unauthorized() throws Exception {
+ unauthorizedFacebookAds.creativeOperations().renameAdCreative("800123456789", "New anauthorized name!");
+ }
+
+ @Test
+ public void deleteAdCreative() throws Exception {
+ mockServer.expect(requestTo("https://graph.facebook.com/v2.3/800123456789"))
+ .andExpect(method(DELETE))
+ .andExpect(header("Authorization", "OAuth someAccessToken"))
+ .andRespond(withSuccess("{\"success\": true}", MediaType.APPLICATION_JSON));
+
+ facebookAds.creativeOperations().deleteAdCreative("800123456789");
+ mockServer.verify();
+ }
+
+ @Test(expected = NotAuthorizedException.class)
+ public void deleteAdCreative_unauthorized() throws Exception {
+ unauthorizedFacebookAds.creativeOperations().deleteAdCreative("800123456789");
+ }
+
+ private void verifyAdCreatives(List creatives) {
+ assertEquals(3, creatives.size());
+ assertEquals("This is the body of the ad creative1", creatives.get(0).getBody());
+ assertEquals("ac67a06abb35a3bef8256c8c9085548e1", creatives.get(0).getImageHash());
+ assertEquals("https://example.com/images/image1.png", creatives.get(0).getImageUrl());
+ assertEquals("Name of the ad creative1", creatives.get(0).getName());
+ assertEquals("900123456789", creatives.get(0).getObjectId());
+ assertEquals(AdCreative.AdCreativeStatus.ACTIVE, creatives.get(0).getStatus());
+ assertEquals("https://example.com/thumbnails/thuumbnail_url1", creatives.get(0).getThumbnailUrl());
+ assertEquals(AdCreative.AdCreativeType.PAGE, creatives.get(0).getType());
+ assertEquals("801123456789", creatives.get(0).getId());
+ assertEquals("This is the body of the ad creative2", creatives.get(1).getBody());
+ assertEquals("ac67a06abb35a3bef8256c8c9085548e2", creatives.get(1).getImageHash());
+ assertEquals("https://example.com/images/image2.png", creatives.get(1).getImageUrl());
+ assertEquals("Name of the ad creative2", creatives.get(1).getName());
+ assertEquals("http://example.com/objects/object", creatives.get(1).getObjectUrl());
+ assertEquals(AdCreative.AdCreativeStatus.ACTIVE, creatives.get(1).getStatus());
+ assertEquals("Test Ad Creative", creatives.get(1).getTitle());
+ assertEquals("https://example.com/thumbnails/thuumbnail_url2", creatives.get(1).getThumbnailUrl());
+ assertEquals(AdCreative.AdCreativeType.DOMAIN, creatives.get(1).getType());
+ assertEquals("802123456789", creatives.get(1).getId());
+ assertEquals("This is the body of the ad creative3", creatives.get(2).getName());
+ assertEquals("987654321_123456789", creatives.get(2).getObjectStoryId());
+ assertEquals(AdCreative.AdCreativeStatus.ACTIVE, creatives.get(2).getStatus());
+ assertEquals("https://example.com/thumbnails/thuumbnail_url3", creatives.get(2).getThumbnailUrl());
+ assertEquals(AdCreative.AdCreativeType.STATUS, creatives.get(2).getType());
+ assertEquals("803123456789", creatives.get(2).getId());
+ }
+
+ private void verifyAdCreative(AdCreative creative) {
+ assertEquals("This is the body of the ad creative", creative.getBody());
+ assertEquals("ac67a06abb35a3bef8256c8c9085548e", creative.getImageHash());
+ assertEquals("https://example.com/images/image.png", creative.getImageUrl());
+ assertEquals("Name of the ad creative", creative.getName());
+ assertEquals("http://example.com/objects/object", creative.getObjectUrl());
+ assertEquals("Title of the ad creative", creative.getTitle());
+ assertEquals("https://example.com/thumbnails/thuumbnail_url", creative.getThumbnailUrl());
+ }
+}
diff --git a/spring-social-facebook/src/test/resources/org/springframework/social/facebook/api/ads/ad-account-campaigns.json b/spring-social-facebook/src/test/resources/org/springframework/social/facebook/api/ads/ad-account-campaigns.json
new file mode 100644
index 000000000..764a787db
--- /dev/null
+++ b/spring-social-facebook/src/test/resources/org/springframework/social/facebook/api/ads/ad-account-campaigns.json
@@ -0,0 +1,35 @@
+{
+ "data": [
+ {
+ "id": "601123456789",
+ "account_id": "123456789",
+ "buying_type": "AUCTION",
+ "campaign_group_status": "ACTIVE",
+ "name": "Campaign #1",
+ "objective": "POST_ENGAGEMENT"
+ },
+ {
+ "id": "602123456789",
+ "account_id": "123456789",
+ "buying_type": "FIXED_CPM",
+ "campaign_group_status": "PAUSED",
+ "name": "Campaign #2",
+ "objective": "NONE"
+ },
+ {
+ "id": "603123456789",
+ "account_id": "123456789",
+ "buying_type": "RESERVED",
+ "campaign_group_status": "ARCHIVED",
+ "name": "Campaign #3",
+ "objective": "WEBSITE_CONVERSIONS",
+ "spend_cap": 50000
+ }
+ ],
+ "paging": {
+ "cursors": {
+ "before": "NjAyNDE2Nzg5NTY1MA==",
+ "after": "NjAyNTIwMDQ1MTQ1MA=="
+ }
+ }
+}
\ No newline at end of file
diff --git a/spring-social-facebook/src/test/resources/org/springframework/social/facebook/api/ads/ad-account-insights.json b/spring-social-facebook/src/test/resources/org/springframework/social/facebook/api/ads/ad-account-insights.json
new file mode 100644
index 000000000..a1af55448
--- /dev/null
+++ b/spring-social-facebook/src/test/resources/org/springframework/social/facebook/api/ads/ad-account-insights.json
@@ -0,0 +1,106 @@
+{
+ "data": [
+ {
+ "account_id": "123456789",
+ "account_name": "Account Test Name #1",
+ "date_start": "2015-02-19",
+ "date_stop": "2015-04-23",
+ "actions_per_impression": 0.016042780748663,
+ "clicks": 8,
+ "unique_clicks": 5,
+ "cost_per_result": 0.66666666666667,
+ "cost_per_total_action": 0.66666666666667,
+ "cpc": 0.25,
+ "cost_per_unique_click": 0.4,
+ "cpm": 10.695187165775,
+ "cpp": 10.869565217391,
+ "ctr": 4.2780748663102,
+ "unique_ctr": 2.7173913043478,
+ "frequency": 1.0163043478261,
+ "impressions": "187",
+ "unique_impressions": 184,
+ "reach": 184,
+ "result_rate": 1.6042780748663,
+ "results": 3,
+ "roas": 1,
+ "social_clicks": 2,
+ "unique_social_clicks": 3,
+ "social_impressions": 4,
+ "unique_social_impressions": 5,
+ "social_reach": 6,
+ "spend": 2,
+ "today_spend": 0,
+ "total_action_value": 0,
+ "total_actions": 3,
+ "total_unique_actions": 2,
+ "actions": [
+ {
+ "action_type": "comment",
+ "value": 2
+ },
+ {
+ "action_type": "post_like",
+ "value": 1
+ },
+ {
+ "action_type": "page_engagement",
+ "value": 3
+ },
+ {
+ "action_type": "post_engagement",
+ "value": 3
+ }
+ ],
+ "unique_actions": [
+ {
+ "action_type": "comment",
+ "value": 1
+ },
+ {
+ "action_type": "post_like",
+ "value": 1
+ },
+ {
+ "action_type": "page_engagement",
+ "value": 2
+ },
+ {
+ "action_type": "post_engagement",
+ "value": 2
+ }
+ ],
+ "cost_per_action_type": [
+ {
+ "action_type": "comment",
+ "value": 1
+ },
+ {
+ "action_type": "post_like",
+ "value": 2
+ },
+ {
+ "action_type": "page_engagement",
+ "value": 0.66666666666667
+ },
+ {
+ "action_type": "post_engagement",
+ "value": 0.66666666666667
+ }
+ ],
+ "video_start_actions": [
+ {
+ "action_type": "video_view",
+ "value": 0
+ }
+ ],
+ "date_start": "2015-02-19",
+ "date_stop": "2015-04-21"
+ }
+ ],
+ "paging": {
+ "cursors": {
+ "before": "MA==",
+ "after": "MA=="
+ }
+ }
+}
\ No newline at end of file
diff --git a/spring-social-facebook/src/test/resources/org/springframework/social/facebook/api/ads/ad-account-no-users.json b/spring-social-facebook/src/test/resources/org/springframework/social/facebook/api/ads/ad-account-no-users.json
new file mode 100644
index 000000000..af48c322b
--- /dev/null
+++ b/spring-social-facebook/src/test/resources/org/springframework/social/facebook/api/ads/ad-account-no-users.json
@@ -0,0 +1,44 @@
+{
+ "id": "act_123456789",
+ "account_groups": [
+ {
+ "account_group_id": "987654321",
+ "name": "Test group name",
+ "status": 1
+ }
+ ],
+ "account_id": 123456789,
+ "account_status": 1,
+ "age": 10.0036226851851852,
+ "amount_spent": "1234",
+ "balance": "10000",
+ "business_city": "Poznan",
+ "business_country_code": "PL",
+ "business_name": "Some business name for the account",
+ "business_state": "wielkopolska",
+ "business_street": "Some street 79A",
+ "business_zip": "66-777",
+ "capabilities": [
+ "DIRECT_SALES",
+ "VIEW_TAGS"
+ ],
+ "created_time": "2015-02-19T00:31:33+0100",
+ "currency": "PLN",
+ "daily_spend_limit": "93263",
+ "end_advertiser": 987654321,
+ "funding_source": "1122334455",
+ "is_personal": 1,
+ "media_agency": 54321,
+ "name": "This is a test account",
+ "offsite_pixels_tos_accepted": false,
+ "partner": 111222333,
+ "spend_cap": "0",
+ "timezone_id": 106,
+ "timezone_name": "Europe/Warsaw",
+ "timezone_offset_hours_utc": 2,
+ "users": {
+ "data": [
+ ]
+ },
+ "tax_id_status": 3
+}
\ No newline at end of file
diff --git a/spring-social-facebook/src/test/resources/org/springframework/social/facebook/api/ads/ad-account-temporarily-unavailable.json b/spring-social-facebook/src/test/resources/org/springframework/social/facebook/api/ads/ad-account-temporarily-unavailable.json
new file mode 100644
index 000000000..9858ecc61
--- /dev/null
+++ b/spring-social-facebook/src/test/resources/org/springframework/social/facebook/api/ads/ad-account-temporarily-unavailable.json
@@ -0,0 +1,53 @@
+{
+ "id": "act_123456789",
+ "account_groups": [
+ {
+ "account_group_id": "987654321",
+ "name": "Test group name",
+ "status": 1
+ }
+ ],
+ "account_id": 123456789,
+ "account_status": 101,
+ "age": 10.0036226851851852,
+ "amount_spent": "1234",
+ "balance": "10000",
+ "business_city": "Poznan",
+ "business_country_code": "PL",
+ "business_name": "Some business name for the account",
+ "business_state": "wielkopolska",
+ "business_street": "Some street 79A",
+ "business_zip": "66-777",
+ "capabilities": [
+ "DIRECT_SALES",
+ "VIEW_TAGS"
+ ],
+ "created_time": "2015-02-19T00:31:33+0100",
+ "currency": "PLN",
+ "daily_spend_limit": "93263",
+ "end_advertiser": 987654321,
+ "funding_source": "1122334455",
+ "is_personal": 1,
+ "media_agency": 54321,
+ "name": "This is a test account",
+ "offsite_pixels_tos_accepted": false,
+ "partner": 111222333,
+ "spend_cap": "0",
+ "timezone_id": 106,
+ "timezone_name": "Europe/Warsaw",
+ "timezone_offset_hours_utc": 2,
+ "users": {
+ "data": [
+ {
+ "id": "1234",
+ "permissions": [
+ 1,
+ 2,
+ 3
+ ],
+ "role": 1001
+ }
+ ]
+ },
+ "tax_id_status": 3
+}
\ No newline at end of file
diff --git a/spring-social-facebook/src/test/resources/org/springframework/social/facebook/api/ads/ad-account-unknown-capabilities.json b/spring-social-facebook/src/test/resources/org/springframework/social/facebook/api/ads/ad-account-unknown-capabilities.json
new file mode 100644
index 000000000..72057e89f
--- /dev/null
+++ b/spring-social-facebook/src/test/resources/org/springframework/social/facebook/api/ads/ad-account-unknown-capabilities.json
@@ -0,0 +1,10 @@
+{
+ "capabilities": [
+ "UNKNOWN_CAPABILITY1",
+ "UNKNOWN_CAPABILITY2",
+ "UNKNOWN_CAPABILITY3",
+ "PREMIUM"
+ ],
+ "account_id": 123456789,
+ "id": "act_123456789"
+}
\ No newline at end of file
diff --git a/spring-social-facebook/src/test/resources/org/springframework/social/facebook/api/ads/ad-account-users.json b/spring-social-facebook/src/test/resources/org/springframework/social/facebook/api/ads/ad-account-users.json
new file mode 100644
index 000000000..7fe5875f6
--- /dev/null
+++ b/spring-social-facebook/src/test/resources/org/springframework/social/facebook/api/ads/ad-account-users.json
@@ -0,0 +1,34 @@
+{
+ "data": [
+ {
+ "id": "123456789",
+ "name": "Account #1",
+ "permissions": [
+ 1,
+ 2,
+ 3,
+ 4,
+ 5,
+ 7
+ ],
+ "role": 1001
+ },
+ {
+ "id": "987654321",
+ "name": "Account #2",
+ "permissions": [
+ 7
+ ],
+ "role": 1003
+ },
+ {
+ "id": "1122334455",
+ "name": "Account #3",
+ "permissions": [
+ 2,
+ 4
+ ],
+ "role": 1004
+ }
+ ]
+}
\ No newline at end of file
diff --git a/spring-social-facebook/src/test/resources/org/springframework/social/facebook/api/ads/ad-account-with-agency.json b/spring-social-facebook/src/test/resources/org/springframework/social/facebook/api/ads/ad-account-with-agency.json
new file mode 100644
index 000000000..d44c3db26
--- /dev/null
+++ b/spring-social-facebook/src/test/resources/org/springframework/social/facebook/api/ads/ad-account-with-agency.json
@@ -0,0 +1,67 @@
+{
+ "id": "act_123456789",
+ "account_groups": [
+ {
+ "account_group_id": "987654321",
+ "name": "Test group name",
+ "status": 1
+ }
+ ],
+ "account_id": 123456789,
+ "account_status": 1,
+ "age": 10.0036226851851852,
+ "agency_client_declaration": {
+ "agency_representing_client": 1,
+ "client_based_in_france": 0,
+ "client_city": "Warsaw",
+ "client_country_code": "PL",
+ "client_email_address": "example@example.com",
+ "client_name": "Some client",
+ "client_postal_code": "66-777",
+ "client_province": "malopolska",
+ "client_street": "Marszalkowska",
+ "client_street2": "1A",
+ "has_written_mandate_from_advertiser": 0,
+ "is_client_paying_invoices": 1
+ },
+ "amount_spent": "1234",
+ "balance": "10000",
+ "business_city": "Poznan",
+ "business_country_code": "PL",
+ "business_name": "Some business name for the account",
+ "business_state": "wielkopolska",
+ "business_street": "Some street 79A",
+ "business_zip": "66-777",
+ "capabilities": [
+ "DIRECT_SALES",
+ "VIEW_TAGS"
+ ],
+ "created_time": "2015-02-19T00:31:33+0100",
+ "currency": "PLN",
+ "daily_spend_limit": "93263",
+ "end_advertiser": 987654321,
+ "funding_source": "1122334455",
+ "is_personal": 1,
+ "media_agency": 54321,
+ "name": "This is a test account",
+ "offsite_pixels_tos_accepted": false,
+ "partner": 111222333,
+ "spend_cap": "0",
+ "timezone_id": 106,
+ "timezone_name": "Europe/Warsaw",
+ "timezone_offset_hours_utc": 2,
+ "users": {
+ "data": [
+ {
+ "id": "1234",
+ "permissions": [
+ 1,
+ 2,
+ 3
+ ],
+ "role": 1001
+ }
+ ]
+ },
+ "tax_id_status": 3
+}
\ No newline at end of file
diff --git a/spring-social-facebook/src/test/resources/org/springframework/social/facebook/api/ads/ad-account-with-few-users.json b/spring-social-facebook/src/test/resources/org/springframework/social/facebook/api/ads/ad-account-with-few-users.json
new file mode 100644
index 000000000..0a4fea6dd
--- /dev/null
+++ b/spring-social-facebook/src/test/resources/org/springframework/social/facebook/api/ads/ad-account-with-few-users.json
@@ -0,0 +1,61 @@
+{
+ "id": "act_123456789",
+ "account_groups": [
+ {
+ "account_group_id": "987654321",
+ "name": "Test group name",
+ "status": 1
+ }
+ ],
+ "account_id": 123456789,
+ "account_status": 1,
+ "age": 10.0036226851851852,
+ "amount_spent": "1234",
+ "balance": "10000",
+ "business_city": "Poznan",
+ "business_country_code": "PL",
+ "business_name": "Some business name for the account",
+ "business_state": "wielkopolska",
+ "business_street": "Some street 79A",
+ "business_zip": "66-777",
+ "capabilities": [
+ "DIRECT_SALES",
+ "VIEW_TAGS"
+ ],
+ "created_time": "2015-02-19T00:31:33+0100",
+ "currency": "PLN",
+ "daily_spend_limit": "93263",
+ "end_advertiser": 987654321,
+ "funding_source": "1122334455",
+ "is_personal": 1,
+ "media_agency": 54321,
+ "name": "This is a test account",
+ "offsite_pixels_tos_accepted": false,
+ "partner": 111222333,
+ "spend_cap": "0",
+ "timezone_id": 106,
+ "timezone_name": "Europe/Warsaw",
+ "timezone_offset_hours_utc": 2,
+ "users": {
+ "data": [
+ {
+ "id": "1234",
+ "permissions": [
+ 1,
+ 2,
+ 3
+ ],
+ "role": 1001
+ },
+ {
+ "id": "3421",
+ "permissions": [
+ 5,
+ 7
+ ],
+ "role": 1003
+ }
+ ]
+ },
+ "tax_id_status": 3
+}
\ No newline at end of file
diff --git a/spring-social-facebook/src/test/resources/org/springframework/social/facebook/api/ads/ad-account-with-funding-details.json b/spring-social-facebook/src/test/resources/org/springframework/social/facebook/api/ads/ad-account-with-funding-details.json
new file mode 100644
index 000000000..627697608
--- /dev/null
+++ b/spring-social-facebook/src/test/resources/org/springframework/social/facebook/api/ads/ad-account-with-funding-details.json
@@ -0,0 +1,58 @@
+{
+ "id": "act_123456789",
+ "account_groups": [
+ {
+ "account_group_id": "987654321",
+ "name": "Test group name",
+ "status": 1
+ }
+ ],
+ "account_id": 123456789,
+ "account_status": 1,
+ "age": 10.0036226851851852,
+ "amount_spent": "1234",
+ "balance": "10000",
+ "business_city": "Poznan",
+ "business_country_code": "PL",
+ "business_name": "Some business name for the account",
+ "business_state": "wielkopolska",
+ "business_street": "Some street 79A",
+ "business_zip": "66-777",
+ "capabilities": [
+ "DIRECT_SALES",
+ "VIEW_TAGS"
+ ],
+ "created_time": "2015-02-19T00:31:33+0100",
+ "currency": "PLN",
+ "daily_spend_limit": "93263",
+ "end_advertiser": 987654321,
+ "funding_source": "1122334455",
+ "funding_source_details": {
+ "id": "12345678987654321",
+ "display_string": "Visa *0001",
+ "type": 1
+ },
+ "is_personal": 1,
+ "media_agency": 54321,
+ "name": "This is a test account",
+ "offsite_pixels_tos_accepted": false,
+ "partner": 111222333,
+ "spend_cap": "0",
+ "timezone_id": 106,
+ "timezone_name": "Europe/Warsaw",
+ "timezone_offset_hours_utc": 2,
+ "users": {
+ "data": [
+ {
+ "id": "1234",
+ "permissions": [
+ 1,
+ 2,
+ 3
+ ],
+ "role": 1001
+ }
+ ]
+ },
+ "tax_id_status": 3
+}
\ No newline at end of file
diff --git a/spring-social-facebook/src/test/resources/org/springframework/social/facebook/api/ads/ad-account-with-tos-accepted.json b/spring-social-facebook/src/test/resources/org/springframework/social/facebook/api/ads/ad-account-with-tos-accepted.json
new file mode 100644
index 000000000..742a628a9
--- /dev/null
+++ b/spring-social-facebook/src/test/resources/org/springframework/social/facebook/api/ads/ad-account-with-tos-accepted.json
@@ -0,0 +1,57 @@
+{
+ "id": "act_123456789",
+ "account_groups": [
+ {
+ "account_group_id": "987654321",
+ "name": "Test group name",
+ "status": 1
+ }
+ ],
+ "account_id": 123456789,
+ "account_status": 1,
+ "age": 10.0036226851851852,
+ "amount_spent": "1234",
+ "balance": "10000",
+ "business_city": "Poznan",
+ "business_country_code": "PL",
+ "business_name": "Some business name for the account",
+ "business_state": "wielkopolska",
+ "business_street": "Some street 79A",
+ "business_zip": "66-777",
+ "capabilities": [
+ "DIRECT_SALES",
+ "VIEW_TAGS"
+ ],
+ "created_time": "2015-02-19T00:31:33+0100",
+ "currency": "PLN",
+ "daily_spend_limit": "93263",
+ "end_advertiser": 987654321,
+ "funding_source": "1122334455",
+ "is_personal": 1,
+ "media_agency": 54321,
+ "name": "This is a test account",
+ "offsite_pixels_tos_accepted": false,
+ "partner": 111222333,
+ "spend_cap": "0",
+ "timezone_id": 106,
+ "timezone_name": "Europe/Warsaw",
+ "timezone_offset_hours_utc": 2,
+ "tos_accepted": {
+ "206760949512025": 1,
+ "215449065224656": 1
+ },
+ "users": {
+ "data": [
+ {
+ "id": "1234",
+ "permissions": [
+ 1,
+ 2,
+ 3
+ ],
+ "role": 1001
+ }
+ ]
+ },
+ "tax_id_status": 3
+}
\ No newline at end of file
diff --git a/spring-social-facebook/src/test/resources/org/springframework/social/facebook/api/ads/ad-account-without-permission.json b/spring-social-facebook/src/test/resources/org/springframework/social/facebook/api/ads/ad-account-without-permission.json
new file mode 100644
index 000000000..dae3477ba
--- /dev/null
+++ b/spring-social-facebook/src/test/resources/org/springframework/social/facebook/api/ads/ad-account-without-permission.json
@@ -0,0 +1,50 @@
+{
+ "id": "act_123456789",
+ "account_groups": [
+ {
+ "account_group_id": "987654321",
+ "name": "Test group name",
+ "status": 1
+ }
+ ],
+ "account_id": 123456789,
+ "account_status": 1,
+ "age": 10.0036226851851852,
+ "amount_spent": "1234",
+ "balance": "10000",
+ "business_city": "Poznan",
+ "business_country_code": "PL",
+ "business_name": "Some business name for the account",
+ "business_state": "wielkopolska",
+ "business_street": "Some street 79A",
+ "business_zip": "66-777",
+ "capabilities": [
+ "DIRECT_SALES",
+ "VIEW_TAGS"
+ ],
+ "created_time": "2015-02-19T00:31:33+0100",
+ "currency": "PLN",
+ "daily_spend_limit": "93263",
+ "end_advertiser": 987654321,
+ "funding_source": "1122334455",
+ "is_personal": 1,
+ "media_agency": 54321,
+ "name": "This is a test account",
+ "offsite_pixels_tos_accepted": false,
+ "partner": 111222333,
+ "spend_cap": "0",
+ "timezone_id": 106,
+ "timezone_name": "Europe/Warsaw",
+ "timezone_offset_hours_utc": 2,
+ "users": {
+ "data": [
+ {
+ "id": "1234",
+ "permissions": [
+ ],
+ "role": 1001
+ }
+ ]
+ },
+ "tax_id_status": 3
+}
\ No newline at end of file
diff --git a/spring-social-facebook/src/test/resources/org/springframework/social/facebook/api/ads/ad-account.json b/spring-social-facebook/src/test/resources/org/springframework/social/facebook/api/ads/ad-account.json
new file mode 100644
index 000000000..1dfeb8673
--- /dev/null
+++ b/spring-social-facebook/src/test/resources/org/springframework/social/facebook/api/ads/ad-account.json
@@ -0,0 +1,53 @@
+{
+ "id": "act_123456789",
+ "account_groups": [
+ {
+ "account_group_id": "987654321",
+ "name": "Test group name",
+ "status": 1
+ }
+ ],
+ "account_id": 123456789,
+ "account_status": 1,
+ "age": 10.0036226851851852,
+ "amount_spent": "1234",
+ "balance": "10000",
+ "business_city": "Poznan",
+ "business_country_code": "PL",
+ "business_name": "Some business name for the account",
+ "business_state": "wielkopolska",
+ "business_street": "Some street 79A",
+ "business_zip": "66-777",
+ "capabilities": [
+ "DIRECT_SALES",
+ "VIEW_TAGS"
+ ],
+ "created_time": "2015-02-19T00:31:33+0100",
+ "currency": "PLN",
+ "daily_spend_limit": "93263",
+ "end_advertiser": 987654321,
+ "funding_source": "1122334455",
+ "is_personal": 1,
+ "media_agency": 54321,
+ "name": "This is a test account",
+ "offsite_pixels_tos_accepted": false,
+ "partner": 111222333,
+ "spend_cap": "0",
+ "timezone_id": 106,
+ "timezone_name": "Europe/Warsaw",
+ "timezone_offset_hours_utc": 2,
+ "users": {
+ "data": [
+ {
+ "id": "1234",
+ "permissions": [
+ 1,
+ 2,
+ 3
+ ],
+ "role": 1001
+ }
+ ]
+ },
+ "tax_id_status": 3
+}
\ No newline at end of file
diff --git a/spring-social-facebook/src/test/resources/org/springframework/social/facebook/api/ads/ad-accounts.json b/spring-social-facebook/src/test/resources/org/springframework/social/facebook/api/ads/ad-accounts.json
new file mode 100644
index 000000000..d2636341a
--- /dev/null
+++ b/spring-social-facebook/src/test/resources/org/springframework/social/facebook/api/ads/ad-accounts.json
@@ -0,0 +1,122 @@
+{
+ "data": [
+ {
+ "id": "act_123456789",
+ "account_groups": [
+ {
+ "account_group_id": "987654321",
+ "name": "Test group name",
+ "status": 1
+ }
+ ],
+ "account_id": 123456789,
+ "account_status": 1,
+ "age": 10.0036226851851852,
+ "amount_spent": "1234",
+ "balance": "10000",
+ "business_city": "Poznan",
+ "business_country_code": "PL",
+ "business_name": "Some business name for the account",
+ "business_state": "wielkopolska",
+ "business_street": "Some street 79A",
+ "business_zip": "66-777",
+ "capabilities": [
+ "DIRECT_SALES",
+ "VIEW_TAGS"
+ ],
+ "created_time": "2015-02-19T00:31:33+0100",
+ "currency": "PLN",
+ "daily_spend_limit": "93263",
+ "end_advertiser": 987654321,
+ "funding_source": "1122334455",
+ "is_personal": 1,
+ "media_agency": 54321,
+ "name": "This is a test account",
+ "offsite_pixels_tos_accepted": false,
+ "partner": 111222333,
+ "spend_cap": "0",
+ "timezone_id": 106,
+ "timezone_name": "Europe/Warsaw",
+ "timezone_offset_hours_utc": 2,
+ "users": {
+ "data": [
+ {
+ "id": "1234",
+ "permissions": [
+ 1,
+ 2,
+ 3
+ ],
+ "role": 1001
+ }
+ ]
+ },
+ "tax_id_status": 3
+ },
+ {
+ "id": "act_77777777",
+ "account_groups": [
+ {
+ "account_group_id": "987654321",
+ "name": "Test group name",
+ "status": 1
+ },
+ {
+ "account_group_id": "11223344",
+ "name": "Test group name 2",
+ "status": 1
+ }
+
+ ],
+ "account_id": 77777777,
+ "account_status": 2,
+ "age": 777.777,
+ "amount_spent": 7777,
+ "balance": 77777,
+ "business_city": "Warsaw",
+ "business_country_code": "PL",
+ "business_name": "Some business name for the account 2",
+ "business_state": "mazowieckie",
+ "business_street": "Some street 2",
+ "business_zip": "77-777",
+ "capabilities": [
+ "DIRECT_SALES",
+ "VIEW_TAGS"
+ ],
+ "created_time": "2015-04-20T00:31:33+0100",
+ "currency": "PLN",
+ "daily_spend_limit": 77,
+ "end_advertiser": 987654321,
+ "funding_source": "77",
+ "is_personal": 1,
+ "media_agency": 54321,
+ "name": "This is a test account 2",
+ "offsite_pixels_tos_accepted": false,
+ "partner": 111222333,
+ "spend_cap": 0,
+ "timezone_id": 106,
+ "timezone_name": "Europe/Warsaw",
+ "timezone_offset_hours_utc": 2,
+ "users": {
+ "data": [
+ {
+ "id": "1234",
+ "permissions": [
+ 1,
+ 2,
+ 3
+ ],
+ "role": 1001
+ }
+ ]
+ },
+ "tax_id_status": 5
+ }
+ ],
+ "paging": {
+ "cursors": {
+ "before": "NjAyMjEyODA0OTQ1MA==",
+ "after": "NjAyMjEyODA0OTQ1MA=="
+ }
+ }
+}
\ No newline at end of file
diff --git a/spring-social-facebook/src/test/resources/org/springframework/social/facebook/api/ads/ad-campaign-empty-buying-type.json b/spring-social-facebook/src/test/resources/org/springframework/social/facebook/api/ads/ad-campaign-empty-buying-type.json
new file mode 100644
index 000000000..221b3d224
--- /dev/null
+++ b/spring-social-facebook/src/test/resources/org/springframework/social/facebook/api/ads/ad-campaign-empty-buying-type.json
@@ -0,0 +1,8 @@
+{
+ "id": "609123456789",
+ "account_id": "123456789",
+ "campaign_group_status": "ACTIVE",
+ "name": "The test campaign name",
+ "objective": "POST_ENGAGEMENT",
+ "spend_cap": 1000
+}
\ No newline at end of file
diff --git a/spring-social-facebook/src/test/resources/org/springframework/social/facebook/api/ads/ad-campaign-insights.json b/spring-social-facebook/src/test/resources/org/springframework/social/facebook/api/ads/ad-campaign-insights.json
new file mode 100644
index 000000000..2b40792de
--- /dev/null
+++ b/spring-social-facebook/src/test/resources/org/springframework/social/facebook/api/ads/ad-campaign-insights.json
@@ -0,0 +1,105 @@
+{
+ "data": [
+ {
+ "account_id": "123456789",
+ "account_name": "Test account name",
+ "date_start": "2015-04-12",
+ "date_stop": "2015-04-13",
+ "actions_per_impression": 0.016042780748663,
+ "clicks": 8,
+ "unique_clicks": 5,
+ "cost_per_result": 0.66666666666667,
+ "cost_per_total_action": 0.66666666666667,
+ "cpc": 0.25,
+ "cost_per_unique_click": 0.4,
+ "cpm": 10.695187165775,
+ "cpp": 10.869565217391,
+ "ctr": 4.2780748663102,
+ "unique_ctr": 2.7173913043478,
+ "frequency": 1.0163043478261,
+ "impressions": "187",
+ "unique_impressions": 184,
+ "objective": "POST_ENGAGEMENT",
+ "reach": 184,
+ "result_rate": 1.6042780748663,
+ "results": 3,
+ "roas": 0,
+ "social_clicks": 0,
+ "unique_social_clicks": 0,
+ "social_impressions": 0,
+ "unique_social_impressions": 0,
+ "social_reach": 0,
+ "spend": 2,
+ "today_spend": 0,
+ "total_action_value": 0,
+ "total_actions": 3,
+ "total_unique_actions": 2,
+ "actions": [
+ {
+ "action_type": "comment",
+ "value": 2
+ },
+ {
+ "action_type": "post_like",
+ "value": 1
+ },
+ {
+ "action_type": "page_engagement",
+ "value": 3
+ },
+ {
+ "action_type": "post_engagement",
+ "value": 3
+ }
+ ],
+ "unique_actions": [
+ {
+ "action_type": "comment",
+ "value": 1
+ },
+ {
+ "action_type": "post_like",
+ "value": 1
+ },
+ {
+ "action_type": "page_engagement",
+ "value": 2
+ },
+ {
+ "action_type": "post_engagement",
+ "value": 2
+ }
+ ],
+ "cost_per_action_type": [
+ {
+ "action_type": "comment",
+ "value": 1
+ },
+ {
+ "action_type": "post_like",
+ "value": 2
+ },
+ {
+ "action_type": "page_engagement",
+ "value": 0.66666666666667
+ },
+ {
+ "action_type": "post_engagement",
+ "value": 0.66666666666667
+ }
+ ],
+ "video_start_actions": [
+ {
+ "action_type": "video_view",
+ "value": 0
+ }
+ ]
+ }
+ ],
+ "paging": {
+ "cursors": {
+ "before": "MA==",
+ "after": "MA=="
+ }
+ }
+}
\ No newline at end of file
diff --git a/spring-social-facebook/src/test/resources/org/springframework/social/facebook/api/ads/ad-campaign-with-unknown-enums.json b/spring-social-facebook/src/test/resources/org/springframework/social/facebook/api/ads/ad-campaign-with-unknown-enums.json
new file mode 100644
index 000000000..d1489a710
--- /dev/null
+++ b/spring-social-facebook/src/test/resources/org/springframework/social/facebook/api/ads/ad-campaign-with-unknown-enums.json
@@ -0,0 +1,8 @@
+{
+ "id": "600123456789",
+ "account_id": "123456789",
+ "buying_type": "OTHER",
+ "campaign_group_status": "DEACTIVATED",
+ "name": "The test campaign name",
+ "objective": "AD_SELLING"
+}
\ No newline at end of file
diff --git a/spring-social-facebook/src/test/resources/org/springframework/social/facebook/api/ads/ad-campaign.json b/spring-social-facebook/src/test/resources/org/springframework/social/facebook/api/ads/ad-campaign.json
new file mode 100644
index 000000000..cf9511acc
--- /dev/null
+++ b/spring-social-facebook/src/test/resources/org/springframework/social/facebook/api/ads/ad-campaign.json
@@ -0,0 +1,9 @@
+{
+ "id": "600123456789",
+ "account_id": "123456789",
+ "buying_type": "AUCTION",
+ "campaign_group_status": "ACTIVE",
+ "name": "The test campaign name",
+ "objective": "POST_ENGAGEMENT",
+ "spend_cap": 1000
+}
\ No newline at end of file
diff --git a/spring-social-facebook/src/test/resources/org/springframework/social/facebook/api/ads/ad-creative-link-ad.json b/spring-social-facebook/src/test/resources/org/springframework/social/facebook/api/ads/ad-creative-link-ad.json
new file mode 100644
index 000000000..c722b4179
--- /dev/null
+++ b/spring-social-facebook/src/test/resources/org/springframework/social/facebook/api/ads/ad-creative-link-ad.json
@@ -0,0 +1,12 @@
+{
+ "body": "This is the body of the ad creative",
+ "image_hash": "ac67a06abb35a3bef8256c8c9085548e",
+ "image_url": "https://example.com/images/image.png",
+ "name": "Name of the ad creative",
+ "object_url": "http://example.com/objects/object",
+ "run_status": "ACTIVE",
+ "title": "Title of the ad creative",
+ "thumbnail_url": "https://example.com/thumbnails/thuumbnail_url",
+ "object_type": "DOMAIN",
+ "id": "803123456789"
+}
\ No newline at end of file
diff --git a/spring-social-facebook/src/test/resources/org/springframework/social/facebook/api/ads/ad-creative-wrong-status.json b/spring-social-facebook/src/test/resources/org/springframework/social/facebook/api/ads/ad-creative-wrong-status.json
new file mode 100644
index 000000000..2aeec90b0
--- /dev/null
+++ b/spring-social-facebook/src/test/resources/org/springframework/social/facebook/api/ads/ad-creative-wrong-status.json
@@ -0,0 +1,12 @@
+{
+ "body": "This is the body of the ad creative",
+ "image_hash": "ac67a06abb35a3bef8256c8c9085548e",
+ "image_url": "https://example.com/images/image.png",
+ "name": "Name of the ad creative",
+ "object_url": "http://example.com/objects/object",
+ "run_status": "WRONG_STATUS",
+ "title": "Title of the ad creative",
+ "thumbnail_url": "https://example.com/thumbnails/thuumbnail_url",
+ "object_type": "DOMAIN",
+ "id": "802123456789"
+}
\ No newline at end of file
diff --git a/spring-social-facebook/src/test/resources/org/springframework/social/facebook/api/ads/ad-creative-wrong-type.json b/spring-social-facebook/src/test/resources/org/springframework/social/facebook/api/ads/ad-creative-wrong-type.json
new file mode 100644
index 000000000..f4051aabb
--- /dev/null
+++ b/spring-social-facebook/src/test/resources/org/springframework/social/facebook/api/ads/ad-creative-wrong-type.json
@@ -0,0 +1,12 @@
+{
+ "body": "This is the body of the ad creative",
+ "image_hash": "ac67a06abb35a3bef8256c8c9085548e",
+ "image_url": "https://example.com/images/image.png",
+ "name": "Name of the ad creative",
+ "object_url": "http://example.com/objects/object",
+ "run_status": "ACTIVE",
+ "title": "Title of the ad creative",
+ "thumbnail_url": "https://example.com/thumbnails/thuumbnail_url",
+ "object_type": "WRONG_TYPE",
+ "id": "801123456789"
+}
\ No newline at end of file
diff --git a/spring-social-facebook/src/test/resources/org/springframework/social/facebook/api/ads/ad-creative.json b/spring-social-facebook/src/test/resources/org/springframework/social/facebook/api/ads/ad-creative.json
new file mode 100644
index 000000000..bd207d7a0
--- /dev/null
+++ b/spring-social-facebook/src/test/resources/org/springframework/social/facebook/api/ads/ad-creative.json
@@ -0,0 +1,8 @@
+{
+ "name": "Creative test name",
+ "object_story_id": "123456789_987654321",
+ "run_status": "ACTIVE",
+ "thumbnail_url": "https://example.net/safe_image.php?d=123456789",
+ "object_type": "STATUS",
+ "id": "800123456789"
+}
\ No newline at end of file
diff --git a/spring-social-facebook/src/test/resources/org/springframework/social/facebook/api/ads/ad-creatives.json b/spring-social-facebook/src/test/resources/org/springframework/social/facebook/api/ads/ad-creatives.json
new file mode 100644
index 000000000..1fd77f820
--- /dev/null
+++ b/spring-social-facebook/src/test/resources/org/springframework/social/facebook/api/ads/ad-creatives.json
@@ -0,0 +1,41 @@
+{
+ "data": [
+ {
+ "body": "This is the body of the ad creative1",
+ "image_hash": "ac67a06abb35a3bef8256c8c9085548e1",
+ "image_url": "https://example.com/images/image1.png",
+ "name": "Name of the ad creative1",
+ "object_id": "900123456789",
+ "run_status": "ACTIVE",
+ "thumbnail_url": "https://example.com/thumbnails/thuumbnail_url1",
+ "object_type": "PAGE",
+ "id": "801123456789"
+ },
+ {
+ "body": "This is the body of the ad creative2",
+ "image_hash": "ac67a06abb35a3bef8256c8c9085548e2",
+ "image_url": "https://example.com/images/image2.png",
+ "name": "Name of the ad creative2",
+ "object_url": "http://example.com/objects/object",
+ "run_status": "ACTIVE",
+ "title": "Test Ad Creative",
+ "thumbnail_url": "https://example.com/thumbnails/thuumbnail_url2",
+ "object_type": "DOMAIN",
+ "id": "802123456789"
+ },
+ {
+ "name": "This is the body of the ad creative3",
+ "object_story_id": "987654321_123456789",
+ "run_status": "ACTIVE",
+ "thumbnail_url": "https://example.com/thumbnails/thuumbnail_url3",
+ "object_type": "STATUS",
+ "id": "803123456789"
+ }
+ ],
+ "paging": {
+ "cursors": {
+ "before": "NjAyNjU3MzQzNDQ1MA==",
+ "after": "NjAyNDE2Nzg5NjY1MA=="
+ }
+ }
+}
\ No newline at end of file
diff --git a/spring-social-facebook/src/test/resources/org/springframework/social/facebook/api/ads/ad-insights.json b/spring-social-facebook/src/test/resources/org/springframework/social/facebook/api/ads/ad-insights.json
new file mode 100644
index 000000000..2b40792de
--- /dev/null
+++ b/spring-social-facebook/src/test/resources/org/springframework/social/facebook/api/ads/ad-insights.json
@@ -0,0 +1,105 @@
+{
+ "data": [
+ {
+ "account_id": "123456789",
+ "account_name": "Test account name",
+ "date_start": "2015-04-12",
+ "date_stop": "2015-04-13",
+ "actions_per_impression": 0.016042780748663,
+ "clicks": 8,
+ "unique_clicks": 5,
+ "cost_per_result": 0.66666666666667,
+ "cost_per_total_action": 0.66666666666667,
+ "cpc": 0.25,
+ "cost_per_unique_click": 0.4,
+ "cpm": 10.695187165775,
+ "cpp": 10.869565217391,
+ "ctr": 4.2780748663102,
+ "unique_ctr": 2.7173913043478,
+ "frequency": 1.0163043478261,
+ "impressions": "187",
+ "unique_impressions": 184,
+ "objective": "POST_ENGAGEMENT",
+ "reach": 184,
+ "result_rate": 1.6042780748663,
+ "results": 3,
+ "roas": 0,
+ "social_clicks": 0,
+ "unique_social_clicks": 0,
+ "social_impressions": 0,
+ "unique_social_impressions": 0,
+ "social_reach": 0,
+ "spend": 2,
+ "today_spend": 0,
+ "total_action_value": 0,
+ "total_actions": 3,
+ "total_unique_actions": 2,
+ "actions": [
+ {
+ "action_type": "comment",
+ "value": 2
+ },
+ {
+ "action_type": "post_like",
+ "value": 1
+ },
+ {
+ "action_type": "page_engagement",
+ "value": 3
+ },
+ {
+ "action_type": "post_engagement",
+ "value": 3
+ }
+ ],
+ "unique_actions": [
+ {
+ "action_type": "comment",
+ "value": 1
+ },
+ {
+ "action_type": "post_like",
+ "value": 1
+ },
+ {
+ "action_type": "page_engagement",
+ "value": 2
+ },
+ {
+ "action_type": "post_engagement",
+ "value": 2
+ }
+ ],
+ "cost_per_action_type": [
+ {
+ "action_type": "comment",
+ "value": 1
+ },
+ {
+ "action_type": "post_like",
+ "value": 2
+ },
+ {
+ "action_type": "page_engagement",
+ "value": 0.66666666666667
+ },
+ {
+ "action_type": "post_engagement",
+ "value": 0.66666666666667
+ }
+ ],
+ "video_start_actions": [
+ {
+ "action_type": "video_view",
+ "value": 0
+ }
+ ]
+ }
+ ],
+ "paging": {
+ "cursors": {
+ "before": "MA==",
+ "after": "MA=="
+ }
+ }
+}
\ No newline at end of file
diff --git a/spring-social-facebook/src/test/resources/org/springframework/social/facebook/api/ads/ad-same-account.json b/spring-social-facebook/src/test/resources/org/springframework/social/facebook/api/ads/ad-same-account.json
new file mode 100644
index 000000000..6f2fea91c
--- /dev/null
+++ b/spring-social-facebook/src/test/resources/org/springframework/social/facebook/api/ads/ad-same-account.json
@@ -0,0 +1,73 @@
+{
+ "data": [
+ {
+ "id": "101123456789",
+ "account_id": "123456789",
+ "adgroup_status": "ACTIVE",
+ "bid_type": "ABSOLUTE_OCPM",
+ "campaign_id": "801123456789",
+ "campaign_group_id": "701123456789",
+ "created_time": "2015-04-10T09:28:54+0200",
+ "creative": {
+ "id": "900123456789"
+ },
+ "name": "Test ad name1",
+ "targeting": {
+ "geo_locations": {
+ "countries": [
+ "PL"
+ ]
+ }
+ },
+ "updated_time": "2015-04-10T13:32:09+0200"
+ },
+ {
+ "id": "102123456789",
+ "account_id": "123456789",
+ "adgroup_status": "ACTIVE",
+ "bid_type": "ABSOLUTE_OCPM",
+ "campaign_id": "802123456789",
+ "campaign_group_id": "702123456789",
+ "created_time": "2015-04-10T09:28:54+0200",
+ "creative": {
+ "id": "900123456789"
+ },
+ "name": "Test ad name2",
+ "targeting": {
+ "geo_locations": {
+ "countries": [
+ "PL"
+ ]
+ }
+ },
+ "updated_time": "2015-04-10T13:32:09+0200"
+ },
+ {
+ "id": "103123456789",
+ "account_id": "123456789",
+ "adgroup_status": "ACTIVE",
+ "bid_type": "ABSOLUTE_OCPM",
+ "campaign_id": "803123456789",
+ "campaign_group_id": "703123456789",
+ "created_time": "2015-04-10T09:28:54+0200",
+ "creative": {
+ "id": "900123456789"
+ },
+ "name": "Test ad name3",
+ "targeting": {
+ "geo_locations": {
+ "countries": [
+ "PL"
+ ]
+ }
+ },
+ "updated_time": "2015-04-10T13:32:09+0200"
+ }
+ ],
+ "paging": {
+ "cursors": {
+ "before": "NjAyMjEyODA0OTQ1MA==",
+ "after": "NjAyMjEyODA0OTQ1MA=="
+ }
+ }
+}
\ No newline at end of file
diff --git a/spring-social-facebook/src/test/resources/org/springframework/social/facebook/api/ads/ad-same-ad-set.json b/spring-social-facebook/src/test/resources/org/springframework/social/facebook/api/ads/ad-same-ad-set.json
new file mode 100644
index 000000000..b472825e6
--- /dev/null
+++ b/spring-social-facebook/src/test/resources/org/springframework/social/facebook/api/ads/ad-same-ad-set.json
@@ -0,0 +1,52 @@
+{
+ "data": [
+ {
+ "id": "101123456789",
+ "account_id": "123456789",
+ "adgroup_status": "ACTIVE",
+ "bid_type": "ABSOLUTE_OCPM",
+ "campaign_id": "800123456789",
+ "campaign_group_id": "700123456789",
+ "created_time": "2015-04-10T09:28:54+0200",
+ "creative": {
+ "id": "900123456789"
+ },
+ "name": "Test ad name1",
+ "targeting": {
+ "geo_locations": {
+ "countries": [
+ "PL"
+ ]
+ }
+ },
+ "updated_time": "2015-04-10T13:32:09+0200"
+ },
+ {
+ "id": "102123456789",
+ "account_id": "123456789",
+ "adgroup_status": "ACTIVE",
+ "bid_type": "ABSOLUTE_OCPM",
+ "campaign_id": "800123456789",
+ "campaign_group_id": "701123456789",
+ "created_time": "2015-04-10T09:28:54+0200",
+ "creative": {
+ "id": "900123456789"
+ },
+ "name": "Test ad name2",
+ "targeting": {
+ "geo_locations": {
+ "countries": [
+ "PL"
+ ]
+ }
+ },
+ "updated_time": "2015-04-10T13:32:09+0200"
+ }
+ ],
+ "paging": {
+ "cursors": {
+ "before": "NjAyMjEyODA0OTQ1MA==",
+ "after": "NjAyMjEyODA0OTQ1MA=="
+ }
+ }
+}
\ No newline at end of file
diff --git a/spring-social-facebook/src/test/resources/org/springframework/social/facebook/api/ads/ad-same-campaign.json b/spring-social-facebook/src/test/resources/org/springframework/social/facebook/api/ads/ad-same-campaign.json
new file mode 100644
index 000000000..99a36f3ce
--- /dev/null
+++ b/spring-social-facebook/src/test/resources/org/springframework/social/facebook/api/ads/ad-same-campaign.json
@@ -0,0 +1,73 @@
+{
+ "data": [
+ {
+ "id": "101123456789",
+ "account_id": "123456789",
+ "adgroup_status": "ACTIVE",
+ "bid_type": "ABSOLUTE_OCPM",
+ "campaign_id": "801123456789",
+ "campaign_group_id": "701123456789",
+ "created_time": "2015-04-10T09:28:54+0200",
+ "creative": {
+ "id": "900123456789"
+ },
+ "name": "Test ad name1",
+ "targeting": {
+ "geo_locations": {
+ "countries": [
+ "PL"
+ ]
+ }
+ },
+ "updated_time": "2015-04-10T13:32:09+0200"
+ },
+ {
+ "id": "102123456789",
+ "account_id": "123456789",
+ "adgroup_status": "ACTIVE",
+ "bid_type": "ABSOLUTE_OCPM",
+ "campaign_id": "802123456789",
+ "campaign_group_id": "701123456789",
+ "created_time": "2015-04-10T09:28:54+0200",
+ "creative": {
+ "id": "900123456789"
+ },
+ "name": "Test ad name2",
+ "targeting": {
+ "geo_locations": {
+ "countries": [
+ "PL"
+ ]
+ }
+ },
+ "updated_time": "2015-04-10T13:32:09+0200"
+ },
+ {
+ "id": "103123456789",
+ "account_id": "123456789",
+ "adgroup_status": "ACTIVE",
+ "bid_type": "ABSOLUTE_OCPM",
+ "campaign_id": "803123456789",
+ "campaign_group_id": "701123456789",
+ "created_time": "2015-04-10T09:28:54+0200",
+ "creative": {
+ "id": "900123456789"
+ },
+ "name": "Test ad name3",
+ "targeting": {
+ "geo_locations": {
+ "countries": [
+ "PL"
+ ]
+ }
+ },
+ "updated_time": "2015-04-10T13:32:09+0200"
+ }
+ ],
+ "paging": {
+ "cursors": {
+ "before": "NjAyMjEyODA0OTQ1MA==",
+ "after": "NjAyMjEyODA0OTQ1MA=="
+ }
+ }
+}
\ No newline at end of file
diff --git a/spring-social-facebook/src/test/resources/org/springframework/social/facebook/api/ads/ad-set-insights.json b/spring-social-facebook/src/test/resources/org/springframework/social/facebook/api/ads/ad-set-insights.json
new file mode 100644
index 000000000..2b40792de
--- /dev/null
+++ b/spring-social-facebook/src/test/resources/org/springframework/social/facebook/api/ads/ad-set-insights.json
@@ -0,0 +1,105 @@
+{
+ "data": [
+ {
+ "account_id": "123456789",
+ "account_name": "Test account name",
+ "date_start": "2015-04-12",
+ "date_stop": "2015-04-13",
+ "actions_per_impression": 0.016042780748663,
+ "clicks": 8,
+ "unique_clicks": 5,
+ "cost_per_result": 0.66666666666667,
+ "cost_per_total_action": 0.66666666666667,
+ "cpc": 0.25,
+ "cost_per_unique_click": 0.4,
+ "cpm": 10.695187165775,
+ "cpp": 10.869565217391,
+ "ctr": 4.2780748663102,
+ "unique_ctr": 2.7173913043478,
+ "frequency": 1.0163043478261,
+ "impressions": "187",
+ "unique_impressions": 184,
+ "objective": "POST_ENGAGEMENT",
+ "reach": 184,
+ "result_rate": 1.6042780748663,
+ "results": 3,
+ "roas": 0,
+ "social_clicks": 0,
+ "unique_social_clicks": 0,
+ "social_impressions": 0,
+ "unique_social_impressions": 0,
+ "social_reach": 0,
+ "spend": 2,
+ "today_spend": 0,
+ "total_action_value": 0,
+ "total_actions": 3,
+ "total_unique_actions": 2,
+ "actions": [
+ {
+ "action_type": "comment",
+ "value": 2
+ },
+ {
+ "action_type": "post_like",
+ "value": 1
+ },
+ {
+ "action_type": "page_engagement",
+ "value": 3
+ },
+ {
+ "action_type": "post_engagement",
+ "value": 3
+ }
+ ],
+ "unique_actions": [
+ {
+ "action_type": "comment",
+ "value": 1
+ },
+ {
+ "action_type": "post_like",
+ "value": 1
+ },
+ {
+ "action_type": "page_engagement",
+ "value": 2
+ },
+ {
+ "action_type": "post_engagement",
+ "value": 2
+ }
+ ],
+ "cost_per_action_type": [
+ {
+ "action_type": "comment",
+ "value": 1
+ },
+ {
+ "action_type": "post_like",
+ "value": 2
+ },
+ {
+ "action_type": "page_engagement",
+ "value": 0.66666666666667
+ },
+ {
+ "action_type": "post_engagement",
+ "value": 0.66666666666667
+ }
+ ],
+ "video_start_actions": [
+ {
+ "action_type": "video_view",
+ "value": 0
+ }
+ ]
+ }
+ ],
+ "paging": {
+ "cursors": {
+ "before": "MA==",
+ "after": "MA=="
+ }
+ }
+}
\ No newline at end of file
diff --git a/spring-social-facebook/src/test/resources/org/springframework/social/facebook/api/ads/ad-set-promoted-object.json b/spring-social-facebook/src/test/resources/org/springframework/social/facebook/api/ads/ad-set-promoted-object.json
new file mode 100644
index 000000000..41838d2ec
--- /dev/null
+++ b/spring-social-facebook/src/test/resources/org/springframework/social/facebook/api/ads/ad-set-promoted-object.json
@@ -0,0 +1,33 @@
+{
+ "id": "705123456789",
+ "account_id": "123456789",
+ "bid_info": {
+ "CLICKS": 500
+ },
+ "bid_type": "ABSOLUTE_OCPM",
+ "budget_remaining": 807,
+ "campaign_group_id": "600123456789",
+ "campaign_status": "PAUSED",
+ "created_time": "2015-07-06T14:18:55+0200",
+ "daily_budget": 2000,
+ "is_autobid": false,
+ "lifetime_budget": 0,
+ "name": "Test promoted object",
+ "promoted_object": {
+ "page_id": "999888777666555"
+ },
+ "start_time": "2015-07-06T14:18:55+0200",
+ "targeting": {
+ "age_max": 65,
+ "age_min": 18,
+ "geo_locations": {
+ "countries": [
+ "PL"
+ ],
+ "location_types": [
+ "home"
+ ]
+ }
+ },
+ "updated_time": "2015-07-06T14:18:55+0200"
+}
\ No newline at end of file
diff --git a/spring-social-facebook/src/test/resources/org/springframework/social/facebook/api/ads/ad-set-wrong-bid-type.json b/spring-social-facebook/src/test/resources/org/springframework/social/facebook/api/ads/ad-set-wrong-bid-type.json
new file mode 100644
index 000000000..372393f63
--- /dev/null
+++ b/spring-social-facebook/src/test/resources/org/springframework/social/facebook/api/ads/ad-set-wrong-bid-type.json
@@ -0,0 +1,23 @@
+{
+ "id": "710123456789",
+ "account_id": "123456789",
+ "campaign_group_id": "600123456789",
+ "name": "Test AdSet",
+ "campaign_status": "ACTIVE",
+ "is_autobid": true,
+ "bid_type": "WRONG_BID_TYPE",
+ "budget_remaining": 50,
+ "daily_budget": 0,
+ "lifetime_budget": 200,
+ "targeting": {
+ "geo_locations": {
+ "countries": [
+ "PL"
+ ]
+ }
+ },
+ "start_time": "2015-04-12T09:19:00+0200",
+ "end_time": "2015-04-13T09:19:00+0200",
+ "created_time": "2015-04-10T09:28:54+0200",
+ "updated_time": "2015-04-10T13:32:09+0200"
+}
\ No newline at end of file
diff --git a/spring-social-facebook/src/test/resources/org/springframework/social/facebook/api/ads/ad-set-wrong-status.json b/spring-social-facebook/src/test/resources/org/springframework/social/facebook/api/ads/ad-set-wrong-status.json
new file mode 100644
index 000000000..44a73105c
--- /dev/null
+++ b/spring-social-facebook/src/test/resources/org/springframework/social/facebook/api/ads/ad-set-wrong-status.json
@@ -0,0 +1,23 @@
+{
+ "id": "709123456789",
+ "account_id": "123456789",
+ "campaign_group_id": "600123456789",
+ "name": "Test AdSet",
+ "campaign_status": "WRONG_STATUS",
+ "is_autobid": true,
+ "bid_type": "ABSOLUTE_OCPM",
+ "budget_remaining": 50,
+ "daily_budget": 0,
+ "lifetime_budget": 200,
+ "targeting": {
+ "geo_locations": {
+ "countries": [
+ "PL"
+ ]
+ }
+ },
+ "start_time": "2015-04-12T09:19:00+0200",
+ "end_time": "2015-04-13T09:19:00+0200",
+ "created_time": "2015-04-10T09:28:54+0200",
+ "updated_time": "2015-04-10T13:32:09+0200"
+}
\ No newline at end of file
diff --git a/spring-social-facebook/src/test/resources/org/springframework/social/facebook/api/ads/ad-set.json b/spring-social-facebook/src/test/resources/org/springframework/social/facebook/api/ads/ad-set.json
new file mode 100644
index 000000000..88619f964
--- /dev/null
+++ b/spring-social-facebook/src/test/resources/org/springframework/social/facebook/api/ads/ad-set.json
@@ -0,0 +1,106 @@
+{
+ "id": "700123456789",
+ "account_id": "123456789",
+ "campaign_group_id": "600123456789",
+ "name": "Test AdSet",
+ "campaign_status": "ACTIVE",
+ "is_autobid": true,
+ "bid_type": "ABSOLUTE_OCPM",
+ "budget_remaining": 50,
+ "daily_budget": 0,
+ "lifetime_budget": 200,
+ "targeting": {
+ "age_max": 20,
+ "age_min": 18,
+ "behaviors": [
+ {
+ "id": "6004854404172",
+ "name": "Technology late adopters"
+ }
+ ],
+ "genders": [
+ 1
+ ],
+ "relationship_statuses": [
+ 2,
+ 9
+ ],
+ "interested_in": [
+ 2
+ ],
+ "geo_locations": {
+ "countries": [
+ "PL"
+ ],
+ "regions": [
+ {
+ "key": "3847"
+ },
+ {
+ "key": "1122"
+ }
+ ],
+ "cities": [
+ {
+ "key": "2430536",
+ "radius": 12,
+ "distance_unit": "mile"
+ },
+ {
+ "key": "11223344",
+ "radius": 55,
+ "distance_unit": "kilometer"
+ }
+ ],
+ "zips": [
+ {
+ "key": "US:94304"
+ },
+ {
+ "key": "US:00501"
+ }
+ ],
+ "location_types": [
+ "home",
+ "recent"
+ ]
+ },
+ "interests": [
+ {
+ "id": "6003629266583",
+ "name": "Hard drives"
+ }
+ ],
+ "page_types": [
+ "feed",
+ "desktop-and-mobile-and-external"
+ ],
+ "education_schools": [
+ {
+ "id": 105930651606,
+ "name": "Harvard University"
+ }
+ ],
+ "education_statuses": [
+ 1,
+ 9,
+ 13
+ ],
+ "work_employers": [
+ {
+ "id": "50431654",
+ "name": "Microsoft"
+ }
+ ],
+ "work_positions": [
+ {
+ "id": 105763692790962,
+ "name": "Business Analyst"
+ }
+ ]
+ },
+ "start_time": "2015-04-12T09:19:00+0200",
+ "end_time": "2015-04-13T09:19:00+0200",
+ "created_time": "2015-04-10T09:28:54+0200",
+ "updated_time": "2015-04-10T13:32:09+0200"
+}
\ No newline at end of file
diff --git a/spring-social-facebook/src/test/resources/org/springframework/social/facebook/api/ads/ad-sets.json b/spring-social-facebook/src/test/resources/org/springframework/social/facebook/api/ads/ad-sets.json
new file mode 100644
index 000000000..306032dcf
--- /dev/null
+++ b/spring-social-facebook/src/test/resources/org/springframework/social/facebook/api/ads/ad-sets.json
@@ -0,0 +1,85 @@
+{
+ "data": [
+ {
+ "account_id": "123456789",
+ "bid_type": "ABSOLUTE_OCPM",
+ "budget_remaining": 37407,
+ "campaign_group_id": "600123456789",
+ "campaign_status": "PAUSED",
+ "created_time": "2015-05-27T11:58:34+0200",
+ "daily_budget": 40000,
+ "end_time": "2015-05-29T22:26:40+0200",
+ "id": "700123456789",
+ "is_autobid": true,
+ "lifetime_budget": 0,
+ "name": "Test AdSet",
+ "start_time": "2015-05-27T11:58:34+0200",
+ "targeting": {
+ "age_max": 65,
+ "age_min": 18,
+ "geo_locations": {
+ "countries": [
+ "BR"
+ ],
+ "location_types": [
+ "home"
+ ]
+ }
+ },
+ "updated_time": "2015-05-27T11:58:34+0200"
+ },
+ {
+ "account_id": "123456789",
+ "bid_type": "ABSOLUTE_OCPM",
+ "budget_remaining": 0,
+ "campaign_group_id": "600123456789",
+ "campaign_status": "ACTIVE",
+ "created_time": "2015-04-10T09:28:54+0200",
+ "daily_budget": 0,
+ "end_time": "2015-04-13T09:19:00+0200",
+ "id": "701123456789",
+ "is_autobid": true,
+ "lifetime_budget": 200,
+ "name": "Real ad set",
+ "start_time": "2015-04-12T09:19:00+0200",
+ "targeting": {
+ "age_max": 20,
+ "age_min": 18,
+ "behaviors": [
+ {
+ "id": "6004854404172",
+ "name": "Technology late adopters"
+ }
+ ],
+ "genders": [
+ 1
+ ],
+ "geo_locations": {
+ "countries": [
+ "PL"
+ ],
+ "location_types": [
+ "home",
+ "recent"
+ ]
+ },
+ "interests": [
+ {
+ "id": "6003629266583",
+ "name": "Hard drives"
+ }
+ ],
+ "page_types": [
+ "feed"
+ ]
+ },
+ "updated_time": "2015-04-10T13:32:09+0200"
+ }
+ ],
+ "paging": {
+ "cursors": {
+ "before": "NjAyNDE2Nzg5NTY1MA==",
+ "after": "NjAyNTIwMDQ1MTQ1MA=="
+ }
+ }
+}
\ No newline at end of file
diff --git a/spring-social-facebook/src/test/resources/org/springframework/social/facebook/api/ads/ad-wrong-bid-type.json b/spring-social-facebook/src/test/resources/org/springframework/social/facebook/api/ads/ad-wrong-bid-type.json
new file mode 100644
index 000000000..861789a50
--- /dev/null
+++ b/spring-social-facebook/src/test/resources/org/springframework/social/facebook/api/ads/ad-wrong-bid-type.json
@@ -0,0 +1,45 @@
+{
+ "id": "100123456789",
+ "account_id": "123456789",
+ "adgroup_status": "ACTIVE",
+ "bid_type": "WRONG_BID_TYPE",
+ "campaign_id": "800123456789",
+ "campaign_group_id": "700123456789",
+ "created_time": "2015-04-10T09:28:54+0200",
+ "creative": {
+ "id": "900123456789"
+ },
+ "name": "Test ad name",
+ "targeting": {
+ "age_max": 20,
+ "age_min": 18,
+ "behaviors": [
+ {
+ "id": "6004854404172",
+ "name": "Technology late adopters"
+ }
+ ],
+ "genders": [
+ 1
+ ],
+ "geo_locations": {
+ "countries": [
+ "PL"
+ ],
+ "location_types": [
+ "home",
+ "recent"
+ ]
+ },
+ "interests": [
+ {
+ "id": "6003629266583",
+ "name": "Hard drives"
+ }
+ ],
+ "page_types": [
+ "feed"
+ ]
+ },
+ "updated_time": "2015-04-10T13:32:09+0200"
+}
\ No newline at end of file
diff --git a/spring-social-facebook/src/test/resources/org/springframework/social/facebook/api/ads/ad-wrong-status.json b/spring-social-facebook/src/test/resources/org/springframework/social/facebook/api/ads/ad-wrong-status.json
new file mode 100644
index 000000000..e71a4d52b
--- /dev/null
+++ b/spring-social-facebook/src/test/resources/org/springframework/social/facebook/api/ads/ad-wrong-status.json
@@ -0,0 +1,45 @@
+{
+ "id": "100123456789",
+ "account_id": "123456789",
+ "adgroup_status": "WRONG_STATUS",
+ "bid_type": "ABSOLUTE_OCPM",
+ "campaign_id": "800123456789",
+ "campaign_group_id": "700123456789",
+ "created_time": "2015-04-10T09:28:54+0200",
+ "creative": {
+ "id": "900123456789"
+ },
+ "name": "Test ad name",
+ "targeting": {
+ "age_max": 20,
+ "age_min": 18,
+ "behaviors": [
+ {
+ "id": "6004854404172",
+ "name": "Technology late adopters"
+ }
+ ],
+ "genders": [
+ 1
+ ],
+ "geo_locations": {
+ "countries": [
+ "PL"
+ ],
+ "location_types": [
+ "home",
+ "recent"
+ ]
+ },
+ "interests": [
+ {
+ "id": "6003629266583",
+ "name": "Hard drives"
+ }
+ ],
+ "page_types": [
+ "feed"
+ ]
+ },
+ "updated_time": "2015-04-10T13:32:09+0200"
+}
\ No newline at end of file
diff --git a/spring-social-facebook/src/test/resources/org/springframework/social/facebook/api/ads/ad.json b/spring-social-facebook/src/test/resources/org/springframework/social/facebook/api/ads/ad.json
new file mode 100644
index 000000000..8842790a9
--- /dev/null
+++ b/spring-social-facebook/src/test/resources/org/springframework/social/facebook/api/ads/ad.json
@@ -0,0 +1,45 @@
+{
+ "id": "100123456789",
+ "account_id": "123456789",
+ "adgroup_status": "ACTIVE",
+ "bid_type": "ABSOLUTE_OCPM",
+ "campaign_id": "800123456789",
+ "campaign_group_id": "700123456789",
+ "created_time": "2015-04-10T09:28:54+0200",
+ "creative": {
+ "id": "900123456789"
+ },
+ "name": "Test ad name",
+ "targeting": {
+ "age_max": 20,
+ "age_min": 18,
+ "behaviors": [
+ {
+ "id": "6004854404172",
+ "name": "Technology late adopters"
+ }
+ ],
+ "genders": [
+ 1
+ ],
+ "geo_locations": {
+ "countries": [
+ "PL"
+ ],
+ "location_types": [
+ "home",
+ "recent"
+ ]
+ },
+ "interests": [
+ {
+ "id": "6003629266583",
+ "name": "Hard drives"
+ }
+ ],
+ "page_types": [
+ "feed"
+ ]
+ },
+ "updated_time": "2015-04-10T13:32:09+0200"
+}
\ No newline at end of file
diff --git a/spring-social-facebook/src/test/resources/org/springframework/social/facebook/api/ads/error-invalid-update-campaign-status.json b/spring-social-facebook/src/test/resources/org/springframework/social/facebook/api/ads/error-invalid-update-campaign-status.json
new file mode 100644
index 000000000..df66933d9
--- /dev/null
+++ b/spring-social-facebook/src/test/resources/org/springframework/social/facebook/api/ads/error-invalid-update-campaign-status.json
@@ -0,0 +1,11 @@
+{
+ "error": {
+ "message": "Invalid parameter",
+ "type": "FacebookApiException",
+ "code": 100,
+ "error_subcode": 1487564,
+ "is_transient": false,
+ "error_user_title": "Update Campaign Status",
+ "error_user_msg": "New campaigns need to be either active or paused."
+ }
+}
\ No newline at end of file
diff --git a/spring-social-facebook/src/test/resources/org/springframework/social/facebook/api/error-100-invalidParameter.json b/spring-social-facebook/src/test/resources/org/springframework/social/facebook/api/error-100-invalidParameter.json
new file mode 100644
index 000000000..86480ae33
--- /dev/null
+++ b/spring-social-facebook/src/test/resources/org/springframework/social/facebook/api/error-100-invalidParameter.json
@@ -0,0 +1,11 @@
+{
+ "error": {
+ "message": "Invalid parameter",
+ "type": "FacebookApiException",
+ "code": 100,
+ "error_subcode": 1349125,
+ "is_transient": false,
+ "error_user_title": "Missing Message Or Attachment",
+ "error_user_msg": "Missing message or attachment."
+ }
+}
\ No newline at end of file