001/*
002 * Copyright (c) 2011-2017 Nexmo Inc
003 *
004 * Permission is hereby granted, free of charge, to any person obtaining a copy
005 * of this software and associated documentation files (the "Software"), to deal
006 * in the Software without restriction, including without limitation the rights
007 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
008 * copies of the Software, and to permit persons to whom the Software is
009 * furnished to do so, subject to the following conditions:
010 *
011 * The above copyright notice and this permission notice shall be included in
012 * all copies or substantial portions of the Software.
013 *
014 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
015 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
016 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
017 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
018 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
019 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
020 * THE SOFTWARE.
021 */
022package com.nexmo.client.verify.endpoints;
023
024import com.nexmo.client.HttpWrapper;
025import com.nexmo.client.NexmoClientException;
026import com.nexmo.client.NexmoResponseParseException;
027import com.nexmo.client.auth.SignatureAuthMethod;
028import com.nexmo.client.auth.TokenAuthMethod;
029import com.nexmo.client.legacyutils.XmlParser;
030import com.nexmo.client.legacyutils.XmlUtil;
031import com.nexmo.client.verify.BaseResult;
032import com.nexmo.client.verify.SearchRequest;
033import com.nexmo.client.verify.SearchResult;
034import com.nexmo.client.verify.VerifyResult;
035import com.nexmo.client.voice.endpoints.AbstractMethod;
036import org.apache.commons.logging.Log;
037import org.apache.commons.logging.LogFactory;
038import org.apache.http.HttpResponse;
039import org.apache.http.client.methods.RequestBuilder;
040import org.apache.http.impl.client.BasicResponseHandler;
041import org.w3c.dom.Document;
042import org.w3c.dom.Element;
043import org.w3c.dom.Node;
044import org.w3c.dom.NodeList;
045
046import java.io.IOException;
047import java.io.UnsupportedEncodingException;
048import java.text.ParseException;
049import java.text.SimpleDateFormat;
050import java.util.ArrayList;
051import java.util.Date;
052import java.util.List;
053
054public class SearchEndpoint extends AbstractMethod<SearchRequest, SearchResult[]> {
055    private static final Log log = LogFactory.getLog(SearchEndpoint.class);
056
057    private static final Class[] ALLOWED_AUTH_METHODS = new Class[]{SignatureAuthMethod.class, TokenAuthMethod.class};
058
059    private static final String DEFAULT_URI = "https://api.nexmo.com/verify/search/xml";
060    
061    private XmlParser xmlParser = new XmlParser();
062
063    private String uri = DEFAULT_URI;
064
065    private static final ThreadLocal<SimpleDateFormat> sDateTimePattern = new ThreadLocal<SimpleDateFormat>() {
066        @Override
067        protected SimpleDateFormat initialValue() {
068            SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
069            sdf.setLenient(false);
070
071            return sdf;
072        }
073    };
074
075    /**
076     * Create a new SearchEndpoint.
077     * <p>
078     * This client is used for calling the verify API's search endpoint.
079     */
080    public SearchEndpoint(HttpWrapper httpWrapper) {
081        super(httpWrapper);
082    }
083
084    @Override
085    protected Class[] getAcceptableAuthMethods() {
086        return ALLOWED_AUTH_METHODS;
087    }
088
089    @Override
090    public RequestBuilder makeRequest(SearchRequest request) throws NexmoClientException, UnsupportedEncodingException {
091        RequestBuilder result = RequestBuilder.post(this.uri);
092        if (request.getRequestIds().length == 1) {
093            result.addParameter("request_id", request.getRequestIds()[0]);
094        } else {
095            for (String requestId : request.getRequestIds())
096                result.addParameter("request_ids", requestId);
097        }
098        return result;
099    }
100
101    @Override
102    public SearchResult[] parseResponse(HttpResponse response) throws IOException {
103        String json = new BasicResponseHandler().handleResponse(response);
104        return parseSearchResponse(json);
105    }
106
107    public SearchResult search(String requestId) throws IOException, NexmoClientException {
108        SearchResult[] result = search(new String[]{requestId});
109        return result != null && result.length > 0 ? result[0] : null;
110    }
111
112    public SearchResult[] search(String... requestIds) throws IOException, NexmoClientException {
113        return this.execute(new SearchRequest(requestIds));
114    }
115
116    protected SearchResult[] parseSearchResponse(String response) throws NexmoResponseParseException {
117        Document doc = xmlParser.parseXml(response);
118
119        Element root = doc.getDocumentElement();
120        if ("verify_response".equals(root.getNodeName())) {
121            // error response
122            VerifyResult result = SharedParsers.parseVerifyResponseXmlNode(root);
123            return new SearchResult[]{
124                    new SearchResult(result.getStatus(),
125                            result.getRequestId(),
126                            null,
127                            null,
128                            null,
129                            0, null,
130                            null,
131                            null, null,
132                            null, null,
133                            null,
134                            result.getErrorText(),
135                            result.isTemporaryError())
136            };
137        } else if (("verify_request").equals(root.getNodeName())) {
138            return new SearchResult[]{parseVerifyRequestXmlNode(root)};
139        } else if ("verification_requests".equals(root.getNodeName())) {
140            List<SearchResult> results = new ArrayList<>();
141
142            NodeList fields = root.getChildNodes();
143            for (int i = 0; i < fields.getLength(); i++) {
144                Node node = fields.item(i);
145                if (node.getNodeType() != Node.ELEMENT_NODE)
146                    continue;
147
148                if ("verify_request".equals(node.getNodeName()))
149                    results.add(parseVerifyRequestXmlNode((Element) node));
150            }
151
152            return results.toArray(new SearchResult[results.size()]);
153        } else {
154            throw new NexmoResponseParseException("No valid response found [ " + response + "] ");
155        }
156    }
157
158    protected static SearchResult parseVerifyRequestXmlNode(Element root) throws NexmoResponseParseException {
159        String requestId = null;
160        String accountId = null;
161        String number = null;
162        String senderId = null;
163        Date dateSubmitted = null;
164        Date dateFinalized = null;
165        Date firstEventDate = null;
166        Date lastEventDate = null;
167        float price = -1;
168        String currency = null;
169        SearchResult.VerificationStatus status = null;
170        List<SearchResult.VerifyCheck> checks = new ArrayList<>();
171        String errorText = null;
172
173        NodeList fields = root.getChildNodes();
174        for (int i = 0; i < fields.getLength(); i++) {
175            Node node = fields.item(i);
176            if (node.getNodeType() != Node.ELEMENT_NODE)
177                continue;
178
179            String name = node.getNodeName();
180            if ("request_id".equals(name)) {
181                requestId = XmlUtil.stringValue(node);
182            } else if ("account_id".equals(name)) {
183                accountId = XmlUtil.stringValue(node);
184            } else if ("status".equals(name)) {
185                String str = XmlUtil.stringValue(node);
186                if (str != null) {
187                    try {
188                        status = SearchResult.VerificationStatus.valueOf(str.replace(' ', '_'));
189                    } catch (IllegalArgumentException e) {
190                        log.error("xml parser .. invalid value in <status> node [ " + str + " ] ");
191                    }
192                }
193            } else if ("number".equals(name)) {
194                number = XmlUtil.stringValue(node);
195            } else if ("price".equals(name)) {
196                String str = XmlUtil.stringValue(node);
197                try {
198                    if (str != null)
199                        price = Float.parseFloat(str);
200                } catch (NumberFormatException e) {
201                    log.error("xml parser .. invalid value in <price> node [ " + str + " ] ");
202                }
203            } else if ("currency".equals(name)) {
204                currency = XmlUtil.stringValue(node);
205            } else if ("sender_id".equals(name)) {
206                senderId = XmlUtil.stringValue(node);
207            } else if ("date_submitted".equals(name)) {
208                String str = XmlUtil.stringValue(node);
209                if (str != null) {
210                    try {
211                        dateSubmitted = parseDateTime(str);
212                    } catch (ParseException e) {
213                        log.error("xml parser .. invalid value in <date_submitted> node [ " + str + " ] ");
214                    }
215                }
216            } else if ("date_finalized".equals(name)) {
217                String str = XmlUtil.stringValue(node);
218                if (str != null) {
219                    try {
220                        dateFinalized = parseDateTime(str);
221                    } catch (ParseException e) {
222                        log.error("xml parser .. invalid value in <date_finalized> node [ " + str + " ] ");
223                    }
224                }
225            } else if ("first_event_date".equals(name)) {
226                String str = XmlUtil.stringValue(node);
227                if (str != null) {
228                    try {
229                        firstEventDate = parseDateTime(str);
230                    } catch (ParseException e) {
231                        log.error("xml parser .. invalid value in <first_event_date> node [ " + str + " ] ");
232                    }
233                }
234            } else if ("last_event_date".equals(name)) {
235                String str = XmlUtil.stringValue(node);
236                if (str != null) {
237                    try {
238                        lastEventDate = parseDateTime(str);
239                    } catch (ParseException e) {
240                        log.error("xml parser .. invalid value in <last_event_date> node [ " + str + " ] ");
241                    }
242                }
243            } else if ("checks".equals(name)) {
244                NodeList checkNodes = node.getChildNodes();
245                for (int j = 0; j < checkNodes.getLength(); j++) {
246                    Node checkNode = checkNodes.item(j);
247                    if (checkNode.getNodeType() != Node.ELEMENT_NODE)
248                        continue;
249
250                    if ("check".equals(checkNode.getNodeName()))
251                        checks.add(parseCheckXmlNode((Element) checkNode));
252                }
253            } else if ("error_text".equals(name)) {
254                errorText = XmlUtil.stringValue(node);
255            }
256        }
257
258        if (status == null)
259            throw new NexmoResponseParseException("Xml Parser - did not find a <status> node");
260
261        return new SearchResult(BaseResult.STATUS_OK,
262                requestId,
263                accountId,
264                status,
265                number,
266                price, currency,
267                senderId,
268                dateSubmitted, dateFinalized,
269                firstEventDate, lastEventDate,
270                checks,
271                errorText, false);
272    }
273
274    protected static SearchResult.VerifyCheck parseCheckXmlNode(Element root) throws NexmoResponseParseException {
275        String code = null;
276        SearchResult.VerifyCheck.Status status = null;
277        Date dateReceived = null;
278        String ipAddress = null;
279
280        NodeList fields = root.getChildNodes();
281        for (int i = 0; i < fields.getLength(); i++) {
282            Node node = fields.item(i);
283            if (node.getNodeType() != Node.ELEMENT_NODE)
284                continue;
285
286            String name = node.getNodeName();
287            if ("code".equals(name)) {
288                code = XmlUtil.stringValue(node);
289            } else if ("status".equals(name)) {
290                String str = XmlUtil.stringValue(node);
291                if (str != null) {
292                    try {
293                        status = SearchResult.VerifyCheck.Status.valueOf(str);
294                    } catch (IllegalArgumentException e) {
295                        log.error("xml parser .. invalid value in <status> node [ " + str + " ] ");
296                    }
297                }
298            } else if ("ip_address".equals(name)) {
299                ipAddress = XmlUtil.stringValue(node);
300            } else if ("date_received".equals(name)) {
301                String str = XmlUtil.stringValue(node);
302                if (str != null) {
303                    try {
304                        dateReceived = parseDateTime(str);
305                    } catch (ParseException e) {
306                        log.error("xml parser .. invalid value in <date_received> node [ " + str + " ] ");
307                    }
308                }
309            }
310        }
311
312        if (status == null)
313            throw new NexmoResponseParseException("Xml Parser - did not find a <status> node");
314
315        return new SearchResult.VerifyCheck(dateReceived, code, status, ipAddress);
316    }
317
318    private static Date parseDateTime(String str) throws ParseException {
319        return sDateTimePattern.get().parse(str);
320    }
321
322}