001package arcommon.net;
002
003import java.awt.Color;
004import java.io.BufferedWriter;
005import java.io.File;
006import java.io.IOException;
007import java.io.OutputStream;
008import java.io.OutputStreamWriter;
009import java.io.UnsupportedEncodingException;
010import java.util.ArrayList;
011import java.util.Arrays;
012import java.util.HashSet;
013import java.util.List;
014
015import arcommon.FileUtil;
016import arcommon.StringUtil;
017import arcommon.xml.XmlUtil;
018
019public class HtmlWriter {
020    
021    public static final String DEFAULT_ENCODING = "UTF-8";
022    public static final String DEFAULT_LANGUAGE = "en";
023    
024    public static final String HTTP_METHOD_GET = "GET";
025    public static final String HTTP_METHOD_PUT = "PUT";
026    public static final String HTTP_METHOD_POST = "POST";
027    
028    public static final String INPUT_TYPE_CHECKBOX = "checkbox";
029    public static final String INPUT_TYPE_DATE = "date";
030    public static final String INPUT_TYPE_DATETIME_LOCAL = "datetime-local";
031    public static final String INPUT_TYPE_FILE = "file";
032    public static final String INPUT_TYPE_HIDDEN = "hidden";
033    public static final String INPUT_TYPE_PASSWORD = "password";
034    public static final String INPUT_TYPE_NUMBER = "number";
035    public static final String INPUT_TYPE_RADIO = "radio";
036    public static final String INPUT_TYPE_SUBMIT = "submit";
037    public static final String INPUT_TYPE_TEXT = "text";
038    public static final String INPUT_TYPE_TIME = "time";
039    
040    public static final String FORM_ENCTYPE_MULTIPART = "multipart/form-data";
041    
042    protected enum HtmlMode {BEFORE_START,
043        BODY, APPLET, APPLET_AFTER_ALT, IMAGE, SOURCE,
044        TABLE, ROW, CELL,
045        CENTER, BOLD, HEADER, FONT_SIZE, STRONG, TEXT_COLOR,
046        SELECT, FORM, FIELDSET,
047        CLOSED
048    };
049    
050    private BufferedWriter outWriter;
051    
052    private String encoding;
053    private String language;
054    
055    private int indent = 0;
056    private boolean startOfLine = true;
057    private HtmlMode mode;
058    private ArrayList<HtmlMode> nestedModes;
059    private int headerActiveLevel;
060    private HashSet<String> usedId;
061    
062    public HtmlWriter(String fileName) {
063        this(fileName, null);
064    }
065    
066    public HtmlWriter(String fileName, String pencoding) {
067        this(fileName, pencoding, null);
068    }
069    
070    public HtmlWriter(String fileName, String pencoding, String planguage) {
071        encoding = pencoding;
072        if (encoding == null) {
073            encoding = DEFAULT_ENCODING;
074        }
075        language = planguage;
076        if (language == null) {
077            language = DEFAULT_LANGUAGE;
078        }
079        
080        File file = new File(fileName).getParentFile();
081        if (file != null) {
082            FileUtil.mkdirs(file);
083        }
084        
085        outWriter = FileUtil.createBufferedWriter(fileName, false, encoding);
086        mode = HtmlMode.BEFORE_START;
087        
088        usedId = new HashSet<String>();
089    }
090    
091    public HtmlWriter(OutputStream poutStream) {
092        language = DEFAULT_LANGUAGE;
093        mode = HtmlMode.BEFORE_START;
094        usedId = new HashSet<String>();
095        setEncoding(poutStream, DEFAULT_ENCODING);
096    }
097    
098    public void writeTitle(String title) {
099        writeTitleAndIcon(title, null);
100    }
101    
102    public void writeTitleAndIcon(String title, String icon) {
103        validateBeforeStart();
104        writeHtmlLine("<!DOCTYPE html>");
105        if (language == null) {
106            writeHtmlLine("<html>");
107        } else {
108            writeHtmlLine("<html lang=\"" + language + "\">");
109        }
110        writeHtmlLine("<head>");
111        if (icon != null) {
112            writeHtmlLine("<link rel=\"icon\" href=\"" + NetUtil.encodeUrlMyVariant(icon) + "\">");
113        }
114        writeHtmlLine("<title>" + title + "</title>");
115        if (encoding != null) {
116            writeHtmlLine("<meta charset=\"" + encoding + "\">");
117        }
118        writeHtmlLine("<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">");
119        writeHtmlLine("</head>");
120        writeHtmlLine("<body>");
121        mode = HtmlMode.BODY;
122    }
123
124    public void writeIFrame(String source) {
125        writeIFrame(source, null, null, null);
126    }
127    
128    public void writeIFrame(String source, int width, int height) {
129        writeIFrame(source, width, height, null);
130    }
131    
132    public void writeIFrame(String source, int width, int height, String style) {
133        writeIFrame(source, "" + width, "" + height, style);
134    }
135    
136    public void writeIFrame(String source, String width, String height, String style) {
137        // "<iframe src=\"" + link + "\" width=\"400\" height=\"500\" style=\"border:none;\"></iframe>");
138        validateNormalMode();
139        if (source == null) {
140            throw new RuntimeException("source == null");
141        }
142        
143        writeHtml("<iframe src=\"" + source + "\"");
144        if (width != null) {
145            writeHtml(" width=\"" + width + "\"");
146        }
147        if (height != null) {
148            writeHtml(" height=\"" + height + "\"");
149        }
150        if (style != null) {
151            writeHtml(" style=\"" + style + "\"");
152        }
153        
154        writeHtml("></iframe>");
155        writeNewlineInternal();
156    }
157    
158    public void writeHeader(String header, int headerLevel) {
159        writeHeader(header, headerLevel, true);
160    }
161    
162    public void writeHeader(String header, int headerLevel, String id) {
163        writeHeader(header, headerLevel, id, true);
164    }
165    
166    public void writeHeader(String header, int headerLevel, boolean center) {
167        writeHeader(header, headerLevel, null, center);
168    }
169    
170    public void writeHeader(String header, int headerLevel, String id, boolean center) {
171        startHeader(headerLevel, id, center);
172        writeText(header);
173        endHeader();
174    }
175    
176    public void writeHorizontalHeader() {
177        validateNormalMode();
178        writeHtmlLine("<hr>");
179    }
180    
181    public static class HtmlImageMap {
182        private String id;
183        private String name;
184        private List<HtmlImageArea> areas;
185        
186        public HtmlImageMap(String pname) {
187            name = pname;
188        }
189
190        public HtmlImageMap(String pid, String pname) {
191            id = pid;
192            name = pname;
193            validateId(id);
194        }
195        
196        public void add(HtmlImageArea htmlImageArea) {
197            if (areas == null) {
198                areas = new ArrayList<HtmlImageArea>();
199            }
200            areas.add(htmlImageArea);
201        }
202    }
203    
204    public static class HtmlImageArea {
205        private String shape;
206        private String coords;
207        private String href;
208        private String alt;
209        private String title;
210        private boolean openInNewWindow;
211        
212        public HtmlImageArea(String pshape, String pcoords, String phref, String palt, String ptitle, boolean popenInNewWindow) {
213            shape = pshape;
214            coords = pcoords;
215            href = phref;
216            alt = palt;
217            title = ptitle;
218            openInNewWindow = popenInNewWindow;
219        }
220    }
221    
222    public void writeImageCenter(String fileName) {
223        writeImageCenter(fileName, null);
224    }
225    
226    public void writeImageCenter(String fileName, int width, int height) {
227        writeImage(fileName, new IdAndStyle(null, null, "display", "block", "margin", "auto"), width, height, null, null);
228    }
229
230    public void writeImageCenter(String fileName, HtmlImageMap htmlImageMap) {
231        writeImage(fileName, new IdAndStyle(null, null, "display", "block", "margin", "auto"), null, null, null, htmlImageMap);
232    }
233
234    public void writeImageCenter(String fileName, int width, int height, HtmlImageMap htmlImageMap) {
235        writeImage(fileName, new IdAndStyle(null, null, "display", "block", "margin", "auto"), width, height, null, htmlImageMap);
236    }
237    
238    public void writeImage(String fileName) {
239        writeImage(fileName, null, null);
240    }
241            
242    public void writeImage(String fileName, String alt, HtmlImageMap htmlImageMap) {
243        writeImage(fileName, null, null, null, alt, htmlImageMap);
244    }
245
246    public void writeImage(String fileName, int width, int height, String alt, HtmlImageMap htmlImageMap) {
247        writeImage(fileName, null, width, height, alt, htmlImageMap);
248    }
249    
250    private String adjustLink(String url) {
251        if (url.startsWith("data:")) {
252            return url;
253        }
254        
255        url = url.replace('\\', '/');
256        url = NetUtil.encodeUrlMyVariant(url);
257        return url;
258    }
259    
260    protected void writeImage(String fileName, IdAndStyle idAndStyle, Integer width, Integer height, String alt, HtmlImageMap htmlImageMap) {
261        String attributesString = "";
262        if (idAndStyle != null) {
263            attributesString += idAndStyle.toString();
264        }
265        if (width != null) {
266            if (width.intValue() <= 0) {
267                throw new RuntimeException("width.intValue() <= 0    " + width.intValue());
268            }
269            attributesString += " width=\"" + width + "\"";
270        }
271        if (height != null) {
272            if (height.intValue() <= 0) {
273                throw new RuntimeException("height.intValue() <= 0    " + height.intValue());
274            }
275            attributesString += " height=\"" + height + "\"";
276        }
277        if (alt != null && alt.length() > 0) {
278            attributesString += " alt=\"" + alt + "\"";
279        }
280        if (htmlImageMap != null) {
281            attributesString += " usemap=\"#" + htmlImageMap.name + "\"";
282        }
283        fileName = adjustLink(fileName);
284        writeHtml("<img" + attributesString + " src=\"" + fileName + "\" />");
285        writeNewlineInternal();
286        
287        if (htmlImageMap != null) {
288            writeHtml("<map" + idToStringWithValidation(htmlImageMap.id) + " name=\"" + htmlImageMap.name + "\">");
289            writeNewlineInternal();
290            for (HtmlImageArea htmlImageArea : htmlImageMap.areas) {
291                String href = htmlImageArea.href;
292                if (href != null) {
293                    href = adjustLink(href);
294                }
295                writeHtml("<area shape=\"" + htmlImageArea.shape + "\""
296                        + " coords=\"" + htmlImageArea.coords + "\""
297                        + (htmlImageArea.openInNewWindow ? " target=\"_blank\"" : "")
298                        + (href != null ? " href=\"" + href + "\"" : "")
299                        + " alt=\"" + htmlImageArea.alt + "\""
300                        + " title=\"" + htmlImageArea.title + "\" />");
301                writeNewlineInternal();
302            }
303            writeHtml("</map>");
304            writeNewlineInternal();
305        }
306    }
307    
308    public static class HtmlMediaSource {
309        private String src;
310        private String type;
311
312        public HtmlMediaSource(String psrc, String ptype) {
313            src = psrc;
314            type = ptype;
315            if (src == null || src.length() == 0) {
316                throw new RuntimeException("src == null || src.length() == 0");
317            }
318            if (type == null || type.length() == 0) {
319                throw new RuntimeException("type == null || type.length() == 0");
320            }
321        }
322        
323        @Override
324        public String toString() {
325            return "<source src=\"" + src + "\" type=\"" + type + "\" />";
326        }
327    }
328    
329    public void writeVideo(int width, int height, boolean autoplay, boolean controls, boolean muted, boolean loop, String preload, HtmlMediaSource... htmlMediaSources) {
330        if (htmlMediaSources.length == 0) {
331            throw new RuntimeException("HtmlMediaSource(s) must be specified (htmlMediaSources.length == 0)");
332        }
333        writeVideo(width, height, autoplay, controls, muted, loop, null, null, preload, htmlMediaSources);
334    }
335    
336    public void writeVideo(int width, int height, boolean autoplay, boolean controls, boolean muted, boolean loop, String src, String alt, String preload, HtmlMediaSource... htmlMediaSources) {
337        writeMedia("video", null, width, height, autoplay, controls, muted, loop, src, alt, preload, htmlMediaSources);
338    }
339    
340    public void writeVideo(IdAndStyle idAndStyle, int width, int height, boolean autoplay, boolean controls, boolean muted, boolean loop, String preload, HtmlMediaSource... htmlMediaSources) {
341        if (htmlMediaSources.length == 0) {
342            throw new RuntimeException("HtmlMediaSource(s) must be specified (htmlMediaSources.length == 0)");
343        }
344        writeVideo(idAndStyle, width, height, autoplay, controls, muted, loop, null, null, preload, htmlMediaSources);
345    }
346    
347    public void writeVideo(IdAndStyle idAndStyle, int width, int height, boolean autoplay, boolean controls, boolean muted, boolean loop, String src, String alt, String preload, HtmlMediaSource... htmlMediaSources) {
348        writeMedia("video", idAndStyle, width, height, autoplay, controls, muted, loop, src, alt, preload, htmlMediaSources);
349    }
350    
351    public void writeAudio(boolean autoplay, boolean controls, boolean muted, boolean loop, String preload, HtmlMediaSource... htmlMediaSources) {
352        if (htmlMediaSources.length == 0) {
353            throw new RuntimeException("HtmlMediaSource(s) must be specified (htmlMediaSources.length == 0)");
354        }
355        writeAudio(autoplay, controls, muted, loop, null, null, preload, htmlMediaSources);
356    }
357    
358    public void writeAudio(boolean autoplay, boolean controls, boolean muted, boolean loop, String src, String alt, String preload, HtmlMediaSource... htmlMediaSources) {
359        writeMedia("audio", null, -1, -1, autoplay, controls, muted, loop, src, alt, preload, htmlMediaSources);
360    }
361
362    public void writeAudio(IdAndStyle idAndStyle, boolean autoplay, boolean controls, boolean muted, boolean loop, String preload, HtmlMediaSource... htmlMediaSources) {
363        if (htmlMediaSources.length == 0) {
364            throw new RuntimeException("HtmlMediaSource(s) must be specified (htmlMediaSources.length == 0)");
365        }
366        writeAudio(idAndStyle, autoplay, controls, muted, loop, null, null, preload, htmlMediaSources);
367    }
368    
369    public void writeAudio(IdAndStyle idAndStyle, boolean autoplay, boolean controls, boolean muted, boolean loop, String src, String alt, String preload, HtmlMediaSource... htmlMediaSources) {
370        writeMedia("audio", idAndStyle, -1, -1, autoplay, controls, muted, loop, src, alt, preload, htmlMediaSources);
371    }
372    
373    private void writeMedia(String tagName, IdAndStyle idAndStyle, int width, int height, boolean autoplay, boolean controls, boolean muted, boolean loop, String src, String alt, String preload, HtmlMediaSource... htmlMediaSources) {
374        if (htmlMediaSources.length == 0 && src == null) {
375            throw new RuntimeException("htmlMediaSources.length == 0 && src == null");
376        }
377        writeHtml("<" + tagName);
378        writeIdAndStyle(idAndStyle);
379        if (width > 0) {
380            writeHtml(" width=\"" + width + "\"");
381        }
382        if (height > 0) {
383            writeHtml(" height=\"" + height + "\"");
384        }
385        if (autoplay) {
386            writeHtml(" autoplay");
387        }
388        if (controls) {
389            writeHtml(" controls");
390        }
391        if (muted) {
392            writeHtml(" muted");
393        }
394        if (loop) {
395            writeHtml(" loop");
396        }
397        if (preload != null) {
398            if (!preload.equals("auto") && !preload.equals("metadata") && !preload.equals("none")) {
399                throw new RuntimeException("Invalid preload: \"" + preload + "\". Must be one of \"auto\", \"metadata\" or \"none\".");
400            }
401            writeHtml(" preload=\"" + preload + "\"");
402        }
403        if (src != null) {
404            writeHtml(" src=\"" + src + "\"");
405        }
406        
407/*      Breaks stuff, odd
408        if (htmlMediaSources.length == 0 && alt == null) {
409            writeHtml(" />");
410            writeNewlineInternal();
411            return ;
412        }*/
413        
414        writeHtml(">");
415        writeNewlineInternal();
416        
417        levelUp(HtmlMode.SOURCE);
418        for (HtmlMediaSource htmlMediaSource : htmlMediaSources) {
419            if (htmlMediaSource == null) {
420                throw new RuntimeException("htmlMediaSource == null " + Arrays.toString(htmlMediaSources));
421            }
422            writeHtml("<source src=\"" + htmlMediaSource.src + "\" type=\"" + htmlMediaSource.type + "\" />");
423            writeNewlineInternal();
424        }
425        levelDown(HtmlMode.SOURCE);
426        
427        if (alt != null) {
428            writeText(alt);
429            writeNewlineInternal();
430        }
431        
432        writeHtml("</" + tagName + ">");
433        writeNewlineInternal();
434    }
435        
436    public void startTable() {
437        startTable(false, false);
438    }
439    
440    public void startTable(boolean border, boolean width100) {
441        startTable(border, width100, null);
442    }
443    
444    public void startTable(boolean border, boolean width100, String backGroundColor) {
445        validateNormalMode();
446        validateModeNotInHierarki(HtmlMode.HEADER);
447        String backGroundColorString = "";
448        if (backGroundColor != null) {
449            backGroundColorString = " bgcolor=\"" + backGroundColor + "\"";
450        }
451        if (border) {
452            if (width100) {
453                writeHtmlLine("<table border=\"1\" cellpadding=\"3\" cellspacing=\"0\" width=\"100%\"" + backGroundColorString + ">");
454            } else {
455                writeHtmlLine("<table border=\"1\" cellpadding=\"3\" cellspacing=\"0\"" + backGroundColorString + ">");
456            }
457        } else {
458            if (width100) {
459                writeHtmlLine("<table width=\"100%\"" + backGroundColorString + ">");
460            } else {
461                writeHtmlLine("<table" + backGroundColorString + ">");
462            }
463        }
464        levelUp(HtmlMode.TABLE, false);
465    }
466
467    public void endTable() {
468        if (mode == HtmlMode.CELL) {
469            endCell();
470        }
471        if (mode == HtmlMode.ROW) {
472            endRow();
473        }
474        levelDown(HtmlMode.TABLE);
475        writeHtmlLine("</table>");
476    }
477    
478    public void startRow() {
479        if (mode != HtmlMode.TABLE) {
480            throw new RuntimeException("mode != HtmlMode.TABLE " + mode);
481        }
482        writeHtmlLine("<tr>");
483        levelUp(HtmlMode.ROW, false);
484    }
485    
486    public void endRow() {
487        levelDown(HtmlMode.ROW);
488        writeHtmlLine("</tr>");
489    }
490    
491    public void nextRow() {
492        nextRow(null, 0, false);
493    }
494
495    public void nextRow(String width, int colspan, boolean noWrap) {
496        nextRow(width, colspan, noWrap, null);
497    }
498    
499    public void nextRow(String width, int colspan, boolean noWrap, String align) {
500        if (mode == HtmlMode.CELL) {
501            endCell();
502        }
503        if (mode == HtmlMode.ROW) {
504            endRow();
505        }
506        startRow();
507        startCell(width, colspan, noWrap, align);
508    }
509    
510    public void writeCell(String content) {
511        writeCell(content, null, 0, false);
512    }
513    
514    public void writeCell(String content, String width, int colspan, boolean noWrap) {
515        writeCell(content, width, colspan, noWrap, null);
516    }
517
518    public void writeCell(String content, String width, int colspan, boolean noWrap, String align) {
519        writeCell(content, null, width, colspan, noWrap, align);
520    }
521    
522    public void writeCell(String content, IdAndStyle idAndStyle, String width, int colspan, boolean noWrap, String align) {
523        if (content == null || content == "") {
524            writeEmptyCell(width, colspan, noWrap, align);
525            return ;
526        }
527        if (mode != HtmlMode.CELL) {
528            startCell(idAndStyle, width, colspan, noWrap, align);
529        } else {
530            if (width != null || (colspan != 1 && colspan != 0) || align != null) {
531                throw new RuntimeException("mode == HtmlMode.CELL and width/colspan/align set (width != null || (colspan != 1 && colspan != 0) || align != null)" + width + " " + colspan + "   " + align);
532            }
533        }
534        writeTextWithNewlines(content);
535        endCell();
536    }
537    
538    public void writeEmptyCell() {
539        writeEmptyCell(null, 0, false);
540    }
541    
542    public void writeEmptyCell(String width, int colspan, boolean noWrap) {
543        writeEmptyCell(width, colspan, noWrap, null);
544    }
545    
546    public void writeEmptyCell(String width, int colspan, boolean noWrap, String align) {
547        if (mode != HtmlMode.CELL) {
548            startCell(width, colspan, noWrap, align);
549        } else {
550            if (width != null || (colspan != 1 && colspan != 0) || align != null) {
551                throw new RuntimeException("mode == HtmlMode.CELL and width/colspan/align set (width != null || (colspan != 1 && colspan != 0) || align != null)" + width + " " + colspan + "   " + align);
552            }
553        }
554        writeHtmlLine("&nbsp");
555        endCell();
556    }
557    
558    public void startCell() {
559        startCell(null, 0, false);
560    }
561    
562    public void startCell(String width, int colspan, boolean noWrap) {
563        startCell(width, colspan, noWrap, null);
564    }
565    
566    public void startCell(String width, int colspan, boolean noWrap, String align) {
567        startCell(null, width, colspan, noWrap, null);
568    }
569    
570    public void startCell(IdAndStyle idAndStyle, String width, int colspan, boolean noWrap, String align) {
571        if (mode == HtmlMode.TABLE) {
572            startRow();
573        }
574        if (mode != HtmlMode.ROW) {
575            throw new RuntimeException("mode != HtmlMode.ROW " + mode);
576        }
577        writeEnsureNewline();
578        
579        String text = "<td";
580        
581        if (width != null) {
582            String widthNumberPart = width;
583            if (width.endsWith("%")) {
584                widthNumberPart = width.substring(0, width.length() - 1);
585            }
586            if (width.endsWith("px")) {
587                widthNumberPart = width.substring(0, width.length() - 2);
588            }
589            if (!StringUtil.isStringAnInt(widthNumberPart)) {
590                throw new RuntimeException("Width is not a number \"" + width + "\" (" + widthNumberPart + ")");
591            }
592            int widthNumber = StringUtil.stringToInt(widthNumberPart);
593            if (width.endsWith("%")) {
594                if (widthNumber <= 0 || widthNumber > 100) {
595                    throw new RuntimeException("widthNumber <= 0 || widthNumber > 100 \"" + width + "\"");
596                }
597            } else {
598                if (widthNumber <= 0) {
599                    throw new RuntimeException("widthNumber <= 0 \"" + width + "\"");
600                }
601            }
602            text += " width=\"" + width + "\"";
603        }
604        
605        if (align != null) {
606            if (!align.equals("right") && !align.equals("center") && !align.equals("left")) {
607                throw new RuntimeException("Invalid align \"" + align + "\"");
608            }
609            text += " align=\"" + align + "\"";
610        }
611        
612        if (colspan > 1) {
613            text += " colspan=" + colspan;
614        }
615        
616        if (idAndStyle != null) {
617            text += idAndStyle.toString();
618        }
619        
620        if (noWrap) {
621            text += " nowrap";
622        }
623        text = text + ">";
624        writeHtmlLine(text);
625        levelUp(HtmlMode.CELL, false);
626    }
627    
628    public void endCell() {
629        levelDown(HtmlMode.CELL);
630        writeEnsureNewline();
631        writeHtmlLine("</td>");
632    }
633    
634    public void nextCell() {
635        nextCell(null, 0, false);
636    }
637    
638    public void nextCell(String width, int colspan, boolean noWrap) {
639        nextCell(width, colspan, noWrap, null);
640    }
641    
642    public void nextCell(String width, int colspan, boolean noWrap, String align) {
643        if (mode == HtmlMode.CELL) {
644            endCell();
645        }
646        startCell(width, colspan, noWrap, align);
647    }
648        
649    public void startCenter() {
650        validateNormalMode();
651        writeHtml("<center>");
652        levelUp(HtmlMode.CENTER, false);
653    }
654    
655    public void endCenter() {
656        levelDown(HtmlMode.CENTER);
657        writeHtml("</center>");
658    }
659    
660    public void startBold() {
661        validateNormalMode();
662        writeHtml("<b>");
663        levelUp(HtmlMode.BOLD);
664    }
665    
666    public void endBold() {
667        levelDown(HtmlMode.BOLD);
668        writeHtml("</b>");
669    }
670
671    public void startHeader(int headerLevel, String id, boolean center) {
672        validateNormalMode();
673        validateModeNotInHierarki(HtmlMode.CENTER);
674        if (headerLevel <= 0) {
675            throw new RuntimeException("header <= 0");
676        }
677        if (headerActiveLevel > 0) {
678            throw new RuntimeException("headerActiveLevel > 0");
679        }
680        writeHtml("<h" + headerLevel + idToStringWithValidation(id) + (center ? " align=\"center\"" : "") + ">");
681        headerActiveLevel = headerLevel;
682        levelUp(HtmlMode.HEADER);
683    }
684
685    public void endHeader() {
686        if (headerActiveLevel == 0) {
687            throw new RuntimeException("headerActiveLevel == 0");
688        }
689        levelDown(HtmlMode.HEADER);
690        writeHtml("</h" + headerActiveLevel + ">");
691        writeNewlineInternal();
692        headerActiveLevel = 0;
693    }
694    
695    public void startFontSize(int size) {
696        validateNormalMode();
697        if (size <= 0) {
698            throw new RuntimeException("size <= 0 " + size);
699        }
700        writeHtml("<span style=\"font-size: " + size + "px\">");
701        levelUp(HtmlMode.FONT_SIZE);
702    }
703
704    public void endFontSize() {
705        levelDown(HtmlMode.FONT_SIZE);
706        writeHtml("</span>");
707    }
708    
709    public void startTextColor(Color color) {
710        validateNormalMode();
711        if (color == null) {
712            throw new RuntimeException("color == null");
713        }
714        String colorString = "#";
715        colorString += StringUtil.toHex(color.getRed(), 2);
716        colorString += StringUtil.toHex(color.getGreen(), 2);
717        colorString += StringUtil.toHex(color.getBlue(), 2);
718        writeHtml("<span style=\"color: " + colorString + "\">");
719        levelUp(HtmlMode.TEXT_COLOR);
720    }
721
722    public void endTextColor() {
723        levelDown(HtmlMode.TEXT_COLOR);
724        writeHtml("</span>");
725    }
726    
727    public void startStrong() {
728        validateNormalMode();
729        writeHtml("<strong>");
730        levelUp(HtmlMode.STRONG);
731    }
732    
733    public void endStrong() {
734        levelDown(HtmlMode.STRONG);
735        writeHtml("</strong>");
736    }
737    
738    public void startFieldset(String legend) {
739        validateNormalMode();
740        validateModeNotInHierarki(HtmlMode.HEADER);
741        writeHtml("<fieldset>");
742        writeNewlineInternal();
743        levelUp(HtmlMode.FIELDSET);
744        if (legend != null) {
745            writeHtmlLine("<legend>" + legend + "</legend>");
746        }
747    }
748    
749    public void endFieldset() {
750        levelDown(HtmlMode.FIELDSET);
751        writeHtml("</fieldset>");
752    }
753    
754    protected void startForm(String action, String method) {
755        startForm(action, method, null);
756    }
757    
758    protected void startForm(String id, String action, String method) {
759        startForm(null, action, method, null);
760    }
761    
762    protected void startForm(String id, String action, String method, String enctype) {
763        validateNormalMode();
764        validateModeNotInHierarki(HtmlMode.HEADER);
765        writeHtml("<form" + idToStringWithValidation(id) + " action=\"" + action + "\" method=\"" + method + "\"" + (enctype != null && enctype.length() > 0 ? " enctype=\"" + enctype + "\"" : "") + ">");
766        writeNewlineInternal();
767        levelUp(HtmlMode.FORM);
768    }
769    
770    protected void endForm() {
771        levelDown(HtmlMode.FORM);
772        writeHtml("</form>");
773        writeNewlineInternal();
774    }
775    
776    public void writeLink(String link, String text) {
777        writeLink(link, text, null, null);
778    }
779    
780    public void writeLinkNewWindow(String link, String text, String title) {
781        writeLink(link, text, "_blank", title);
782    }
783    
784    public void writeLink(String link, String text, String target, String title) {
785        validateNormalMode();
786        String targetString = "";
787        if (target != null && !target.equals("")) {
788            targetString = " target=\"" + target + "\"";
789        }
790        String titleString = "";
791        if (title != null && !title.equals("")) {
792            titleString = " title=\"" + XmlUtil.encodeXmlString(title) + "\"";
793        }
794        link = adjustLink(link);
795        writeHtml("<a href=\"" + link + "\"" + targetString + titleString + ">");
796        if (text == null) {
797            text = link;
798        }
799        String[] split = text.split(" ");
800        for (int i = 0; i < split.length; i++) {
801            if (i > 0) {
802                writeNbsp();
803            }
804            writeText(split[i]);
805        }
806        writeHtml("</a>");
807    }
808    
809    public void writeImageLink(String link, String imageSrc) {
810        writeImageLink(link, imageSrc, null, null);
811    }
812    
813    public void writeImageLinkNewWindow(String link, String imageSrc, String title) {
814        writeImageLink(link, imageSrc, "_blank", title);
815    }
816    
817    public void writeImageLink(String link, String imageSrc, String target, String title) {
818        writeImageLink(link, imageSrc, target, title, null, null);
819    }
820    
821    public void writeImageLink(String link, String imageSrc, String target, String title, Integer width, Integer height) {
822        validateNormalMode();
823        String targetString = "";
824        if (target != null && !target.equals("")) {
825            targetString = " target=\"" + target + "\"";
826        }
827        String titleString = "";
828        if (title != null && !title.equals("")) {
829            titleString = " title=\"" + XmlUtil.encodeXmlString(title) + "\"";
830        }
831        link = adjustLink(link);
832        writeHtml("<a href=\"" + link + "\"" + targetString + titleString + ">");
833        levelUp(HtmlMode.IMAGE);
834        writeNewlineInternal();
835        writeImage(imageSrc, null, width, height, title, null);
836        levelDown(HtmlMode.IMAGE);
837        writeHtml("</a>");
838    }
839    
840    public void writeBreakLine() {
841        validateNormalMode();
842        validateModeNotInHierarki(HtmlMode.HEADER);
843        writeHtml("<br>");
844        writeNewlineInternal();
845    }
846    
847    public void writeNbsp() {
848        writeHtml("&nbsp;");
849    }
850    
851    public void startAppletTag(String code, int width, int height) {
852        startAppletTag(code, width, height, null);
853    }
854
855    public void startAppletTag(String code, int width, int height, String codebase) {
856        validateNormalMode();
857        validateModeNotInHierarki(HtmlMode.HEADER);
858        String codebaseString = "";
859        if (codebase != null && !codebase.equals("")) {
860            codebaseString = " codebase=\"" + codebase + "\"";
861        }
862        writeHtmlLine("<APPLET" + codebaseString + " CODE=\"" + code + "\" WIDTH=" + width + " HEIGHT=" + height + ">");
863        levelUp(HtmlMode.APPLET);
864    }
865    
866    public void writeAppletParam(String name, String value) {
867        if (mode != HtmlMode.APPLET) {
868            throw new RuntimeException("mode != HtmlMode.APPLET " + mode);
869        }
870        writeHtmlLine("<param name=\"" + name + "\" value=\"" + value + "\">");
871    }
872
873    public void writeAppletAlt() {
874        writeAppletAlt("Your browser understands the &lt;APPLET&gt; tag but isn't running the applet, for some reason.", "Your browser is ignoring the &lt;APPLET&gt; tag!");
875    }
876    
877    public void writeAppletAlt(String altText, String text) {
878        if (mode != HtmlMode.APPLET) {
879            throw new RuntimeException("mode != HtmlMode.APPLET " + mode);
880        }
881        writeHtml("ALT=\"");
882        writeText(altText);
883        writeHtml("\"");
884        writeNewlineInternal();
885        writeTextLine(text);
886        mode = HtmlMode.APPLET_AFTER_ALT;
887    }
888    
889    public void endAppletTag() {
890        if (mode != HtmlMode.APPLET && mode != HtmlMode.APPLET_AFTER_ALT) {
891            throw new RuntimeException("mode != HtmlMode.APPLET && mode != HtmlMode.APPLET_AFTER_ALT " + mode);
892        }
893        levelDown();
894        writeHtmlLine("</APPLET>");
895    }
896    
897    private void writeEnsureNewline() {
898        if (!startOfLine) {
899            writeNewlineInternal();
900        }
901    }
902    
903    public void writeTextWithNewlines(String text) {
904        writeTextWithNewlines(text, 0);
905    }
906    
907    public void writeTextWithNewlines(String text, int indentCount) {
908        String[] split = text.split("\n");
909        for (int i = 0; i < split.length; i++) {
910            if (i > 0) {
911                writeBreakLine();
912                for (int j = 0; j < indentCount; j++) {
913                    writeNbsp();
914                }
915            }
916            writeText(split[i]);
917        }
918    }
919
920    public void writeTextLine(String line) {
921        writeText(line);
922        writeNewlineInternal();
923    }
924    
925    /**
926     * Writes the text with escaping / XML encoding.
927     * <br>
928     * E.g. "&gt;" is changed to "&amp;gt;"<br>
929     * E.g. "&amp;gt;" is changed to "&amp;amp;gt;"
930     * @see #writeHtml(String)
931     */
932    public void writeText(String text) {
933        validateNormalMode();
934        String html = XmlUtil.encodeXmlString(text);
935        html = html.replace("&apos;", "&#39;"); // &apos; doesn't work in old HTML and in Internet Explorer
936        writeHtml(html);
937    }
938    
939    public void writeHtmlLine(String line) {
940        writeHtml(line);
941        writeNewlineInternal();
942    }
943
944    /**
945     * Writes the HTML as is e.g. no escaping / XML encoding.
946     * <br>
947     * E.g. "&gt;" is not changed
948     * @see #writeText(String)
949     */
950    public void writeHtml(String html) {
951        if (startOfLine && indent > 0) {
952            String indentString = StringUtil.stringWithNumberOfChar(indent * 2, ' ');
953            writeString(indentString);
954        }
955        startOfLine = false;
956        writeString(html);
957    }
958    
959    protected void writeNewlineInternal() {
960        writeString("\n");
961        startOfLine = true;
962    }
963    
964    protected void writeString(String text) {
965        try {
966            outWriter.write(text);
967        } catch (Exception e) {
968            throw new RuntimeException(e);
969        }
970    }
971    
972    protected void forceClose() {
973        try {
974            outWriter.close();
975            mode = HtmlMode.CLOSED;
976        } catch (IOException e) {
977            throw new RuntimeException(e);
978        }
979    }
980    
981    public boolean isEmpty() {
982        return mode == HtmlMode.BEFORE_START;
983    }
984
985    public boolean isOpenAndNotNestedTags() {
986        return mode == HtmlMode.BODY;
987    }
988    
989    public boolean isClosed() {
990        return mode == HtmlMode.CLOSED;
991    }
992    
993    public void close() {
994        try {
995            if (isEmpty()) {
996                if (isClosed()) {
997                    throw new RuntimeException("isClosed()");
998                }
999            } else {
1000                if (!isOpenAndNotNestedTags()) {
1001                    throw new RuntimeException("!isOpenAndNotNestedTags() " + mode);
1002                }
1003                writeNewlineInternal();
1004                if (mode == HtmlMode.BODY) {
1005                    writeHtmlLine("</body>");
1006                }
1007                writeHtmlLine("</html>");
1008            }
1009            outWriter.close();
1010            mode = HtmlMode.CLOSED;
1011        } catch (Exception e) {
1012            throw new RuntimeException(e);
1013        }
1014    }
1015    
1016    public String getLanguage() {
1017        return language;
1018    }
1019    
1020    public void setLanguage(String planguage) {
1021        validateBeforeStart();
1022        if (planguage == "dk") {
1023            throw new RuntimeException("planguage == \"dk\". Should be \"da\"");
1024        }
1025        language = planguage;
1026    }
1027    
1028    public String getEncoding() {
1029        return encoding;
1030    }
1031    
1032    protected void setEncoding(OutputStream poutStream, String pencoding) {
1033        validateBeforeStart();
1034        encoding = pencoding;
1035        try {
1036            outWriter = new BufferedWriter(new OutputStreamWriter(poutStream, encoding));
1037        } catch (UnsupportedEncodingException e) {
1038            throw new RuntimeException(e);
1039        }
1040    }
1041    
1042    protected void levelUp(HtmlMode pmode) {
1043        levelUp(pmode, true);
1044    }
1045    
1046    private void levelUp(HtmlMode pmode, boolean checkForUniqueness) {
1047        storeCurrentMode();
1048        if (checkForUniqueness) {
1049            checkNestedModeForUniqueness(pmode);
1050        }
1051        if (mode == pmode) {
1052            throw new RuntimeException("Mode nested in same mode (mode == pmode) " + mode + " " + pmode);
1053        }
1054        mode = pmode;
1055        indent++;
1056    }
1057    
1058    protected void levelDown() {
1059        levelDown(null);
1060    }
1061    
1062    protected void levelDown(HtmlMode pmode) {
1063        if (pmode != null && mode != pmode) {
1064            throw new RuntimeException("mode != HtmlMode." + pmode + ", mode: HtmlMode." + mode);
1065        }
1066        restoredMode();
1067        indent--;
1068    }
1069    
1070    protected void validateNormalMode() {
1071        if (mode != HtmlMode.BODY && mode != HtmlMode.CELL && mode != HtmlMode.CENTER && mode != HtmlMode.BOLD && mode != HtmlMode.HEADER && mode != HtmlMode.FONT_SIZE && mode != HtmlMode.STRONG && mode != HtmlMode.FIELDSET && mode != HtmlMode.FORM && mode != HtmlMode.TEXT_COLOR) {
1072            throw new RuntimeException("mode != HtmlMode.BODY && mode != HtmlMode.NO_FRAME && mode != HtmlMode.CELL && mode != HtmlMode.CENTER && mode != HtmlMode.BOLD && mode != HtmlMode.HEADER && mode != HtmlMode.FONT && mode != HtmlMode.STRONG && mode != HtmlMode.FIELDSET && mode != HtmlMode.FORM && mode != HtmlMode.COLOR " + mode);
1073        }
1074    }
1075    
1076    protected void validateBeforeStart() {
1077        if (mode != HtmlMode.BEFORE_START) {
1078            throw new RuntimeException("mode != HtmlMode.BEFORE_START " + mode);
1079        }
1080    }
1081    
1082    protected void validateModeNotInHierarki(HtmlMode searchMode) {
1083        if (inHierarki(searchMode)) {
1084            if (searchMode == HtmlMode.FORM) {
1085                throw new RuntimeException("Already in form");
1086            }
1087
1088            throw new RuntimeException("Mode HtmlMode." + searchMode + " is in hierarki (mode: " + mode + ", nestedModes: " + nestedModes + ")");
1089        }
1090    }
1091    
1092    protected void validateInForm() {
1093        validateNormalMode();
1094        if (!inHierarki(HtmlMode.FORM)) {
1095            throw new RuntimeException("Not in form (" + mode + " " + nestedModes + ")");
1096        }
1097    }
1098    
1099    protected void writeIdAndStyle(IdAndStyle idAndStyle) {
1100        if (idAndStyle == null) {
1101            return ;
1102        }
1103        
1104        validateAndAddId(idAndStyle.getId());
1105        
1106        writeHtml(idAndStyle.toString());
1107    }
1108    
1109    protected void writeId(String id) {
1110        if (id == null) {
1111            return ;
1112        }
1113
1114        writeHtml(idToStringWithValidation(id));
1115    }
1116    
1117    public String idToStringWithValidation(String id) {
1118        if (id == null) {
1119            return "";
1120        }
1121
1122        validateAndAddId(id);
1123        return idToStringNoValidation(id);
1124    }
1125    
1126    public static String idToStringNoValidation(String id) {
1127        if (id == null) {
1128            return "";
1129        }
1130
1131        return " id=\"" + XmlUtil.encodeXmlString(id) + "\"";
1132    }
1133    
1134    public static void validateId(String id) {
1135        if (id == null) {
1136            return ;
1137        }
1138
1139        if (id.contains(" ")) {
1140            throw new RuntimeException("id.contains(\" \") \"" + id + "\"");
1141        }
1142        if (id.length() == 0) {
1143            throw new RuntimeException("id.length() == 0 \"" + id + "\"");
1144        }
1145    }
1146    
1147    protected void validateAndAddId(String id) {
1148        if (id == null) {
1149            return ;
1150        }
1151        
1152        validateId(id);
1153        if (usedId.contains(id)) {
1154            throw new RuntimeException("Id \"" + id + "\" is not unique " + usedId);
1155        }
1156        usedId.add(id);
1157    }
1158    
1159    protected boolean inHierarki(HtmlMode searchMode) {
1160        if (mode == searchMode) {
1161            return true;
1162        }
1163        
1164        if (nestedModes == null) {
1165            return false;
1166        }
1167        
1168        for (HtmlMode htmlMode : nestedModes) {
1169            if (htmlMode == searchMode) {
1170                return true;
1171            }
1172        }
1173        
1174        return false;
1175    }
1176    
1177    private void checkNestedModeForUniqueness(HtmlMode searchMode) {
1178/*        if (mode == searchMode) {
1179            throw new RuntimeException("Mode is not unique " + searchMode + " (mode == searchMode)");
1180        }
1181        if (nestedModes == null) {
1182            return ;
1183        }
1184        for (int i = 0; i < nestedModes.size();i++) {
1185            if (searchMode == nestedModes.get(i)) {
1186                throw new RuntimeException("Mode is not unique " + searchMode + " " + i + " " + nestedModes);
1187            }
1188        }*/
1189        if (inHierarki(searchMode)) {
1190            throw new RuntimeException("Mode is not unique! searchMode: " + searchMode + ", mode: " + mode + ", nestedModes: " + nestedModes);
1191        }
1192    }
1193    
1194    private void storeCurrentMode() {
1195        if (nestedModes == null) {
1196            nestedModes = new ArrayList<HtmlMode>();
1197        }
1198        nestedModes.add(mode);
1199    }
1200    
1201    private void restoredMode() {
1202        mode = nestedModes.remove(nestedModes.size() - 1);
1203    }
1204}