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}