001    package railo.loader.engine;
002    
003    import java.io.BufferedOutputStream;
004    import java.io.EOFException;
005    import java.io.File;
006    import java.io.FileOutputStream;
007    import java.io.IOException;
008    import java.io.InputStream;
009    import java.io.OutputStream;
010    import java.io.PrintWriter;
011    import java.io.UnsupportedEncodingException;
012    import java.lang.reflect.InvocationTargetException;
013    import java.lang.reflect.Method;
014    import java.net.URL;
015    import java.net.URLDecoder;
016    import java.util.ArrayList;
017    import java.util.Arrays;
018    import java.util.Date;
019    import java.util.Iterator;
020    import java.util.List;
021    
022    import javax.servlet.ServletConfig;
023    import javax.servlet.ServletException;
024    
025    import railo.Version;
026    import railo.loader.TP;
027    import railo.loader.classloader.RailoClassLoader;
028    import railo.loader.util.ExtensionFilter;
029    import railo.loader.util.Util;
030    
031    import com.intergral.fusiondebug.server.FDControllerFactory;
032    
033    /**
034     * Factory to load CFML Engine
035     */
036    public class CFMLEngineFactory {
037            
038             // set to false to disable patch loading, for example in major alpha releases
039        private static final boolean PATCH_ENABLED = true;
040        
041            private static CFMLEngineFactory factory;
042        private static File railoServerRoot;
043        private static CFMLEngineWrapper engineListener;
044        private CFMLEngine engine;
045        private ClassLoader mainClassLoader=new TP().getClass().getClassLoader();
046        private int version;
047        private List<EngineChangeListener> listeners=new ArrayList<EngineChangeListener>();
048        private File resourceRoot;
049    
050            private PrintWriter out;
051        
052        
053        /**
054         * Constructor of the class
055         */
056        protected CFMLEngineFactory(){
057        }
058    
059        /**
060         * returns instance of this factory (singelton-> always the same instance)
061         * do auto update when changes occur
062         * @param config 
063         * @return Singelton Instance of the Factory
064         * @throws ServletException 
065         */
066        public static CFMLEngine getInstance(ServletConfig config) throws ServletException {
067            
068            if(engineListener!=null) {
069                    if(factory==null) factory=engineListener.getCFMLEngineFactory();
070                    return engineListener;
071            }
072            
073            if(factory==null) factory=new CFMLEngineFactory();
074            
075            
076            // read init param from config
077            factory.setInitParam(config);
078            
079            CFMLEngine engine = factory.getEngine();
080            engine.addServletConfig(config);
081            engineListener = new CFMLEngineWrapper(engine);
082            
083            // add listener for update
084            factory.addListener(engineListener);
085            return engineListener;
086        }
087    
088        /**
089         * returns instance of this factory (singelton-> always the same instance)
090         * do auto update when changes occur
091         * @return Singelton Instance of the Factory
092         * @throws RuntimeException 
093         */
094        public static CFMLEngine getInstance() throws RuntimeException {
095            if(engineListener!=null) return engineListener;
096            throw new RuntimeException("engine is not initalized, you must first call getInstance(ServletConfig)");
097        }
098    
099        /**
100         * used only for internal usage
101         * @param engine
102         * @throws RuntimeException
103         */
104        public static void registerInstance(CFMLEngine engine) throws RuntimeException {
105            if(factory==null) factory=engine.getCFMLEngineFactory();
106            
107            // first update existing listener
108            if(engineListener!=null) {
109                    if(engineListener.equalTo(engine, true)) return;
110                    engineListener.onUpdate(engine);// perhaps this is still refrenced in the code, because of that we update it
111                    factory.removeListener(engineListener);
112            }
113            
114            // now register this
115            if(engine instanceof CFMLEngineWrapper) 
116                    engineListener=(CFMLEngineWrapper) engine;
117            else 
118                    engineListener = new CFMLEngineWrapper(engine);
119            
120            factory.addListener(engineListener);    
121        }
122        
123        
124        /**
125         * returns instance of this factory (singelton-> always the same instance)
126         * @param config
127         * @param listener 
128         * @return Singelton Instance of the Factory
129         * @throws ServletException 
130         */
131        public static CFMLEngine getInstance(ServletConfig config, EngineChangeListener listener) throws ServletException {
132            getInstance(config);
133            
134            // add listener for update
135            factory.addListener(listener);
136            
137            // read init param from config
138            factory.setInitParam(config);
139            
140            CFMLEngine e = factory.getEngine();
141            e.addServletConfig(config);
142            
143            // make the FDController visible for the FDClient
144            FDControllerFactory.makeVisible();
145            
146            return e;
147        }
148        
149        void setInitParam(ServletConfig config) {
150            if(railoServerRoot!=null) return;
151            
152            String initParam=config.getInitParameter("railo-server-directory");
153            if(Util.isEmpty(initParam))initParam=config.getInitParameter("railo-server-root");
154            if(Util.isEmpty(initParam))initParam=config.getInitParameter("railo-server-dir");
155            if(Util.isEmpty(initParam))initParam=config.getInitParameter("railo-server");
156            initParam=Util.parsePlaceHolder(Util.removeQuotes(initParam,true));
157            
158            try {
159                if(!Util.isEmpty(initParam)) {
160                    File root=new File(initParam);
161                    if(!root.exists()) {
162                        if(root.mkdirs()) {
163                            railoServerRoot=root.getCanonicalFile();
164                            return;
165                        }
166                    }
167                    else if(root.canWrite()) {
168                        railoServerRoot=root.getCanonicalFile();
169                        return;
170                    }
171                }
172            }
173            catch(IOException ioe){}
174        }
175        
176    
177            /**
178         * adds a listener to the factory that will be informed when a new engine will be loaded.
179         * @param listener
180         */
181        private void addListener(EngineChangeListener listener) {
182           if(!listeners.contains(listener)) {
183               listeners.add(listener);
184           }
185        }
186        
187        private void removeListener(EngineChangeListener listener) {
188            listeners.remove(listener);
189         }
190    
191        /**
192         * @return CFML Engine
193         * @throws ServletException
194         */
195        private CFMLEngine getEngine() throws ServletException {
196            if(engine==null)initEngine();
197            return engine;
198        }
199    
200        private void initEngine() throws ServletException {
201            
202            int coreVersion=Version.getIntVersion();
203            long coreCreated=Version.getCreateTime();
204            
205            
206            // get newest railo version as file
207            File patcheDir=null;
208            try {
209                patcheDir = getPatchDirectory();
210                log("railo-server-root:"+patcheDir.getParent());
211            } 
212            catch (IOException e) {
213               throw new ServletException(e);
214            }
215            
216            File[] patches=PATCH_ENABLED?patcheDir.listFiles(new ExtensionFilter(new String[]{"."+getCoreExtension()})):null;
217            File railo=null;
218            if(patches!=null) {
219                for(int i=0;i<patches.length;i++) {
220                    if(patches[i].getName().startsWith("tmp.rc")) {
221                        patches[i].delete();
222                    }
223                    else if(patches[i].lastModified()<coreCreated) {
224                        patches[i].delete();
225                    }
226                    else if(railo==null || isNewerThan(Util.toInVersion(patches[i].getName()),Util.toInVersion(railo.getName()))) {
227                        railo=patches[i];
228                    }
229                }
230            }
231            if(railo!=null && isNewerThan(coreVersion,Util.toInVersion(railo.getName())))railo=null;
232            
233            // Load Railo
234            //URL url=null;
235            try {
236                // Load core version when no patch available
237                if(railo==null) {
238                    tlog("Load Build in Core");
239                    // 
240                    String coreExt=getCoreExtension();
241                    engine=getCore(coreExt);
242                    
243                    
244                    railo=new File(patcheDir,engine.getVersion()+"."+coreExt);
245                   if(PATCH_ENABLED) {
246                            InputStream bis = new TP().getClass().getResourceAsStream("/core/core."+coreExt);
247                            OutputStream bos=new BufferedOutputStream(new FileOutputStream(railo));
248                            Util.copy(bis,bos);
249                            Util.closeEL(bis,bos);
250                    }
251                }
252                else {
253                    try {
254                            engine=getEngine(new RailoClassLoader(railo,mainClassLoader));
255                    }
256                    catch(EOFException e) {
257                            System.err.println("Railo patch file "+railo+" is invalid, please delete it");
258                            engine=getCore(getCoreExtension());
259                    }
260                }
261                version=Util.toInVersion(engine.getVersion());
262                
263                tlog("Loaded Railo Version "+engine.getVersion());
264            }
265            catch(InvocationTargetException e) {
266                e.getTargetException().printStackTrace();
267                throw new ServletException(e.getTargetException());
268            }
269            catch(Exception e) {
270                e.printStackTrace();
271                throw new ServletException(e);
272            }
273            
274            //check updates
275            String updateType=engine.getUpdateType();
276            if(updateType==null || updateType.length()==0)updateType="manuell";
277            
278            if(updateType.equalsIgnoreCase("auto")) {
279                new UpdateChecker(this).start();
280            }
281            
282        }
283        
284    
285        private String getCoreExtension() throws ServletException {
286            URL res = new TP().getClass().getResource("/core/core.rcs");
287            if(res!=null) return "rcs";
288            
289            res = new TP().getClass().getResource("/core/core.rc");
290            if(res!=null) return "rc";
291            
292            throw new ServletException("missing core file");
293            }
294    
295            private CFMLEngine getCore(String ext) throws SecurityException, IllegalArgumentException, ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InvocationTargetException, IOException {
296            InputStream is = null;
297            try {
298                    is = new TP().getClass().getResourceAsStream("/core/core."+ext);
299                    RailoClassLoader classLoader=new RailoClassLoader(is,mainClassLoader,ext.equalsIgnoreCase("rcs"));
300                    return getEngine(classLoader);
301            }
302            finally {
303                    Util.closeEL(is);
304            }
305            }
306    
307            /**
308         * method to initalize a update of the CFML Engine.
309         * checks if there is a new Version and update it whwn a new version is available
310         * @param password
311         * @return has updated
312         * @throws IOException
313         * @throws ServletException 
314         */
315        public boolean update(String password) throws IOException, ServletException {
316            if(!engine.can(CFMLEngine.CAN_UPDATE,password))
317                throw new IOException("access denied to update CFMLEngine");
318            //new RunUpdate(this).start();
319            return update();
320        }
321    
322        /**
323         * restart the cfml engine
324         * @param password
325         * @return has updated
326         * @throws IOException 
327         * @throws ServletException 
328         */
329        public boolean restart(String password) throws IOException, ServletException {
330            if(!engine.can(CFMLEngine.CAN_RESTART_ALL,password))
331                throw new IOException("access denied to restart CFMLEngine");
332            
333            return _restart();
334        }
335    
336        /**
337         * restart the cfml engine
338         * @param password
339         * @return has updated
340         * @throws IOException 
341         * @throws ServletException 
342         */
343        public boolean restart(String configId, String password) throws IOException, ServletException {
344            if(!engine.can(CFMLEngine.CAN_RESTART_CONTEXT,password))// TODO restart single context
345                throw new IOException("access denied to restart CFML Context (configId:"+configId+")");
346            
347            return _restart();
348        }
349        
350        /**
351         * restart the cfml engine
352         * @param password
353         * @return has updated
354         * @throws IOException 
355         * @throws ServletException 
356         */
357        private synchronized boolean _restart() throws ServletException {
358            engine.reset();
359            initEngine();
360            registerInstance(engine);
361            callListeners(engine);
362            System.gc(); 
363            System.gc();
364            return true;
365        }
366    
367            /**
368         * updates the engine when a update is available
369         * @return has updated
370         * @throws IOException
371         * @throws ServletException
372         */
373        private boolean update() throws IOException, ServletException {
374            
375            URL hostUrl=getEngine().getUpdateLocation();
376            if(hostUrl==null)hostUrl=new URL("http://www.getrailo.org");
377            URL infoUrl=new URL(hostUrl,"/railo/remote/version/info.cfm?ext="+getCoreExtension()+"&version="+version);// FUTURE replace with Info.cfc or better move the functionality to core if possible. something like engine.getUpdater a class provided by the core and defined (interface) by the loader.
378            
379            tlog("Check for update at "+hostUrl);
380            
381            String availableVersion = Util.toString((InputStream)infoUrl.getContent()).trim();
382            
383            if(availableVersion.length()!=9) throw new IOException("can't get update info from ["+infoUrl+"]");
384            if(!isNewerThan(Util.toInVersion(availableVersion),version)) {
385                tlog("There is no newer Version available");
386                return false;
387            }
388            
389            tlog("Found a newer Version \n - current Version "+Util.toStringVersion(version)+"\n - available Version "+availableVersion);
390            
391            URL updateUrl=new URL(hostUrl,"/railo/remote/version/update.cfm?ext="+getCoreExtension()+"&version="+availableVersion);
392            File patchDir=getPatchDirectory();
393            File newRailo=new File(patchDir,availableVersion+("."+getCoreExtension()));//isSecure?".rcs":".rc"
394            
395            if(newRailo.createNewFile()) {
396                Util.copy((InputStream)updateUrl.getContent(),new FileOutputStream(newRailo));  
397            }
398            else {
399                tlog("File for new Version already exists, won't copy new one");
400                return false;
401            }
402            try {
403            engine.reset();
404            }
405            catch(Throwable t) {
406                    t.printStackTrace();
407            }
408            
409            // Test new railo version valid
410            //FileClassLoader classLoader=new FileClassLoader(newRailo,mainClassLoader);
411            RailoClassLoader classLoader=new RailoClassLoader(newRailo,mainClassLoader);
412            //URLClassLoader classLoader=new URLClassLoader(new URL[]{newRailo.toURL()},mainClassLoader);
413            String v="";
414            try {
415                CFMLEngine e = getEngine(classLoader);
416                if(e==null)throw new IOException("can't load engine");
417                v=e.getVersion();
418                engine=e;
419                version=Util.toInVersion(v);
420                //e.reset();
421                callListeners(e);
422            }
423            catch (Exception e) {
424                classLoader=null;
425                System.gc();
426                try {
427                    newRailo.delete();
428                }
429                catch(Exception ee){}
430                tlog("There was a Problem with the new Version, can't install ("+e+":"+e.getMessage()+")");
431                e.printStackTrace();
432                return false;
433            }
434            
435            tlog("Version ("+v+")installed");
436            return true;
437        }
438        
439        
440        /**
441         * method to initalize a update of the CFML Engine.
442         * checks if there is a new Version and update it whwn a new version is available
443         * @param password
444         * @return has updated
445         * @throws IOException
446         * @throws ServletException 
447         */
448        public boolean removeUpdate(String password) throws IOException, ServletException {
449            if(!engine.can(CFMLEngine.CAN_UPDATE,password))
450                throw new IOException("access denied to update CFMLEngine");
451            return removeUpdate();
452        }
453        
454    
455        /**
456         * method to initalize a update of the CFML Engine.
457         * checks if there is a new Version and update it whwn a new version is available
458         * @param password
459         * @return has updated
460         * @throws IOException
461         * @throws ServletException 
462         */
463        public boolean removeLatestUpdate(String password) throws IOException, ServletException {
464            if(!engine.can(CFMLEngine.CAN_UPDATE,password))
465                throw new IOException("access denied to update CFMLEngine");
466            return removeLatestUpdate();
467        }
468        
469        
470        
471        /**
472         * updates the engine when a update is available
473         * @return has updated
474         * @throws IOException
475         * @throws ServletException
476         */
477        private boolean removeUpdate() throws IOException, ServletException {
478            File patchDir=getPatchDirectory();
479            File[] patches=patchDir.listFiles(new ExtensionFilter(new String[]{"railo","rc","rcs"}));
480            
481            for(int i=0;i<patches.length;i++) {
482                    if(!patches[i].delete())patches[i].deleteOnExit();
483            }
484            _restart();
485            return true;
486        }
487        
488    
489        private boolean removeLatestUpdate() throws IOException, ServletException {
490            File patchDir=getPatchDirectory();
491            File[] patches=patchDir.listFiles(new ExtensionFilter(new String[]{"."+getCoreExtension()}));
492            File patch=null;
493            for(int i=0;i<patches.length;i++) {
494                     if(patch==null || isNewerThan(Util.toInVersion(patches[i].getName()),Util.toInVersion(patch.getName()))) {
495                     patch=patches[i];
496                 }
497            }
498            if(patch!=null && !patch.delete())patch.deleteOnExit();
499            
500            _restart();
501            return true;
502        }
503        
504    
505            public String[] getInstalledPatches() throws ServletException, IOException {
506                    File patchDir=getPatchDirectory();
507            File[] patches=patchDir.listFiles(new ExtensionFilter(new String[]{"."+getCoreExtension()}));
508            
509            List<String> list=new ArrayList<String>();
510            String name;
511            int extLen=getCoreExtension().length()+1;
512            for(int i=0;i<patches.length;i++) {
513                    name=patches[i].getName();
514                    name=name.substring(0, name.length()-extLen);
515                     list.add(name);
516            }
517            String[] arr = list.toArray(new String[list.size()]);
518            Arrays.sort(arr);
519            return arr;
520            }
521        
522    
523        /**
524         * call all registred listener for update of the engine
525         * @param engine
526         */
527        private void callListeners(CFMLEngine engine) {
528            Iterator<EngineChangeListener> it = listeners.iterator();
529            while(it.hasNext()) {
530                it.next().onUpdate(engine);
531            }
532        }
533        
534    
535        private File getPatchDirectory() throws IOException {
536            File pd = new File(getResourceRoot(),"patches");
537            if(!pd.exists())pd.mkdirs();
538            return pd;
539        }
540    
541        /**
542         * return directory to railo resource root
543         * @return railo root directory
544         * @throws IOException
545         */
546        public File getResourceRoot() throws IOException {
547            if(resourceRoot==null) {
548                resourceRoot=new File(getRuningContextRoot(),"railo-server");
549                if(!resourceRoot.exists()) resourceRoot.mkdirs();
550            }
551            return resourceRoot;
552        }
553        
554        /**
555         * @return return running context root
556         * @throws IOException 
557         * @throws IOException 
558         */
559        private File getRuningContextRoot() throws IOException {
560            
561            if(railoServerRoot!=null) {
562                return railoServerRoot;
563            }
564            File dir=getClassLoaderRoot(mainClassLoader);
565            dir.mkdirs();
566            if(dir.exists() && dir.isDirectory()) return dir;
567            
568                
569               
570            throw new IOException("can't create/write to directory ["+dir+"], set \"init-param\" \"railo-server-directory\" with path to writable directory");
571        }
572        /**
573         * returns the path where the classloader is located
574         * @param cl ClassLoader
575         * @return file of the classloader root
576         */
577        public static File getClassLoaderRoot(ClassLoader cl) {
578            String path="railo/loader/engine/CFMLEngine.class";
579            URL res = cl.getResource(path);
580           
581            // get file and remove all after !
582            String strFile=null;
583                    try {
584                            strFile = URLDecoder.decode(res.getFile().trim(),"iso-8859-1");
585                    } catch (UnsupportedEncodingException e) {
586                            
587                    }
588            int index=strFile.indexOf('!');
589            if(index!=-1)strFile=strFile.substring(0,index);
590            
591            // remove path at the end
592            index=strFile.lastIndexOf(path);
593            if(index!=-1)strFile=strFile.substring(0,index);
594            
595            // remove "file:" at start and railo.jar at the end
596            if(strFile.startsWith("file:"))strFile=strFile.substring(5);
597            if(strFile.endsWith("railo.jar")) strFile=strFile.substring(0,strFile.length()-9);
598                    
599            File file=new File(strFile);
600            if(file.isFile())file=file.getParentFile();
601            
602            return file;
603        }
604    
605        /**
606         * check left value against right value
607         * @param left
608         * @param right
609         * @return returns if right is newer than left
610         */
611        private boolean isNewerThan(int left, int right) {
612            return left>right;
613        }
614      
615        /**
616         * Load CFMl Engine Implementation (railo.runtime.engine.CFMLEngineImpl) from a Classloader
617         * @param classLoader
618         * @return loaded CFML Engine
619         * @throws ClassNotFoundException 
620         * @throws NoSuchMethodException 
621         * @throws SecurityException 
622         * @throws InvocationTargetException 
623         * @throws IllegalAccessException 
624         * @throws IllegalArgumentException 
625         */
626        private CFMLEngine getEngine(ClassLoader classLoader) throws ClassNotFoundException, SecurityException, NoSuchMethodException, IllegalArgumentException, IllegalAccessException, InvocationTargetException {
627            Class clazz=classLoader.loadClass("railo.runtime.engine.CFMLEngineImpl");
628            Method m = clazz.getMethod("getInstance",new Class[]{CFMLEngineFactory.class});
629            return (CFMLEngine) m.invoke(null,new Object[]{this});
630            
631        }
632    
633        /**
634         * log info to output
635         * @param obj Object to output
636         */
637        public void tlog(Object obj) {
638            log(new Date()+ " "+obj);
639        }
640        
641        /**
642         * log info to output
643         * @param obj Object to output
644         */
645        public void log(Object obj) {
646            if(out==null){
647                    boolean isCLI=false;
648                    String str=System.getProperty("railo.cli.call");
649                    if(!Util.isEmpty(str, true)) {
650                            str=str.trim();
651                            isCLI="true".equalsIgnoreCase(str) || "yes".equalsIgnoreCase(str);
652                            
653                    }
654                    
655                    if(isCLI) {
656                            try{
657                                    File dir = new File(getResourceRoot(),"logs");
658                                    dir.mkdirs();
659                                    File file = new File(dir,"out");
660                                    
661                            file.createNewFile();
662                            out=new PrintWriter(file);
663                            }
664                            catch(Throwable t){t.printStackTrace();}
665                    }
666                    if(out==null)out=new PrintWriter(System.out);
667            }
668            out.write(""+obj+"\n");   
669            out.flush();
670        }
671        
672        private class UpdateChecker extends Thread {
673            private CFMLEngineFactory factory;
674    
675            private UpdateChecker(CFMLEngineFactory factory) {
676                this.factory=factory;
677            }
678            
679            public void run() {
680                long time=10000;
681                while(true) {
682                    try {
683                        sleep(time);
684                        time=1000*60*60*24;
685                        factory.update();
686                        
687                    } catch (Exception e) {
688                        
689                    }
690                }
691            }
692        }
693    
694    }