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.sms;
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.sms.messages.Message;
031import com.nexmo.client.voice.endpoints.AbstractMethod;
032import org.apache.commons.logging.Log;
033import org.apache.commons.logging.LogFactory;
034import org.apache.http.HttpResponse;
035import org.apache.http.client.methods.RequestBuilder;
036import org.apache.http.impl.client.BasicResponseHandler;
037import org.w3c.dom.Document;
038import org.w3c.dom.Node;
039import org.w3c.dom.NodeList;
040
041import java.io.IOException;
042import java.io.UnsupportedEncodingException;
043import java.math.BigDecimal;
044import java.util.ArrayList;
045import java.util.List;
046
047public class SendMessageEndpoint extends AbstractMethod<Message, SmsSubmissionResult[]> {
048    private static final Log log = LogFactory.getLog(SmsClient.class);
049
050    private static final Class[] ALLOWED_AUTH_METHODS = new Class[]{SignatureAuthMethod.class, TokenAuthMethod.class};
051
052    private static final String DEFAULT_URI = "https://rest.nexmo.com/sms/xml";
053
054    private XmlParser xmlParser = new XmlParser();
055    private String uri = DEFAULT_URI;
056
057    public SendMessageEndpoint(HttpWrapper httpWrapper) {
058        super(httpWrapper);
059    }
060
061    @Override
062    protected Class[] getAcceptableAuthMethods() {
063        return ALLOWED_AUTH_METHODS;
064    }
065
066    @Override
067    public RequestBuilder makeRequest(Message message) throws NexmoClientException, UnsupportedEncodingException {
068        RequestBuilder request = RequestBuilder.post(uri);
069        message.addParams(request);
070        return request;
071    }
072
073    @Override
074    public SmsSubmissionResult[] parseResponse(HttpResponse response) throws IOException {
075        return parseResponse(new BasicResponseHandler().handleResponse(response));
076    }
077
078    protected SmsSubmissionResult[] parseResponse(String response) throws NexmoResponseParseException {
079        // parse the response doc ...
080
081        /*
082            We receive a response from the api that looks like this, parse the document
083            and turn it into an array of SmsSubmissionResult, one object per <message> node
084
085                <mt-submission-response>
086                    <messages count='x'>
087                        <message>
088                            <to>xxx</to>
089                            <messageId>xxx</messageId>
090                            <status>xx</status>
091                            <errorText>ff</errorText>
092                            <clientRef>xxx</clientRef>
093                            <remainingBalance>##.##</remainingBalance>
094                            <messagePrice>##.##</messagePrice>
095                            <reachability status='x' description='xxx' />
096                            <network>23410</network>
097                        </message>
098                    </messages>
099                </mt-submission-response>
100        */
101
102        List<SmsSubmissionResult> results = new ArrayList<>();
103
104        Document doc = xmlParser.parseXml(response);
105
106        NodeList replies = doc.getElementsByTagName("mt-submission-response");
107        for (int i = 0; i < replies.getLength(); i++) {
108            Node reply = replies.item(i);
109            NodeList messageLists = reply.getChildNodes();
110            for (int i2 = 0; i2 < messageLists.getLength(); i2++) {
111                Node messagesNode = messageLists.item(i2);
112                if (messagesNode.getNodeType() != Node.ELEMENT_NODE)
113                    continue;
114                if (messagesNode.getNodeName().equals("messages")) {
115                    NodeList messages = messagesNode.getChildNodes();
116                    for (int i3 = 0; i3 < messages.getLength(); i3++) {
117                        SmsSubmissionResult message = parseMessageXmlNode(messages.item(i3));
118                        if (message != null) {
119                            results.add(message);
120                        }
121                    }
122                }
123            }
124        }
125
126        return results.toArray(new SmsSubmissionResult[results.size()]);
127    }
128
129    private SmsSubmissionResult parseMessageXmlNode(Node messageNode)
130            throws NexmoResponseParseException {
131        if (messageNode.getNodeType() != Node.ELEMENT_NODE)
132            return null;
133
134        int status = -1;
135        String messageId = null;
136        String destination = null;
137        String errorText = null;
138        String clientReference = null;
139        BigDecimal remainingBalance = null;
140        BigDecimal messagePrice = null;
141        SmsSubmissionReachabilityStatus smsSubmissionReachabilityStatus = null;
142        String network = null;
143
144        NodeList nodes = messageNode.getChildNodes();
145        for (int i4 = 0; i4 < nodes.getLength(); i4++) {
146            Node node = nodes.item(i4);
147            if (node.getNodeType() != Node.ELEMENT_NODE)
148                continue;
149            if (node.getNodeName().equals("messageId")) {
150                messageId = node.getFirstChild() == null ? null : node.getFirstChild().getNodeValue();
151            } else if (node.getNodeName().equals("to")) {
152                destination = node.getFirstChild() == null ? null : node.getFirstChild().getNodeValue();
153            } else if (node.getNodeName().equals("status")) {
154                String str = node.getFirstChild() == null ? null : node.getFirstChild().getNodeValue();
155                try {
156                    status = Integer.parseInt(str);
157                } catch (NumberFormatException e) {
158                    log.error("xml parser .. invalid value in <status> node [ " + str + " ] ");
159                    status = SmsSubmissionResult.STATUS_INTERNAL_ERROR;
160                }
161            } else if (node.getNodeName().equals("errorText")) {
162                errorText = node.getFirstChild() == null ? null : node.getFirstChild().getNodeValue();
163            } else if (node.getNodeName().equals("clientRef")) {
164                clientReference = node.getFirstChild() == null ? null : node.getFirstChild().getNodeValue();
165            } else if (node.getNodeName().equals("remainingBalance")) {
166                String str = node.getFirstChild() == null ? null : node.getFirstChild().getNodeValue();
167                try {
168                    if (str != null)
169                        remainingBalance = new BigDecimal(str);
170                } catch (NumberFormatException e) {
171                    log.error("xml parser .. invalid value in <remainingBalance> node [ " + str + " ] ");
172                }
173            } else if (node.getNodeName().equals("messagePrice")) {
174                String str = node.getFirstChild() == null ? null : node.getFirstChild().getNodeValue();
175                try {
176                    if (str != null)
177                        messagePrice = new BigDecimal(str);
178                } catch (NumberFormatException e) {
179                    log.error("xml parser .. invalid value in <messagePrice> node [ " + str + " ] ");
180                }
181            } else if (node.getNodeName().equals("network")) {
182                network = node.getFirstChild() == null ? null : node.getFirstChild().getNodeValue();
183            } else {
184                log.error("xml parser .. unknown node found in status-return, expected [ messageId, to, status, errorText, clientRef, messagePrice, remainingBalance, reachability, network ] -- found [ " + node.getNodeName() + " ] ");
185            }
186        }
187
188        if (status == -1)
189            throw new NexmoResponseParseException("Xml Parser - did not find a <status> node");
190
191        // Is this a temporary error ?
192        boolean temporaryError = (status == SmsSubmissionResult.STATUS_THROTTLED ||
193                status == SmsSubmissionResult.STATUS_INTERNAL_ERROR ||
194                status == SmsSubmissionResult.STATUS_TOO_MANY_BINDS);
195
196        return new SmsSubmissionResult(status,
197                destination,
198                messageId,
199                errorText,
200                clientReference,
201                remainingBalance,
202                messagePrice,
203                temporaryError,
204                smsSubmissionReachabilityStatus,
205                network);
206    }
207}