001package com.randomnoun.common.jessop;
002
003/* (c) 2016 randomnoun. All Rights Reserved. This work is licensed under a
004 * BSD Simplified License. ( http://www.randomnoun.com/bsd-simplified.html ) 
005 */
006
007import java.io.PrintWriter;
008import java.util.regex.Matcher;
009import java.util.regex.Pattern;
010
011import javax.script.ScriptException;
012
013import org.apache.log4j.Logger;
014
015
016/** This is an abstract class that supports generic support for creating template scripts from jessop source.
017 * 
018 * <p>This class is responsible for processing jessop declarations (e.g. <code>&lt;%@ jessop language="javascript" engine="rhino" %&gt;</code>),
019 * and switching to the correct language JessopScriptBuilder implementation.
020 * 
021 * <p>Note that having multiple languages in the same script file is not yet supported by jessop. 
022 * The declaration (if it exists) should therefore only appear once, and be the first thing that appears in a jessop source file.
023 * 
024 * <p>If the declaration is missing, then the default JavascriptJessopScriptBuilder is used, using the 'rhino' engine.
025 * 
026 * @author knoxg
027 */
028// this should be subclassed by specific languages (javascript etc)
029public abstract class AbstractJessopScriptBuilder implements JessopScriptBuilder {
030
031        protected Logger logger = Logger.getLogger(AbstractJessopScriptBuilder.class);
032        protected JessopDeclarations declarations;
033        protected Tokeniser tokeniser;
034        protected PrintWriter pw;
035
036        @Override
037        public void setPrintWriter(PrintWriter pw) {
038                this.pw = pw;
039        }
040        
041        @Override
042        public void setTokeniserAndDeclarations(Tokeniser t, JessopDeclarations declarations) {
043                this.tokeniser = t;
044                this.declarations = declarations;
045        }
046        @Override
047        public JessopDeclarations getDeclarations() {
048                return declarations;
049        }
050
051        @Override
052        public void emitDeclaration(int line, String s) throws ScriptException {
053                // declType attr1="val1" attr2="val2"
054                // don't really feel like tokenising this at the moment
055                s = s.trim();
056                // can't do this in 1 regex for some reason
057                //   Pattern declPattern = Pattern.compile("([^\\s\"]+)\\s*(?:(\\S+)=\"([^\"]*)\"\\s*)*$");
058                // so breaking into subregexes
059                String declType;
060                Pattern declTypePattern = Pattern.compile("^([^\\s\"]+)");
061                Matcher m = declTypePattern.matcher(s);
062                if (m.find()) {
063                        declType = m.group(1);
064                } else {
065                        throw new ScriptException("Could not parse declaration '" + s + "'", null, line);
066                }
067                if (!declType.equals("jessop")) {
068                        logger.warn("Unknown declaration type '" + declType + "'");
069                        // just ignore unknown declarations
070                        return;
071                }
072                s = s.substring(declType.length()).trim();
073                logger.debug("s=" + s);
074                Pattern declAttrPattern = Pattern.compile("(\\S+)=\"([^\"]*)\"");
075                m = declAttrPattern.matcher(s);
076                while (m.find()) {
077                        // do something
078                        String attrName = m.group(1);
079                        String attrValue = m.group(2);
080                        if (attrName.equals("language")) {
081                                // change the JessopScriptBuilder based on the language
082                                // the registry of ScriptBuilders is kept in the EngineFactory
083                                JessopScriptEngineFactory jsf = (JessopScriptEngineFactory) tokeniser.jse.getFactory();
084                                JessopScriptBuilder newBuilder = jsf.getJessopScriptBuilderForLanguage(attrValue);
085                                newBuilder.setPrintWriter(pw);
086                                newBuilder.setTokeniserAndDeclarations(tokeniser, declarations);   // pass on tokeniser state and declarations to new jsb
087                                tokeniser.setJessopScriptBuilder(newBuilder);       // tokeniser should use this jsb from this point on
088                                // should probably wait until all attributes are parsed, but hey
089                                //if (declarations.engine==null) { 
090                                        declarations.engine = newBuilder.getDefaultScriptEngineName();
091                                        declarations.exceptionConverter = newBuilder.getDefaultExceptionConverterClassName();
092                                        declarations.bindingsConverter = newBuilder.getDefaultBindingsConverterClassName();
093                                //}
094                                
095                                /*
096                                JessopScriptBuilder newBuilder;
097                                if (attrValue.equals("javascript")) {
098                                        newBuilder = new JavascriptJessopScriptBuilder(); 
099                                        newBuilder.setPrintWriter(pw);
100                                        newBuilder.setTokeniser(tokeniser, declarations);   // pass on tokeniser state and declarations to new jsb
101                                        tokeniser.setJessopScriptBuilder(newBuilder);       // tokeniser should use this jsb from this point on
102                                        if (declarations.engine==null) { declarations.engine = "rhino"; }  // default engine for javascript
103
104                                } else if (attrValue.equals("java")) {
105                                        newBuilder = new JavaJessopScriptBuilder(); 
106                                        newBuilder.setPrintWriter(pw);
107                                        newBuilder.setTokeniser(tokeniser, declarations);   
108                                        tokeniser.setJessopScriptBuilder(newBuilder);       
109                                        if (declarations.engine==null) { declarations.engine = "beanshell"; }  // default engine for java
110
111                                } else if (attrValue.equals("lua")) {
112                                        newBuilder = new LuaJessopScriptBuilder(); 
113                                        newBuilder.setPrintWriter(pw);
114                                        newBuilder.setTokeniser(tokeniser, declarations);   
115                                        tokeniser.setJessopScriptBuilder(newBuilder);       
116                                        if (declarations.engine==null) { declarations.engine = "luaj"; }  // default engine for lua
117
118                                } else if (attrValue.equals("python") || attrValue.equals("python2")) {
119                                        newBuilder = new Python2JessopScriptBuilder(); 
120                                        newBuilder.setPrintWriter(pw);
121                                        newBuilder.setTokeniser(tokeniser, declarations);   
122                                        tokeniser.setJessopScriptBuilder(newBuilder);       
123                                        if (declarations.engine==null) { declarations.engine = "jython"; }  // default engine for lua
124
125                                } else {
126                                        throw new IllegalArgumentException("Unknown language '" + attrValue + "'");
127                                }
128                                */
129                                
130                        } else if (attrName.equals("engine")) {
131                                // if we're changing engines, this will reset the default exception converter.
132                                // we may want to keep a registry of engine names -> ExceptionConverters
133                                // at a later stage
134                                if (!attrValue.equals(declarations.getEngine())) {
135                                        declarations.setExceptionConverter(null);
136                                }
137                                declarations.setEngine(attrValue);
138                                
139                        } else if (attrName.equals("suppressEol")) {
140                                declarations.setSuppressEol(Boolean.valueOf(attrValue));
141
142                        } else if (attrName.equals("compileTarget")) {
143                                declarations.setCompileTarget(Boolean.valueOf(attrValue));
144
145                        } else if (attrName.equals("filename")) {
146                                declarations.setFilename(attrValue);
147
148                        } else if (attrName.equals("exceptionConverter")) {
149                                declarations.setExceptionConverter(attrValue);
150
151                        } else if (attrName.equals("bindingsConverter")) {
152                                declarations.setBindingsConverter(attrValue);
153
154                        }
155                        logger.debug("Found attr " + m.group(1) + "," + m.group(2));
156                }
157        }
158        
159        @Override
160        public abstract void emitText(int line, String s);
161        
162        @Override
163        public abstract void emitExpression(int line, String s);
164        
165        @Override
166        public abstract void emitScriptlet(int line, String s);
167        
168        @Override
169        public String getDefaultExceptionConverterClassName() {
170                return null;
171        }
172
173        @Override
174        public String getDefaultBindingsConverterClassName() {
175                return null;
176        }
177
178        /** Conditionally remove the first newline from the supplied string.
179         * 
180         * <p>This method is used to perform <code>suppressEol</code> declaration processing.
181         * 
182         * <p>When the <code>suppressEol</code> declaration is <code>true</code>, and the text to be emitted by the output script
183         * immediately follows a scriptlet and begins with a newline (or whitespace followed by a newline), 
184         * then we want to remove that (whitespace and) newline.
185         * 
186         * <p>If there are non-whitespace characters before the first newline, then it is not suppressed.
187         * 
188         * @param s text which is to be emitted by the output script
189         * @param suppressEol if true, remove the beginning whitespace and newline, if it exists.
190         * 
191         * @return the supplied string, with the first newline conditionally removed
192         */
193        protected String suppressEol(String s, boolean suppressEol) {
194                // ok. if s starts with a newline, 
195                // *and* suppressEol is true,
196                // *and* this text is being emitted on a line that has nothing but expressions (and whitespace), 
197                // then suppress the newline.
198                if (s.indexOf("\n")!=-1 && suppressEol) {
199                        boolean isFirstLineJustWhitespace = true;
200                        int pos = 0;
201                        while (pos<s.length() && isFirstLineJustWhitespace) {
202                                char ch = s.charAt(pos);
203                                if (ch=='\n') { pos++; break; }
204                                if (!Character.isWhitespace(ch)) { isFirstLineJustWhitespace = false; break; }
205                                pos++;
206                        }
207                        if (isFirstLineJustWhitespace) {
208                                s = s.substring(pos);
209                        }
210                }
211                return s;
212        }
213}