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(Tiers.class, new RecurlyObjectsSerializer<Tiers,Tier>(Tiers.class, "tier")); 101 m.addSerializer(Transactions.class, new RecurlyObjectsSerializer<Transactions, Transaction>(Transactions.class, "transaction")); 102 m.addSerializer(Usages.class, new RecurlyObjectsSerializer<Usages, Usage>(Usages.class, "usage")); 103 xmlMapper.registerModule(m); 104 105 return xmlMapper; 106 } 107 108 public static XmlMapper sharedXmlMapper() { 109 return XmlMapperHolder.xmlMapper; 110 } 111 112 public static Boolean booleanOrNull(@Nullable final Object object) { 113 if (isNull(object)) { 114 return null; 115 } 116 117 // Booleans are represented as objects (e.g. <display_quantity type="boolean">false</display_quantity>), which Jackson 118 // will interpret as an Object (Map), not Booleans. 119 if (object instanceof Map) { 120 final Map map = (Map) object; 121 if (map.keySet().size() == 2 && "boolean".equalsIgnoreCase((String) map.get("type"))) { 122 return Boolean.valueOf((String) map.get("")); 123 } 124 } 125 126 return Boolean.valueOf(object.toString()); 127 } 128 129 public static String stringOrNull(@Nullable final Object object) { 130 if (isNull(object)) { 131 return null; 132 } 133 134 return object.toString().trim(); 135 } 136 137 @SuppressWarnings("unchecked") 138 public static <E extends Enum<E>> E enumOrNull(Class<E> enumClass, @Nullable final Object object, final Boolean upCase) { 139 if (isNull(object)) { 140 return null; 141 } else if (enumClass.isAssignableFrom(object.getClass())) { 142 return (E) object; 143 } 144 145 String value = object.toString().trim(); 146 147 if (upCase) { 148 value = value.toUpperCase(); 149 } 150 151 return (E) Enum.valueOf(enumClass, value); 152 } 153 154 @SuppressWarnings("unchecked") 155 public static <E extends Enum<E>> E enumOrNull(Class<E> enumClass, @Nullable final Object object) { 156 return enumOrNull(enumClass, object, false); 157 } 158 159 public static Integer integerOrNull(@Nullable final Object object) { 160 if (isNull(object)) { 161 return null; 162 } 163 164 // Integers are represented as objects (e.g. <year type="integer">2015</year>), which Jackson 165 // will interpret as an Object (Map), not Integers. 166 if (object instanceof Map) { 167 final Map map = (Map) object; 168 if (map.keySet().size() == 2 && "integer".equalsIgnoreCase((String) map.get("type"))) { 169 return Integer.valueOf((String) map.get("")); 170 } 171 } 172 173 return Integer.valueOf(object.toString()); 174 } 175 176 public static Long longOrNull(@Nullable final Object object) { 177 if (isNull(object)) { 178 return null; 179 } 180 181 // Ids are represented as objects (e.g. <id type="integer">1988596967980562362</id>), which Jackson 182 // will interpret as an Object (Map), not Longs. 183 if (object instanceof Map) { 184 final Map map = (Map) object; 185 if (map.keySet().size() == 2 && "integer".equalsIgnoreCase((String) map.get("type"))) { 186 return Long.valueOf((String) map.get("")); 187 } 188 } 189 190 return Long.valueOf(object.toString()); 191 } 192 193 public static BigDecimal bigDecimalOrNull(@Nullable final Object object) { 194 if (isNull(object)) { 195 return null; 196 } 197 198 // BigDecimals are represented as objects (e.g. <tax_rate type="float">0.0875</tax_rate>), which Jackson 199 // will interpret as an Object (Map), not Longs. 200 if (object instanceof Map) { 201 final Map map = (Map) object; 202 if (map.keySet().size() == 2 && "float".equalsIgnoreCase((String) map.get("type"))) { 203 return new BigDecimal((String) map.get("")); 204 } 205 } 206 207 return new BigDecimal(object.toString()); 208 } 209 210 public static DateTime dateTimeOrNull(@Nullable final Object object) { 211 if (isNull(object)) { 212 return null; 213 } 214 215 // DateTimes are represented as objects (e.g. <created_at type="dateTime">2011-04-19T07:00:00Z</created_at>), which Jackson 216 // will interpret as an Object (Map), not DateTimes. 217 if (object instanceof Map) { 218 final Map map = (Map) object; 219 if (map.keySet().size() == 2 && "dateTime".equalsIgnoreCase((String) map.get("type"))) { 220 return new DateTime(map.get("")); 221 } 222 } 223 224 return new DateTime(object.toString()); 225 } 226 227 public static boolean isNull(@Nullable final Object object) { 228 if (object == null) { 229 return true; 230 } 231 232 // Hack to work around Recurly output for nil values: the response will contain 233 // an element with a nil attribute (e.g. <city nil="nil"></city> or <username nil="true"></username>) which Jackson will 234 // interpret as an Object (Map), not a String. 235 if (object instanceof Map) { 236 final Map map = (Map) object; 237 if (map.keySet().size() >= 1 && map.get(NIL_STR) != null && NIL_VAL.contains(map.get(NIL_STR).toString())) { 238 return true; 239 } 240 } 241 242 return false; 243 } 244 245 <T extends RecurlyObject> T fetch(final T object, final Class<T> clazz) { 246 if (object.getHref() == null || recurlyClient == null) { 247 return object; 248 } 249 return recurlyClient.doGETWithFullURL(clazz, object.getHref()); 250 } 251 252 public void setRecurlyClient(final RecurlyClient recurlyClient) { 253 this.recurlyClient = recurlyClient; 254 } 255 256 @Override 257 public boolean equals(final Object o) { 258 if (this == o) return true; 259 if (o == null || getClass() != o.getClass()) return false; 260 261 return this.hashCode() == o.hashCode(); 262 } 263 264 /** 265 * Holder for the shared {@link XmlMapper}. Not putting it directly under {@link RecurlyObject} 266 * to maker it (sort of) lazy. 267 */ 268 private static class XmlMapperHolder { 269 270 private static final XmlMapper xmlMapper = newXmlMapper(); 271 272 } 273 274}