All Downloads are FREE. Search and download functionalities are using the official Maven repository.

cat.inspiracio.orange.Programmer Maven / Gradle / Ivy

/*    Copyright 2019 Alexander Bunkenburg

   Licensed under the Apache License, Version 2.0 (the "License");
   you may not use this file except in compliance with the License.
   You may obtain a copy of the License at

       http://www.apache.org/licenses/LICENSE-2.0

   Unless required by applicable law or agreed to in writing, software
   distributed under the License is distributed on an "AS IS" BASIS,
   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   See the License for the specific language governing permissions and
   limitations under the License.
*/
package cat.inspiracio.orange;

import java.io.IOException;
import java.io.Writer;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.TreeSet;

import org.apache.maven.plugin.logging.Log;
import org.w3c.dom.DocumentType;
import org.w3c.dom.Element;
import org.w3c.dom.Node;

import cat.inspiracio.html.DocumentRecurser;
import cat.inspiracio.html.DocumentWriter;

/** Transforms a html document into a java class that renders it. 
 * 
 * Has state: not threadsafe. */
class Programmer extends ProgramWriter{

	// state ----------------------------------------------------
	
	private Log log;
	
	/** Package name of the class being written.
	 * Corresponds to file path. 
	 * Like "cat.inspiracio.orange.webapp.cacti" 
	 * for /cactus/cactus.html. */
	private String packageName;
	
	/** Class name of the class being written. 
	 * Corresponds to file name.
	 * Like "cactus" for /cacti/cactus.html. */
	private String className;

	/** Path of this html file, absolute in web app.
	 * Like "/index.html" or "/WEB-INF/includes/hr.html". */
	private String path;

	// construction --------------------------------------------

	Programmer(Writer w){super(w);}

	void setLog(Log l){log=l;}
	void setPath(String s){path=s;}
	void setPackage(String p){packageName=p;}
	void setClass(String c){className=c;}

	/** Generates a class for n, writing it to writer. */
	void generate(Node n) throws Exception{
		init(n);
		node(n);
		close();
	}

	/** Everything from the start of the file to the start of the body
	 * of the write method. */
	private void init(Node n) throws IOException{
		packageDeclaration();
		importStatements(n);
		startClass();
		constructor();
		startRender();
	}
	
	/** Ends the write()-method, inserts any declarations, ends the class. */
	private void close() throws IOException{
		endRender();
		declarations();
		endClass();
	}
	
	// recursers -----------------------------------------------

	Programmer packageDeclaration() throws IOException{
		write("package ").write(packageName).writeln(';');
		writeln();
		return this;
	}

	Programmer startClass() throws IOException{
		write("public class ").write(className).writeln(" extends Template{");
		writeln();
		indent();
		return this;
	}

	Programmer constructor() throws IOException{
		write("public ").write(className).writeln("(PageContext pc){super(pc);}");
		writeln();
		return this;
	}
	
	Programmer startRender() throws IOException{
		writeln("@Override public final void render() throws Exception {");
		indent();
		return this;
	}
	
	private void importStatements(final Node n) throws IOException{
		Setimports=findImports(n);
		writeImports(imports);
	}
	
	/** Finds all imported classes. */
	SetfindImports(final Node n) throws IOException{
		/* Class names for imports. Fully-qualified, alphabetic order. */
		final Setimports=new TreeSet();
		imports.add("cat.inspiracio.orange.Template");
		imports.add("jakarta.servlet.jsp.PageContext");

		/* Recurses over the element to find all class-imports. */
		try{
			new DocumentRecurser(){

				{
					node(n);
				}

				@Override protected DocumentRecurser open(Element e){
					if(e.hasAttribute("data-import")){
						String a=e.getAttribute("data-import");
						String[]is=a.split(",");
						for(String i : is){
							i=i.trim();
							if(!empty(i))
								imports.add(i);
						}
						e.removeAttribute("data-import");
					}
					return this;
				}

			};
		}catch(Exception e){throw new IOException(e);}
		return imports;
	}

	/** Writes imports in alphabetic order. */
	Programmer writeImports(Setimports) throws IOException{
		for(String i : imports)
			writeln("import " + i + ";");
		writeln();
		return this;
	}

	@Override protected Programmer doctype(DocumentType type)throws IOException{
		ww("");
		return this;
	}

    /** Check for data-substitute. */
    @Override protected Programmer element(Element e) throws Exception{

    	if(e.hasAttribute("data-if"))
    		return dataIf(e);
    	
    	if(e.hasAttribute("data-for"))
    		return dataFor(e);
    	
    	if(e.hasAttribute("data-substitute"))
    		return dataSubstitute(e);
    	
    	if(e.hasAttribute("data-while"))
    		return dataWhile(e);
    	
		super.element(e);
		return this;
    }

    /** Renders an element with data-if attribute.
     * Generates an if-statement. */
    private Programmer dataIf(Element e) throws Exception{
    	String v=e.getAttribute("data-if");
		e.removeAttribute("data-if");
    	log.info("data-if = " + v);
		writeln("if(" + v + "){").indent();
		element(e).outdent().writeln('}');
		return this;
	}
    
    /** Renders an element with data-for attribute.
     * Generates a for-loop. */    
    private Programmer dataFor(Element e) throws Exception{
    	String v=e.getAttribute("data-for");
		e.removeAttribute("data-for");
		log.info("data-for = " + v);
		writeln("for(" + v + "){").indent();
		element(e).outdent().writeln('}');
		return this;
	}
    
    /** Renders an element with data-while attribute.
     * Generates a while-loop. */    
    private Programmer dataWhile(Element e) throws Exception{
    	String v=e.getAttribute("data-while");
		e.removeAttribute("data-while");
		log.info("data-while = " + v);
		writeln("while(" + v + "){").indent();
		element(e).outdent().writeln('}');
		return this;
	}
    
    /** Renders an element with data-substitute attribute. 
     * Generates a call to another class. */
    private Programmer dataSubstitute(Element e) throws IOException{
    	String v = e.getAttribute("data-substitute");
		log.info("data-substitute = " + v);

		// path = my path inside webapp, starts with /
		// v = absolute or relative to path
		Resolver resolver=new Resolver();
		String className = resolver.fqcn(path, v);
		write("new ").write(className).writeln("(pageContext).render();");
		return this;
	}
    
    /** Writes opening tag and the attributes.
	 * If the element has no child nodes, the final ">" of the opening tag
	 * is not written, so that close(e) can write "/>" --- unless the element
	 * is one of the few elements that need a separate closing tag even if they
	 * have no children. */
	@Override protected Programmer open(Element e) throws Exception {
		String tag=e.getTagName();
		boolean b=e.hasChildNodes() || needClosingTag(tag);
		
		if(0==e.getAttributes().getLength()){
			if(b)
				ww("<" + tag + ">");
			else
				ww("<" + tag);
		}
		
		else{
			ww("<" + tag);
			attributes(e);
			if(b)
				ww(">");
		}
		return this;
	}

	/** Writes " key=\"value\". 
	 * 
	 * In value, " is escaped to "&". 
	 * 
	 * If the value is null or empty, writes just the key. 
	 * 
	 * Does not support escaped expressions \${E}.
	 * */
    @Override protected Programmer attribute(String key, String value) throws Exception {
        //no value --- normal case
    	if(empty(value))
            return (Programmer)ww(" " + key);
        
        //value is literally "true" or "false" --- degenerate case
    	if("true".equals(value))
    		return (Programmer)ww(" " + key);//or: key=key
    	
    	if("false".equals(value))
    		return this;

    	//Value has no ${E} --- usual case, must be fast and neat
    	if(!value.contains("${"))
    		return (Programmer)ww(" " + key + "=" + quote(value));//Escapes " in the value
    	
    	//Whole value is just one ${E} which may be boolean --- a normal case
    	if(isExpression(value)){
    		String E=value.substring(2, value.length()-1);
    		write("attribute(").literal(key).writeln(", " + E + ");");
			return this;
		}

    	//Value is partly literal and contains expressions --- unusual case
    	ww(" " + key + "=\"");	//opens "
    	Listparts=Part.parse(value);
    	for(Part p : parts){
        	if(p.isLiteral())
        		ww(p.getLiteral());
        	else
        		write("write(unquote(").write(p.getExpression()).writeln("));");
    	}
    	return (Programmer)ww("\"");//closes "
    }
    
    /** Is whole value just one expression, like "${karte.getGenus()}"?
     * That's a frequent case. */
    boolean isExpression(String value){
    	// I think this is not too simple.
		// Correctly false for "${x} and ${y}".
		// Correctly false for "${x} and \${y}".
    	return value!=null &&
    			3parts=Part.parse(s);
		for(Part p : parts)
			if(p.isLiteral())
				ww(escape(p.getLiteral()));
			else
				writeln("write(escape(" + p.getExpression() + "));");
		return this;
	}
	
	/** Writes an element's closing tag.
     * If the element has no children, only writes "/>" ---
     * unless the element is one of the few elements that need a separate
     * closing tag even if they have no child elements. */
	@Override protected Programmer close(Element e) throws IOException {
		String tag=e.getTagName();
		boolean b=e.hasChildNodes() || needClosingTag(tag);
		if(b)
			return (Programmer)ww("");
		else
			return (Programmer)ww("/>");//opt: for void elements like  could output ">" rather than "/>"
	}

	private Programmer declarations() throws IOException{
		if(declarations!=null)
			for(String d : declarations)
				writeln(d);
		return this;
	}

	private Programmer endRender() throws IOException{
		outdent().writeln().writeln('}').writeln();
		return this;
	}

	private Programmer endClass() throws IOException{
		outdent().writeln('}').writeln();
		return this;
	}

	/** accumulated declarations */
	private final Set declarations=new HashSet<>();
	
	/** Writes a script element.
     * 
     * Script elements are special:
     * They have no child elements except for text, 
     * and the text should not be escaped. In that way,
     * the javascript program in there can have < > &.
     * 
     * @param element Must be script and must have no children
     * other than text.
     */
	@Override protected void script(Element element) throws Exception{
		if(isDeclaration(element)){
			String source=element.getTextContent();
			declarations.add(source);
			return;
		}
		
		if(isServerScript(element)){
			//Design decision:
			//I don't enclose the script in a new block: { }
			//so that it can initialise variables that are used further down.
			String source=element.getTextContent();
			if(!empty(source))
				writeln(source);//no escaping at all
			return;
		}
		
        open(element);
        String s=element.getTextContent();//correct if precondition holds
        if(!empty(s))
        	ww(s);//no escaping for html
        close(element);
    }
	
	boolean isServerScript(Element e){
		String type=e.getAttribute("type");
		return "server/java".equals(type);
	}

	private boolean isDeclaration(Element e){
		String type=e.getAttribute("type");
		return "server/java-declarations".equals(type);
	}

	/** No output */
    @Override protected Programmer cdata(String s) throws Exception{return this;}

	/** No output */
	@Override protected Programmer comment(String s) throws Exception{return this;}
	
    /** just for easy access in tests */
	protected Programmer node(Node n) throws Exception{return (Programmer)super.node(n);}

}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy