001/* 002 * Copyright 2010-2014 Ning, Inc. 003 * Copyright 2014-2015 The Billing Project, LLC 004 * 005 * The Billing Project licenses this file to you under the Apache License, version 2.0 006 * (the "License"); you may not use this file except in compliance with the 007 * License. You may obtain a copy of the License at: 008 * 009 * http://www.apache.org/licenses/LICENSE-2.0 010 * 011 * Unless required by applicable law or agreed to in writing, software 012 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 013 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 014 * License for the specific language governing permissions and limitations 015 * under the License. 016 */ 017 018package com.ning.billing.recurly.model; 019 020import com.fasterxml.jackson.annotation.JsonIgnore; 021import com.fasterxml.jackson.annotation.JsonProperty; 022import com.fasterxml.jackson.annotation.JsonIgnoreProperties; 023import com.fasterxml.jackson.annotation.JsonInclude; 024import com.fasterxml.jackson.core.Version; 025import com.fasterxml.jackson.databind.AnnotationIntrospector; 026import com.fasterxml.jackson.databind.SerializationFeature; 027import com.fasterxml.jackson.databind.introspect.AnnotationIntrospectorPair; 028import com.fasterxml.jackson.databind.introspect.JacksonAnnotationIntrospector; 029import com.fasterxml.jackson.databind.module.SimpleModule; 030import com.fasterxml.jackson.databind.type.TypeFactory; 031import com.fasterxml.jackson.dataformat.xml.XmlMapper; 032import com.fasterxml.jackson.datatype.joda.JodaModule; 033import com.fasterxml.jackson.module.jaxb.JaxbAnnotationIntrospector; 034import com.fasterxml.jackson.module.jaxb.JaxbAnnotationModule; 035import com.ning.billing.recurly.RecurlyClient; 036import com.ning.billing.recurly.model.jackson.RecurlyObjectsSerializer; 037import com.ning.billing.recurly.model.jackson.RecurlyXmlSerializerProvider; 038import org.joda.time.DateTime; 039 040import javax.annotation.Nullable; 041import javax.xml.bind.annotation.XmlTransient; 042import javax.xml.stream.XMLInputFactory; 043import java.math.BigDecimal; 044import java.util.Arrays; 045import java.util.List; 046import java.util.Map; 047 048@JsonIgnoreProperties(ignoreUnknown = true) 049public abstract class RecurlyObject { 050 051 @XmlTransient 052 private RecurlyClient recurlyClient; 053 054 @XmlTransient 055 protected String href; 056 057 public static final String NIL_STR = "nil"; 058 public static final List<String> NIL_VAL = Arrays.asList("nil", "true"); 059 060 // See https://github.com/killbilling/recurly-java-library/issues/4 for why 061 // @JsonIgnore is required here and @XmlTransient is not enough 062 @JsonIgnore 063 public String getHref() { 064 return href; 065 } 066 067 @JsonProperty 068 public void setHref(final Object href) { 069 this.href = stringOrNull(href); 070 } 071 072 public static XmlMapper newXmlMapper() { 073 final XMLInputFactory xmlInputFactory = XMLInputFactory.newFactory(); 074 xmlInputFactory.setProperty(XMLInputFactory.IS_SUPPORTING_EXTERNAL_ENTITIES, Boolean.FALSE); 075 xmlInputFactory.setProperty(XMLInputFactory.SUPPORT_DTD, Boolean.FALSE); 076 final XmlMapper xmlMapper = new XmlMapper(xmlInputFactory); 077 xmlMapper.setSerializerProvider(new RecurlyXmlSerializerProvider()); 078 final AnnotationIntrospector primary = new JacksonAnnotationIntrospector(); 079 final AnnotationIntrospector secondary = new JaxbAnnotationIntrospector(TypeFactory.defaultInstance()); 080 final AnnotationIntrospector pair = new AnnotationIntrospectorPair(primary, secondary); 081 xmlMapper.setAnnotationIntrospector(pair); 082 xmlMapper.registerModule(new JodaModule()); 083 xmlMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS); 084 xmlMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL); 085 xmlMapper.registerModule(new JaxbAnnotationModule()); 086 087 final SimpleModule m = new SimpleModule("module", new Version(1, 0, 0, null, null, null)); 088 m.addSerializer(Accounts.class, new RecurlyObjectsSerializer<Accounts, Account>(Accounts.class, "account")); 089 m.addSerializer(AddOns.class, new RecurlyObjectsSerializer<AddOns, AddOn>(AddOns.class, "add_on")); 090 m.addSerializer(Adjustments.class, new RecurlyObjectsSerializer<Adjustments, Adjustment>(Adjustments.class, "adjustment")); 091 m.addSerializer(Coupons.class, new RecurlyObjectsSerializer<Coupons, Coupon>(Coupons.class, "coupon")); 092 m.addSerializer(CustomFields.class, new RecurlyObjectsSerializer<CustomFields, CustomField>(CustomFields.class, "custom_field")); 093 m.addSerializer(Invoices.class, new RecurlyObjectsSerializer<Invoices, Invoice>(Invoices.class, "invoice")); 094 m.addSerializer(Plans.class, new RecurlyObjectsSerializer<Plans, Plan>(Plans.class, "plan")); 095 m.addSerializer(RecurlyErrors.class, new RecurlyObjectsSerializer<RecurlyErrors, RecurlyError>(RecurlyErrors.class, "error")); 096 m.addSerializer(ShippingAddresses.class, new RecurlyObjectsSerializer<ShippingAddresses, ShippingAddress>(ShippingAddresses.class, "shipping_address")); 097 m.addSerializer(ShippingFees.class, new RecurlyObjectsSerializer<ShippingFees, ShippingFee>(ShippingFees.class, "shipping_fee")); 098 m.addSerializer(SubscriptionAddOns.class, new RecurlyObjectsSerializer<SubscriptionAddOns, SubscriptionAddOn>(SubscriptionAddOns.class, "subscription_add_on")); 099 m.addSerializer(Subscriptions.class, new RecurlyObjectsSerializer<Subscriptions, Subscription>(Subscriptions.class, "subscription")); 100 m.addSerializer(Transactions.class, new RecurlyObjectsSerializer<Transactions, Transaction>(Transactions.class, "transaction")); 101 m.addSerializer(Usages.class, new RecurlyObjectsSerializer<Usages, Usage>(Usages.class, "usage")); 102 xmlMapper.registerModule(m); 103 104 return xmlMapper; 105 } 106 107 public static Boolean booleanOrNull(@Nullable final Object object) { 108 if (isNull(object)) { 109 return null; 110 } 111 112 // Booleans are represented as objects (e.g. <display_quantity type="boolean">false</display_quantity>), which Jackson 113 // will interpret as an Object (Map), not Booleans. 114 if (object instanceof Map) { 115 final Map map = (Map) object; 116 if (map.keySet().size() == 2 && "boolean".equalsIgnoreCase((String) map.get("type"))) { 117 return Boolean.valueOf((String) map.get("")); 118 } 119 } 120 121 return Boolean.valueOf(object.toString()); 122 } 123 124 public static String stringOrNull(@Nullable final Object object) { 125 if (isNull(object)) { 126 return null; 127 } 128 129 return object.toString().trim(); 130 } 131 132 @SuppressWarnings("unchecked") 133 public static <E extends Enum<E>> E enumOrNull(Class<E> enumClass, @Nullable final Object object, final Boolean upCase) { 134 if (isNull(object)) { 135 return null; 136 } else if (enumClass.isAssignableFrom(object.getClass())) { 137 return (E) object; 138 } 139 140 String value = object.toString().trim(); 141 142 if (upCase) { 143 value = value.toUpperCase(); 144 } 145 146 return (E) Enum.valueOf(enumClass, value); 147 } 148 149 @SuppressWarnings("unchecked") 150 public static <E extends Enum<E>> E enumOrNull(Class<E> enumClass, @Nullable final Object object) { 151 return enumOrNull(enumClass, object, false); 152 } 153 154 public static Integer integerOrNull(@Nullable final Object object) { 155 if (isNull(object)) { 156 return null; 157 } 158 159 // Integers are represented as objects (e.g. <year type="integer">2015</year>), which Jackson 160 // will interpret as an Object (Map), not Integers. 161 if (object instanceof Map) { 162 final Map map = (Map) object; 163 if (map.keySet().size() == 2 && "integer".equalsIgnoreCase((String) map.get("type"))) { 164 return Integer.valueOf((String) map.get("")); 165 } 166 } 167 168 return Integer.valueOf(object.toString()); 169 } 170 171 public static Long longOrNull(@Nullable final Object object) { 172 if (isNull(object)) { 173 return null; 174 } 175 176 // Ids are represented as objects (e.g. <id type="integer">1988596967980562362</id>), which Jackson 177 // will interpret as an Object (Map), not Longs. 178 if (object instanceof Map) { 179 final Map map = (Map) object; 180 if (map.keySet().size() == 2 && "integer".equalsIgnoreCase((String) map.get("type"))) { 181 return Long.valueOf((String) map.get("")); 182 } 183 } 184 185 return Long.valueOf(object.toString()); 186 } 187 188 public static BigDecimal bigDecimalOrNull(@Nullable final Object object) { 189 if (isNull(object)) { 190 return null; 191 } 192 193 // BigDecimals are represented as objects (e.g. <tax_rate type="float">0.0875</tax_rate>), which Jackson 194 // will interpret as an Object (Map), not Longs. 195 if (object instanceof Map) { 196 final Map map = (Map) object; 197 if (map.keySet().size() == 2 && "float".equalsIgnoreCase((String) map.get("type"))) { 198 return new BigDecimal((String) map.get("")); 199 } 200 } 201 202 return new BigDecimal(object.toString()); 203 } 204 205 public static DateTime dateTimeOrNull(@Nullable final Object object) { 206 if (isNull(object)) { 207 return null; 208 } 209 210 // DateTimes are represented as objects (e.g. <created_at type="dateTime">2011-04-19T07:00:00Z</created_at>), which Jackson 211 // will interpret as an Object (Map), not DateTimes. 212 if (object instanceof Map) { 213 final Map map = (Map) object; 214 if (map.keySet().size() == 2 && "dateTime".equalsIgnoreCase((String) map.get("type"))) { 215 return new DateTime(map.get("")); 216 } 217 } 218 219 return new DateTime(object.toString()); 220 } 221 222 public static boolean isNull(@Nullable final Object object) { 223 if (object == null) { 224 return true; 225 } 226 227 // Hack to work around Recurly output for nil values: the response will contain 228 // an element with a nil attribute (e.g. <city nil="nil"></city> or <username nil="true"></username>) which Jackson will 229 // interpret as an Object (Map), not a String. 230 if (object instanceof Map) { 231 final Map map = (Map) object; 232 if (map.keySet().size() >= 1 && map.get(NIL_STR) != null && NIL_VAL.contains(map.get(NIL_STR).toString())) { 233 return true; 234 } 235 } 236 237 return false; 238 } 239 240 <T extends RecurlyObject> T fetch(final T object, final Class<T> clazz) { 241 if (object.getHref() == null || recurlyClient == null) { 242 return object; 243 } 244 return recurlyClient.doGETWithFullURL(clazz, object.getHref()); 245 } 246 247 public void setRecurlyClient(final RecurlyClient recurlyClient) { 248 this.recurlyClient = recurlyClient; 249 } 250 251 @Override 252 public boolean equals(final Object o) { 253 if (this == o) return true; 254 if (o == null || getClass() != o.getClass()) return false; 255 256 return this.hashCode() == o.hashCode(); 257 } 258}