001/*
002 * Copyright 2010-2013 Ning, Inc.
003 *
004 * Ning licenses this file to you under the Apache License, version 2.0
005 * (the "License"); you may not use this file except in compliance with the
006 * License.  You may obtain a copy of the License at:
007 *
008 *    http://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
012 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
013 * License for the specific language governing permissions and limitations
014 * under the License.
015 */
016
017package com.ning.billing.recurly.model;
018
019import java.util.Arrays;
020import java.util.List;
021import java.util.Map;
022
023import javax.annotation.Nullable;
024import javax.xml.bind.annotation.XmlTransient;
025
026import org.joda.time.DateTime;
027
028import com.ning.billing.recurly.RecurlyClient;
029import com.ning.billing.recurly.model.jackson.RecurlyObjectsSerializer;
030
031import com.fasterxml.jackson.annotation.JsonIgnore;
032import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
033import com.fasterxml.jackson.core.Version;
034import com.fasterxml.jackson.databind.AnnotationIntrospector;
035import com.fasterxml.jackson.databind.SerializationFeature;
036import com.fasterxml.jackson.databind.introspect.AnnotationIntrospectorPair;
037import com.fasterxml.jackson.databind.introspect.JacksonAnnotationIntrospector;
038import com.fasterxml.jackson.databind.module.SimpleModule;
039import com.fasterxml.jackson.databind.type.TypeFactory;
040import com.fasterxml.jackson.dataformat.xml.XmlMapper;
041import com.fasterxml.jackson.datatype.joda.JodaModule;
042import com.fasterxml.jackson.module.jaxb.JaxbAnnotationIntrospector;
043
044@JsonIgnoreProperties(ignoreUnknown = true)
045public abstract class RecurlyObject {
046
047    @XmlTransient
048    private RecurlyClient recurlyClient;
049
050    @XmlTransient
051    protected String href;
052
053    public static final String NIL_STR = "nil";
054    public static final List<String> NIL_VAL = Arrays.asList("nil", "true");
055
056    // See https://github.com/killbilling/recurly-java-library/issues/4 for why
057    // @JsonIgnore is required here and @XmlTransient is not enough
058    @JsonIgnore
059    public String getHref() {
060        return href;
061    }
062
063    public void setHref(final Object href) {
064        this.href = stringOrNull(href);
065    }
066
067    public static XmlMapper newXmlMapper() {
068        final XmlMapper xmlMapper = new XmlMapper();
069        final AnnotationIntrospector primary = new JacksonAnnotationIntrospector();
070        final AnnotationIntrospector secondary = new JaxbAnnotationIntrospector(TypeFactory.defaultInstance());
071        final AnnotationIntrospector pair = new AnnotationIntrospectorPair(primary, secondary);
072        xmlMapper.setAnnotationIntrospector(pair);
073        xmlMapper.registerModule(new JodaModule());
074        xmlMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
075
076        final SimpleModule m = new SimpleModule("module", new Version(1, 0, 0, null, null, null));
077        m.addSerializer(Accounts.class, new RecurlyObjectsSerializer<Accounts, Account>(Accounts.class, "account"));
078        m.addSerializer(AddOns.class, new RecurlyObjectsSerializer<AddOns, AddOn>(AddOns.class, "add_on"));
079        m.addSerializer(Adjustments.class, new RecurlyObjectsSerializer<Adjustments, Adjustment>(Adjustments.class, "adjustment"));
080        m.addSerializer(Coupons.class, new RecurlyObjectsSerializer<Coupons, Coupon>(Coupons.class, "coupon"));
081        m.addSerializer(Invoices.class, new RecurlyObjectsSerializer<Invoices, Invoice>(Invoices.class, "invoice"));
082        m.addSerializer(Plans.class, new RecurlyObjectsSerializer<Plans, Plan>(Plans.class, "plan"));
083        m.addSerializer(RecurlyErrors.class, new RecurlyObjectsSerializer<RecurlyErrors, RecurlyError>(RecurlyErrors.class, "error"));
084        m.addSerializer(SubscriptionAddOns.class, new RecurlyObjectsSerializer<SubscriptionAddOns, SubscriptionAddOn>(SubscriptionAddOns.class, "subscription_add_on"));
085        m.addSerializer(Subscriptions.class, new RecurlyObjectsSerializer<Subscriptions, Subscription>(Subscriptions.class, "subscription"));
086        m.addSerializer(Transactions.class, new RecurlyObjectsSerializer<Transactions, Transaction>(Transactions.class, "transaction"));
087        xmlMapper.registerModule(m);
088
089        return xmlMapper;
090    }
091
092    public static Boolean booleanOrNull(@Nullable final Object object) {
093        if (isNull(object)) {
094            return null;
095        }
096
097        // Booleans are represented as objects (e.g. <display_quantity type="boolean">false</display_quantity>), which Jackson
098        // will interpret as an Object (Map), not Booleans.
099        if (object instanceof Map) {
100            final Map map = (Map) object;
101            if (map.keySet().size() == 2 && "boolean".equals(map.get("type"))) {
102                return Boolean.valueOf((String) map.get(""));
103            }
104        }
105
106        return Boolean.valueOf(object.toString());
107    }
108
109    public static String stringOrNull(@Nullable final Object object) {
110        if (isNull(object)) {
111            return null;
112        }
113
114        return object.toString().trim();
115    }
116
117    public static Integer integerOrNull(@Nullable final Object object) {
118        if (isNull(object)) {
119            return null;
120        }
121
122        // Integers are represented as objects (e.g. <year type="integer">2015</year>), which Jackson
123        // will interpret as an Object (Map), not Integers.
124        if (object instanceof Map) {
125            final Map map = (Map) object;
126            if (map.keySet().size() == 2 && "integer".equals(map.get("type"))) {
127                return Integer.valueOf((String) map.get(""));
128            }
129        }
130
131        return Integer.valueOf(object.toString());
132    }
133
134    public static DateTime dateTimeOrNull(@Nullable final Object object) {
135        if (isNull(object)) {
136            return null;
137        }
138
139        // DateTimes are represented as objects (e.g. <created_at type="datetime">2011-04-19T07:00:00Z</created_at>), which Jackson
140        // will interpret as an Object (Map), not DateTimes.
141        if (object instanceof Map) {
142            final Map map = (Map) object;
143            if (map.keySet().size() == 2 && "datetime".equals(map.get("type"))) {
144                return new DateTime(map.get(""));
145            }
146        }
147
148        return new DateTime(object.toString());
149    }
150
151    public static boolean isNull(@Nullable final Object object) {
152        if (object == null) {
153            return true;
154        }
155
156        // Hack to work around Recurly output for nil values: the response will contain
157        // an element with a nil attribute (e.g. <city nil="nil"></city> or <username nil="true"></username>) which Jackson will
158        // interpret as an Object (Map), not a String.
159        if (object instanceof Map) {
160            final Map map = (Map) object;
161            if (map.keySet().size() >= 1 && map.get(NIL_STR) != null && NIL_VAL.contains(map.get(NIL_STR).toString())) {
162                return true;
163            }
164        }
165
166        return false;
167    }
168
169    <T extends RecurlyObject> T fetch(final T object, final Class<T> clazz) {
170        if (object.getHref() == null || recurlyClient == null) {
171            return object;
172        }
173        return recurlyClient.doGETWithFullURL(clazz, object.getHref());
174    }
175
176    public void setRecurlyClient(final RecurlyClient recurlyClient) {
177        this.recurlyClient = recurlyClient;
178    }
179}