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("" + tag + ">");
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);}
}