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 **/
019
020package lucee.runtime.converter;
021
022import java.io.IOException;
023import java.io.Writer;
024import java.util.Calendar;
025import java.util.HashSet;
026import java.util.Iterator;
027import java.util.List;
028import java.util.ListIterator;
029import java.util.Map;
030import java.util.Map.Entry;
031import java.util.Set;
032
033import lucee.commons.date.JREDateTimeUtil;
034import lucee.commons.lang.StringUtil;
035import lucee.runtime.PageContext;
036import lucee.runtime.engine.ThreadLocalPageContext;
037import lucee.runtime.op.Caster;
038import lucee.runtime.op.Decision;
039import lucee.runtime.type.Array;
040import lucee.runtime.type.Collection;
041import lucee.runtime.type.Collection.Key;
042import lucee.runtime.type.Query;
043import lucee.runtime.type.Struct;
044import lucee.runtime.type.dt.DateTime;
045import lucee.runtime.type.util.CollectionUtil;
046
047
048/**
049 * class to serialize to Convert CFML Objects (query,array,struct usw) to a JavaScript representation
050 */
051public final class JSConverter extends ConverterSupport {
052
053        private static final String NULL = "null";
054        private boolean useShortcuts=false;
055        private boolean useWDDX=true;
056
057        /**
058         * serialize a CFML object to a JavaScript Object
059         * @param object object to serialize
060         * @param clientVariableName name of the variable to create 
061         * @return vonverte Javascript Code as String
062         * @throws ConverterException
063         */
064        public String serialize(Object object, String clientVariableName) throws ConverterException {
065                StringBuilder sb=new StringBuilder();
066                _serialize(clientVariableName,object,sb,new HashSet<Object>());
067                String str = sb.toString().trim();
068                return clientVariableName+"="+str+(StringUtil.endsWith(str, ';')?"":";");
069                //return sb.toString();
070        }
071
072        
073
074           
075        @Override
076        public void writeOut(PageContext pc, Object source, Writer writer) throws ConverterException, IOException {
077                writer.write(_serialize(source));
078                writer.flush();
079        }
080        private String _serialize(Object object) throws ConverterException {
081                StringBuilder sb=new StringBuilder();
082                _serialize("tmp",object,sb,new HashSet<Object>());
083                String str = sb.toString().trim();
084                return str+(StringUtil.endsWith(str, ';')?"":";");
085                //return sb.toString();
086        }
087        
088        
089        
090        private void _serialize(String name,Object object,StringBuilder sb,Set<Object> done) throws ConverterException {
091                // NULL
092                if(object==null) {
093                        sb.append(goIn());
094                        sb.append(NULL+";");
095                        return;
096                }
097                // CharSequence (String, StringBuilder ...)
098                if(object instanceof CharSequence) {
099                        sb.append(goIn());
100                        sb.append(StringUtil.escapeJS(object.toString(),'"'));
101                        sb.append(";");
102                        return;
103                }
104                // Number
105                if(object instanceof Number) {
106                        sb.append(goIn());
107                        sb.append("\"");
108                        sb.append(Caster.toString(((Number)object)));
109                        sb.append("\";");
110                        return;
111                }
112                // Date
113                if(Decision.isDateSimple(object,false)) {
114                        _serializeDateTime(Caster.toDate(object,false,null,null),sb);
115                        return;
116                }
117                // Boolean
118                if(object instanceof Boolean) {
119                        sb.append(goIn());
120                        sb.append("\"");
121                        sb.append((((Boolean)object).booleanValue()?"true":"false"));
122                        sb.append("\";");
123                        return;
124                }
125                
126                Object raw = LazyConverter.toRaw(object);
127                if(done.contains(raw)){
128                        sb.append(NULL+";");
129                        return;
130                }
131                done.add(raw);
132                try {
133                        // Struct
134                        if(object instanceof Struct) {
135                                _serializeStruct(name,(Struct)object,sb,done);
136                                return;
137                        }
138                        // Map
139                        if(object instanceof Map) {
140                                _serializeMap(name,(Map)object,sb,done);
141                                return;
142                        }
143                        // List
144                        if(object instanceof List) {
145                                _serializeList(name,(List)object,sb,done);
146                                return;
147                        }
148                        // Array
149                        if(Decision.isArray(object)) {
150                                _serializeArray(name,Caster.toArray(object,null),sb,done);
151                                return;
152                        }
153                        // Query
154                        if(object instanceof Query) {
155                                _serializeQuery(name,(Query)object,sb,done);
156                                return;
157                        }
158                }
159                finally {
160                        done.remove(raw);
161                }
162                
163                throw new ConverterException("can't serialize Object of type ["+Caster.toClassName(object)+"] to a js representation");
164                //deep--;
165                //return rtn;
166        }
167        
168
169        
170        /**
171         * serialize a Array
172         * @param name 
173         * @param array Array to serialize
174         * @param sb 
175         * @param done 
176         * @return serialized array
177         * @throws ConverterException
178         */
179        private void _serializeArray(String name, Array array, StringBuilder sb, Set<Object> done) throws ConverterException {
180                _serializeList(name,array.toList(),sb,done);
181        }
182        
183        /**
184         * serialize a List (as Array)
185         * @param name 
186         * @param list List to serialize
187         * @param sb 
188         * @param done 
189         * @return serialized list
190         * @throws ConverterException
191         */
192        private void _serializeList(String name, List list, StringBuilder sb, Set<Object> done) throws ConverterException {
193                
194                
195                if(useShortcuts)sb.append("[];");
196                else sb.append("new Array();");
197                
198                ListIterator it=list.listIterator();
199                int index=-1;
200                while(it.hasNext()) {
201                        //if(index!=-1)sb.append(",");
202                        index = it.nextIndex();
203                        sb.append(name+"["+index+"]=");
204                        _serialize(name+"["+index+"]",it.next(),sb,done);
205                        //sb.append(";");
206                }
207        }
208
209        /**
210         * serialize a Struct
211         * @param name 
212         * @param struct Struct to serialize
213         * @param done 
214         * @param sb2 
215         * @return serialized struct
216         * @throws ConverterException
217         */
218        private String _serializeStruct(String name, Struct struct, StringBuilder sb, Set<Object> done) throws ConverterException {
219                if(useShortcuts)sb.append("{};");
220                else sb.append("new Object();");
221                
222                Iterator<Entry<Key, Object>> it = struct.entryIterator();
223                Entry<Key, Object> e;
224                while(it.hasNext()) {
225                        e = it.next();
226                        // lower case ist ok!
227                        String key=StringUtil.escapeJS(Caster.toString(e.getKey().getLowerString(),""),'"');
228            sb.append(name+"["+key+"]=");
229                        _serialize(name+"["+key+"]",e.getValue(),sb,done);
230                }
231        return sb.toString();
232        }
233
234        /**
235         * serialize a Map (as Struct)
236         * @param name 
237         * @param map Map to serialize
238         * @param done 
239         * @param sb2 
240         * @return serialized map
241         * @throws ConverterException
242         */
243        private String _serializeMap(String name, Map map, StringBuilder sb, Set<Object> done) throws ConverterException {
244
245                if(useShortcuts)sb.append("{}");
246                else sb.append("new Object();");
247                Iterator it=map.keySet().iterator();
248                while(it.hasNext()) {
249                        Object key=it.next();
250                        String skey=StringUtil.toLowerCase(StringUtil.escapeJS(key.toString(),'"'));
251            sb.append(name+"["+skey+"]=");
252                        _serialize(name+"["+skey+"]",map.get(key),sb,done);
253                        //sb.append(";");
254                }
255                return sb.toString();
256        }
257        
258        /**
259         * serialize a Query
260         * @param query Query to serialize
261         * @param done 
262         * @return serialized query
263         * @throws ConverterException
264         */
265        private void _serializeQuery(String name,Query query,StringBuilder sb, Set<Object> done) throws ConverterException {
266                if(useWDDX)_serializeWDDXQuery(name,query,sb,done);
267                else _serializeASQuery(name,query,sb,done);
268        }
269
270        private void _serializeWDDXQuery(String name,Query query,StringBuilder sb, Set<Object> done) throws ConverterException {
271                Iterator<Key> it = query.keyIterator();
272                Key k;
273                sb.append("new WddxRecordset();");
274                
275                int recordcount=query.getRecordcount();
276                int i=-1;
277                while(it.hasNext()) {
278                        i++;
279                        k = it.next();
280                        if(useShortcuts)sb.append("col"+i+"=[];");
281                        else sb.append("col"+i+"=new Array();");
282                        // lower case ist ok!
283                        String skey = StringUtil.escapeJS(k.getLowerString(),'"');
284                        for(int y=0;y<recordcount;y++) {
285                                
286                                sb.append("col"+i+"["+y+"]=");
287                                
288                                _serialize("col"+i+"["+y+"]",query.getAt(k,y+1,null),sb,done);
289                                
290                        }
291                        sb.append(name+"["+skey+"]=col"+i+";col"+i+"=null;");
292                }
293        }
294
295        private void _serializeASQuery(String name,Query query,StringBuilder sb, Set<Object> done) throws ConverterException {
296
297                Collection.Key[] keys = CollectionUtil.keys(query);
298                String[] strKeys = new String[keys.length];
299                for(int i=0;i<strKeys.length;i++) {
300                        strKeys[i] = StringUtil.escapeJS(keys[i].getString(),'"');
301                }
302                if(useShortcuts)sb.append("[];");
303                else sb.append("new Array();");
304                
305                int recordcount=query.getRecordcount();
306                for(int i=0;i<recordcount;i++) {
307                        if(useShortcuts)sb.append(name+"["+i+"]={};");
308                        else sb.append(name+"["+i+"]=new Object();");
309                        
310                        for(int y=0;y<strKeys.length;y++) {
311                                sb.append(name+"["+i+"]["+strKeys[y]+"]=");
312                                _serialize(name+"["+i+"]["+strKeys[y]+"]",query.getAt(keys[y],i+1,null),sb,done);
313                        }
314                }
315        }
316        
317        
318
319        /**
320         * serialize a DateTime
321         * @param dateTime DateTime to serialize
322         * @param sb 
323         * @param sb
324         * @throws ConverterException
325         */
326        private synchronized void _serializeDateTime(DateTime dateTime, StringBuilder sb) {
327           
328                Calendar c = JREDateTimeUtil.getThreadCalendar(ThreadLocalPageContext.getTimeZone());
329                c.setTime(dateTime);
330            sb.append(goIn());
331            sb.append("new Date(");
332            sb.append(c.get(Calendar.YEAR));
333            sb.append(",");
334            sb.append(c.get(Calendar.MONTH));
335            sb.append(",");
336            sb.append(c.get(Calendar.DAY_OF_MONTH));
337            sb.append(",");
338            sb.append(c.get(Calendar.HOUR_OF_DAY));
339            sb.append(",");
340            sb.append(c.get(Calendar.MINUTE));
341            sb.append(",");
342            sb.append(c.get(Calendar.SECOND));
343            sb.append(");");
344        }
345
346        private String goIn() {
347                //StringBuilder rtn=new StringBuilder(deep);
348                //for(int i=0;i<deep;i++) rtn.append('\t');
349                return "";//rtn.toString();
350        }
351
352        public void useShortcuts(boolean useShortcuts) { 
353                this.useShortcuts=useShortcuts;
354                
355        }
356
357        public void useWDDX(boolean useWDDX) {
358                this.useWDDX=useWDDX;
359        }
360        
361        /*
362         * @param args
363         * @throws Exception
364         
365        public static void main(String[] args) throws Exception {
366                JSConverter js=new JSConverter();
367                Query query=QueryNew.call(null,"aaa,bbb,ccc");
368                QueryAddRow.call(null,query);
369                QuerySetCell.call(null,query,"aaa","1.1");
370                QuerySetCell.call(null,query,"bbb","1.2");
371                QuerySetCell.call(null,query,"ccc","1.3");
372                QueryAddRow.call(null,query);
373                QuerySetCell.call(null,query,"aaa","2.1");
374                QuerySetCell.call(null,query,"bbb","2.2");
375                QuerySetCell.call(null,query,"ccc","2.3");
376                QueryAddRow.call(null,query);
377                QuerySetCell.call(null,query,"aaa","3.1");
378                QuerySetCell.call(null,query,"bbb","3.2");
379                QuerySetCell.call(null,query,"ccc","3.3<hello>");
380                Array arr2=List ToArray.call(null,"111,222");
381                Array arr=List ToArray.call(null,"aaaa,bbb,ccc,dddd,eee");
382                
383                arr.set(10,arr2);
384
385                Struct sct= new Struct();
386                sct.set("aaa","val1");
387                sct.set("bbb","val2");
388                sct.set("ccc","val3");
389                sct.set("ddd",arr2);
390                
391                /*
392        }*/
393}