001/**
002 *
003 * Copyright (c) 2014, the Railo Company Ltd. All rights reserved.
004 *
005 * This library is free software; you can redistribute it and/or
006 * modify it under the terms of the GNU Lesser General Public
007 * License as published by the Free Software Foundation; either 
008 * version 2.1 of the License, or (at your option) any later version.
009 * 
010 * This library is distributed in the hope that it will be useful,
011 * but WITHOUT ANY WARRANTY; without even the implied warranty of
012 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
013 * Lesser General Public License for more details.
014 * 
015 * You should have received a copy of the GNU Lesser General Public 
016 * License along with this library.  If not, see <http://www.gnu.org/licenses/>.
017 * 
018 **/
019package lucee.runtime.net.http;
020
021import java.io.IOException;
022import java.io.InputStream;
023import java.net.MalformedURLException;
024import java.net.URL;
025import java.nio.charset.Charset;
026import java.util.HashMap;
027import java.util.Iterator;
028import java.util.Map;
029import java.util.Map.Entry;
030
031import lucee.commons.io.IOUtil;
032import lucee.commons.lang.ExceptionUtil;
033import lucee.commons.lang.StringUtil;
034import lucee.commons.lang.mimetype.MimeType;
035import lucee.commons.net.HTTPUtil;
036import lucee.commons.net.http.HTTPEngine;
037import lucee.commons.net.http.HTTPResponse;
038import lucee.commons.net.http.Header;
039import lucee.runtime.ComponentPage;
040import lucee.runtime.Info;
041import lucee.runtime.PageContext;
042import lucee.runtime.PageContextImpl;
043import lucee.runtime.converter.ConverterException;
044import lucee.runtime.converter.JSONConverter;
045import lucee.runtime.converter.ScriptConverter;
046import lucee.runtime.dump.DumpData;
047import lucee.runtime.dump.DumpProperties;
048import lucee.runtime.dump.DumpTable;
049import lucee.runtime.dump.SimpleDumpData;
050import lucee.runtime.engine.ThreadLocalPageContext;
051import lucee.runtime.exp.ApplicationException;
052import lucee.runtime.exp.ExpressionException;
053import lucee.runtime.exp.PageException;
054import lucee.runtime.exp.PageRuntimeException;
055import lucee.runtime.net.proxy.ProxyData;
056import lucee.runtime.net.rpc.RPCException;
057import lucee.runtime.op.Caster;
058import lucee.runtime.type.Array;
059import lucee.runtime.type.ArrayImpl;
060import lucee.runtime.type.Collection;
061import lucee.runtime.type.Collection.Key;
062import lucee.runtime.type.Iteratorable;
063import lucee.runtime.type.KeyImpl;
064import lucee.runtime.type.Objects;
065import lucee.runtime.type.Struct;
066import lucee.runtime.type.StructImpl;
067import lucee.runtime.type.UDF;
068import lucee.runtime.type.dt.DateTime;
069import lucee.runtime.type.it.KeyAsStringIterator;
070import lucee.runtime.type.it.KeyIterator;
071import lucee.runtime.type.it.ObjectsEntryIterator;
072import lucee.runtime.type.it.ObjectsIterator;
073import lucee.runtime.type.util.ArrayUtil;
074import lucee.runtime.type.util.KeyConstants;
075import lucee.runtime.type.util.ListUtil;
076import lucee.runtime.type.util.UDFUtil;
077
078/**
079 * Client to implement http based webservice
080 */
081public class HTTPClient implements Objects, Iteratorable {
082
083        private static final long serialVersionUID = -7920478535030737537L;
084
085        private static final String USER_AGENT = "Lucee "+Info.getFullVersionInfo();
086
087        
088        private URL metaURL;
089        private String username;
090        private String password;
091        private ProxyData proxyData;
092        private URL url;
093        private Struct meta;
094
095        private int argumentsCollectionFormat=-1;
096
097        public HTTPClient(String httpUrl, String username, String password, ProxyData proxyData) throws PageException {
098                try {
099                        url=HTTPUtil.toURL(httpUrl,true);
100                        
101                        if(!StringUtil.isEmpty(this.url.getQuery())) throw new ApplicationException("invalid url, query string is not allowed as part of the call");
102                        metaURL=HTTPUtil.toURL(url.toExternalForm()+"?cfml",true);
103                }
104                catch (MalformedURLException e) {
105                        throw Caster.toPageException(e);
106                }
107                
108                this.username=username;
109                this.password=password;
110                this.proxyData=proxyData;
111                
112        }
113
114        @Override
115        public DumpData toDumpData(PageContext pageContext, int maxlevel, DumpProperties dp) {
116                try {
117                        Array args;
118                        Struct sct = getMetaData(pageContext),val,a;
119                        DumpTable cfc = new DumpTable("udf","#66ccff","#ccffff","#000000"),udf,arg;
120                        cfc.setTitle("Web Service (HTTP)");
121                        if(dp.getMetainfo())cfc.setComment(url.toExternalForm());
122                        Iterator<Entry<Key, Object>> it = sct.entryIterator();
123                        Entry<Key, Object> e;
124                        // Loop UDFs
125                        while(it.hasNext()){
126                                e = it.next();
127                                val=Caster.toStruct(e.getValue());
128                                
129                                // udf name
130                                udf = new DumpTable("udf","#66ccff","#ccffff","#000000");
131                                arg = new DumpTable("udf","#66ccff","#ccffff","#000000");
132                                
133                                cfc.appendRow(1, new SimpleDumpData(e.getKey().getString()),udf);
134                                
135                                // args
136                                args = Caster.toArray(val.get(KeyConstants._arguments));
137                                udf.appendRow(1,new SimpleDumpData("arguments"),arg);
138                                arg.appendRow(7,new SimpleDumpData("name"),new SimpleDumpData("required"),new SimpleDumpData("type"));
139                                Iterator<Object> ait = args.valueIterator();
140                                while(ait.hasNext()){
141                                        a=Caster.toStruct(ait.next());
142                                        arg.appendRow(0,
143                                                        new SimpleDumpData(Caster.toString(a.get(KeyConstants._name))),
144                                                        new SimpleDumpData(Caster.toString(a.get(KeyConstants._required))),
145                                                        new SimpleDumpData(Caster.toString(a.get(KeyConstants._type))));
146                                        
147                                }
148
149                                // return type
150                                udf.appendRow(1,new SimpleDumpData("return type"),new SimpleDumpData(Caster.toString(val.get(KeyConstants._returntype))));
151                                
152                                
153                                /*
154                                cfc.appendRow(new DumpRow(0,new DumpData[]{
155                                                new SimpleDumpData(arg.getDisplayName()),
156                                                new SimpleDumpData(e.getKey().getString()),
157                                                new SimpleDumpData(arg.isRequired()),
158                                                new SimpleDumpData(arg.getTypeAsString()),
159                                                def,
160                                                new SimpleDumpData(arg.getHint())}));*/
161                                
162                        }
163                        return cfc;
164                
165                }
166                catch (Throwable t) {
167                        ExceptionUtil.rethrowIfNecessary(t);
168                        throw new PageRuntimeException(Caster.toPageException(t));
169                }
170        }
171        
172        private Struct getMetaData(PageContext pc) {
173                if(meta==null) {
174                        pc=ThreadLocalPageContext.get(pc);
175                        InputStream is=null;
176                        
177                        try{
178                                HTTPResponse rsp = HTTPEngine.get(metaURL, username, password, -1, 0, "UTF-8", USER_AGENT, proxyData, null);
179                                MimeType mt = getMimeType(rsp,null);
180                                int format = MimeType.toFormat(mt, -1);
181                                if(format==-1) throw new ApplicationException("cannot convert response with mime type ["+mt+"] to a CFML Object");
182                                is = rsp.getContentAsStream();
183                                Struct data = Caster.toStruct(ReqRspUtil.toObject(pc,IOUtil.toBytes(is,false),format,mt.getCharset(),null));
184                                Object oUDF=data.get(KeyConstants._functions,null);
185                                Object oAACF=data.get(ComponentPage.ACCEPT_ARG_COLL_FORMATS,null);
186                                
187                                if(oUDF!=null && oAACF!=null) {
188                                        meta=Caster.toStruct(oUDF);
189                                        String[] strFormats = ListUtil.listToStringArray(Caster.toString(oAACF),',');
190                                        argumentsCollectionFormat=UDFUtil.toReturnFormat(strFormats,UDF.RETURN_FORMAT_JSON);
191                                }
192                                else {
193                                        meta=data;
194                                }
195                                
196                                
197                        }
198                        catch(Throwable t) {
199                                ExceptionUtil.rethrowIfNecessary(t);
200                                throw new PageRuntimeException(Caster.toPageException(t));
201                        }
202                        finally {
203                                IOUtil.closeEL(is);
204                        }
205                }
206                return meta;
207        }
208
209        @Override
210        public Iterator<Key> keyIterator() {
211                try {
212                        return getMetaData(null).keyIterator();
213                }
214                catch (Exception e) {
215                        return new KeyIterator(new Collection.Key[0]);
216                }
217        }
218
219        @Override
220        public Object call(PageContext pc, Key methodName, Object[] arguments) throws PageException {
221                checkFunctionExistence(pc,methodName,false);
222                
223                if(arguments.length==0) return _callWithNamedValues(pc, methodName, new StructImpl());
224                Struct m = checkFunctionExistence(pc,methodName,true);
225                
226                
227                Array args = Caster.toArray(m.get(KeyConstants._arguments,null),null);
228                if(args==null) args=new ArrayImpl();
229                Struct sct=new StructImpl(),el;
230                String name;
231                for(int i=0;i<arguments.length;i++){
232                        if(args.size()>i) {
233                                el=Caster.toStruct(args.get(i+1, null),null);
234                                if(el!=null) {
235                                        name=Caster.toString(el.get(KeyConstants._name,null),null);
236                                        if(!StringUtil.isEmpty(name)) {
237                                                sct.set(name, arguments[i]);
238                                                continue;
239                                        }
240                                }
241                        }
242                        sct.set("arg"+(i+1), arguments[i]);
243                }
244                
245                
246                return _callWithNamedValues(pc, methodName, sct);
247        }
248
249        @Override
250
251        public Object callWithNamedValues(PageContext pc, Key methodName, Struct args) throws PageException {
252                checkFunctionExistence(pc,methodName,false);
253                return _callWithNamedValues(pc, methodName, args);
254                
255        }
256        
257        private Object _callWithNamedValues(PageContext pc, Key methodName, Struct args) throws PageException {
258                
259                // prepare request
260                Map<String,String> formfields=new HashMap<String, String>();
261                formfields.put("method", methodName.getString());
262                formfields.put("returnformat", "cfml");
263                
264                
265                String str;
266                try {
267                        if(UDF.RETURN_FORMAT_JSON==argumentsCollectionFormat)   {
268                                Charset cs = ((PageContextImpl)pc).getWebCharset();
269                                str = new JSONConverter(true,cs).serialize(pc,args,false);
270                                formfields.put("argumentCollectionFormat", "json");
271                        }
272                        else if(UDF.RETURN_FORMAT_SERIALIZE==argumentsCollectionFormat) {
273                                str = new ScriptConverter().serialize(args);
274                                formfields.put("argumentCollectionFormat", "cfml");
275                        }
276                        else {
277                                str = new ScriptConverter().serialize(args); // Json interpreter in Lucee also accepts cfscript
278                        }
279                }
280                catch (ConverterException e) {
281                        throw Caster.toPageException(e);
282                }
283                
284                
285                // add aparams to request
286                formfields.put("argumentCollection", str);
287                /*
288                Iterator<Entry<Key, Object>> it = args.entryIterator();
289                Entry<Key, Object> e;
290                while(it.hasNext()){
291                        e = it.next();
292                        formfields.put(e.getKey().getString(), Caster.toString(e.getValue()));
293                }*/
294                
295                Map<String,String> headers=new HashMap<String, String>();
296                headers.put("accept", "application/cfml,application/json"); // application/java disabled for the moment, it is not working when we have different lucee versions
297                
298                InputStream is=null;
299                try {
300                        // call remote cfc
301                        HTTPResponse rsp = HTTPEngine.post(url, username, password, -1, 0, "UTF-8", USER_AGENT, proxyData,headers, formfields);
302                        
303                        // read result
304                        Header[] rspHeaders = rsp.getAllHeaders();
305                        MimeType mt = getMimeType(rspHeaders,null);
306                        int format = MimeType.toFormat(mt, -1);
307                        if(format==-1) {
308                                if(rsp.getStatusCode()!=200) {
309                                        boolean hasMsg=false;
310                                        String msg=rsp.getStatusText();
311                                        for(int i=0;i<rspHeaders.length;i++){
312                                                if(rspHeaders[i].getName().equalsIgnoreCase("exception-message")){
313                                                        msg=rspHeaders[i].getValue();
314                                                        hasMsg=true;
315                                                }
316                                        }
317                                        is = rsp.getContentAsStream();
318                                        ApplicationException ae = new ApplicationException("remote component throws the following error:"+msg);
319                                        if(!hasMsg)ae.setAdditional(KeyImpl.init("respone-body"),IOUtil.toString(is, mt.getCharset()));
320                                        
321                                        throw ae;
322                                }
323                                throw new ApplicationException("cannot convert response with mime type ["+mt+"] to a CFML Object");
324                        }
325                        is = rsp.getContentAsStream();
326                        return ReqRspUtil.toObject(pc,IOUtil.toBytes(is,false),format,mt.getCharset(),null);
327                }
328                catch (IOException ioe) {
329                        throw Caster.toPageException(ioe);
330                }
331                finally {
332                        IOUtil.closeEL(is);
333                }
334        }
335
336        private Struct checkFunctionExistence(PageContext pc,Key methodName, boolean getDataFromRemoteIfNecessary) throws ApplicationException {
337                if(getDataFromRemoteIfNecessary) getMetaData(pc);
338                if(meta==null) return null;
339                Struct m = Caster.toStruct(meta.get(methodName,null),null);
340                if(m==null) throw new ApplicationException(
341                                "the remote component has no function with name ["+methodName+"]",
342                                ExceptionUtil.createSoundexDetail(methodName.getString(),meta.keysAsStringIterator(),"functions"));
343                return m;
344        }
345
346
347        private MimeType getMimeType(HTTPResponse rsp, MimeType defaultValue) {
348                return getMimeType(rsp.getAllHeaders(), defaultValue);
349        }
350        private MimeType getMimeType(Header[] headers, MimeType defaultValue) {
351                String returnFormat=null,contentType=null;
352                for(int i=0;i<headers.length;i++){
353                        if(headers[i].getName().equalsIgnoreCase("Return-Format"))returnFormat=headers[i].getValue();
354                        else if(headers[i].getName().equalsIgnoreCase("Content-Type"))contentType=headers[i].getValue();
355                }
356                MimeType rf=null,ct=null;
357                
358                // return format
359                if(!StringUtil.isEmpty(returnFormat)) {
360                        int format=UDFUtil.toReturnFormat(returnFormat,-1);
361                        rf=MimeType.toMimetype(format, null);
362                }
363                // ContentType
364                if(!StringUtil.isEmpty(contentType)) {
365                        ct= MimeType.getInstance(contentType);
366                }
367                if(rf!=null && ct!=null) {
368                        if(rf.same(ct)) return ct; // because this has perhaps a charset definition
369                        return rf;
370                }
371                if(rf!=null) return rf;
372                if(ct!=null) return ct;
373                
374                
375                return defaultValue;
376        }
377
378        @Override
379        public Object get(PageContext pc, Collection.Key key) throws PageException {
380        return call(pc,KeyImpl.init("get"+key.getString()), ArrayUtil.OBJECT_EMPTY);
381        }
382
383        @Override
384        public Object get(PageContext pc, Collection.Key key, Object defaultValue) {
385                try {
386            return call(pc,KeyImpl.init("get"+StringUtil.ucFirst(key.getString())), ArrayUtil.OBJECT_EMPTY);
387        } catch (PageException e) {
388            return defaultValue;
389        }
390        }
391        
392        @Override
393        public Object set(PageContext pc, Collection.Key propertyName, Object value) throws PageException {
394        return call(pc,KeyImpl.init("set"+propertyName.getString()), new Object[]{value});
395        }
396
397        @Override
398        public Object setEL(PageContext pc, Collection.Key propertyName, Object value) {
399                try {
400            return call(pc,KeyImpl.init("set"+propertyName.getString()), new Object[]{value});
401        } catch (PageException e) {
402            return null;
403        }
404        }
405        
406        @Override
407        public Iterator<String> keysAsStringIterator() {
408                return new KeyAsStringIterator(keyIterator());
409        }
410
411        @Override
412        public Iterator<Object> valueIterator() {
413                return new ObjectsIterator(keyIterator(),this);
414        }
415        
416        @Override
417        public Iterator<Entry<Key, Object>> entryIterator() {
418                return new ObjectsEntryIterator(keyIterator(), this);
419        }
420
421        @Override
422    public String castToString() throws ExpressionException {
423        throw new RPCException("can't cast Webservice to a string");
424    }
425
426        @Override
427        public String castToString(String defaultValue) {
428                return defaultValue;
429        }
430
431    @Override
432    public boolean castToBooleanValue() throws ExpressionException {
433        throw new RPCException("can't cast Webservice to a boolean");
434    }
435    
436    @Override
437    public Boolean castToBoolean(Boolean defaultValue) {
438        return defaultValue;
439    }
440
441    @Override
442    public double castToDoubleValue() throws ExpressionException {
443        throw new RPCException("can't cast Webservice to a number");
444    }
445    
446    @Override
447    public double castToDoubleValue(double defaultValue) {
448        return defaultValue;
449    }
450
451    @Override
452    public DateTime castToDateTime() throws RPCException {
453        throw new RPCException("can't cast Webservice to a Date Object");
454    }
455    
456    @Override
457    public DateTime castToDateTime(DateTime defaultValue) {
458        return defaultValue;
459    }
460
461        @Override
462        public int compareTo(boolean b) throws ExpressionException {
463                throw new ExpressionException("can't compare Webservice Object with a boolean value");
464        }
465
466        @Override
467        public int compareTo(DateTime dt) throws PageException {
468                throw new ExpressionException("can't compare Webservice Object with a DateTime Object");
469        }
470
471        @Override
472        public int compareTo(double d) throws PageException {
473                throw new ExpressionException("can't compare Webservice Object with a numeric value");
474        }
475
476        @Override
477        public int compareTo(String str) throws PageException {
478                throw new ExpressionException("can't compare Webservice Object with a String");
479        }
480}