view src/nabble/view/web/template/NamlEditor.java @ 19:18cf4872fd7f

remove anonymous posting
author Franklin Schmidt <fschmidt@gmail.com>
date Fri, 29 May 2020 22:58:25 -0600
parents 7ecd1a4ef557
children
line wrap: on
line source


package nabble.view.web.template;

import fschmidt.util.java.HtmlUtils;
import fschmidt.util.servlet.JtpContext;
import nabble.model.DailyNumber;
import nabble.model.Node;
import nabble.model.Site;
import nabble.model.User;
import nabble.modules.ModuleManager;
import nabble.naml.compiler.CompileException;
import nabble.naml.compiler.Macro;
import nabble.naml.compiler.Meaning;
import nabble.naml.compiler.Source;
import nabble.view.lib.Jtp;
import nabble.view.lib.Permissions;
import nabble.view.lib.Shared;
import org.apache.commons.fileupload.FileItem;
import org.apache.commons.fileupload.FileUploadException;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.nio.charset.Charset;
import java.nio.charset.CharsetEncoder;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;


public final class NamlEditor extends HttpServlet {

	protected void service(HttpServletRequest request, HttpServletResponse response)
		throws ServletException, IOException
	{
		User user = Jtp.getUser(request);
		if (user == null) {
			simpleLoginPage("You must login to edit the NAML code of this application.", request, response);
			return;
		}
		boolean isSiteAdmin = Permissions.isInGroup(user, Permissions.ADMINISTRATORS_GROUP);
		boolean isSysAdmin = Permissions.isSysAdmin(user);

		if (!isSiteAdmin && !isSysAdmin) {
			simpleLoginPage("You must login to edit the NAML code of this application.", request, response);
			return;
		}
		buildPage(request, response);
	}

	public static void buildPage(HttpServletRequest request, HttpServletResponse response)
		throws ServletException, IOException
	{
		Site site = Jtp.getSite(request);
		Map<String,String> tweaks = new TreeMap<String,String>(site.getCustomTweaks());
		Node rootNode = site.getRootNode();
		Exception exception = site.getTweakException();

		String macroDef = null;
		String selectedFileName = null;
		String overridenBody = null;
		String meaningId = request.getParameter("id");
		if (meaningId != null) {
			Meaning meaning = site.getProgram().getMeaning(meaningId);
			Macro macro = (Macro) meaning;

			String macroType = macro.getType().toString().toLowerCase();
			String macroBody = macro.element.toString();
			int posOpen = macroBody.indexOf('<');
			int posClose = macroBody.indexOf('>', posOpen+1);
			macroDef = macroBody.substring(posOpen, posClose);
			macroDef = macroDef.replaceFirst("<"+macroType, "<override_"+macroType);
			int posClosingTag = macroBody.lastIndexOf("</");
			String closingTag = "</override_" + macroType + '>';
			overridenBody = macroDef + macroBody.substring(posClose, posClosingTag) + closingTag;
			for( Map.Entry<String,String> entry : tweaks.entrySet() ) {
				String name = entry.getKey();
				String content = entry.getValue();
				if (content.contains(macroDef)) {
					selectedFileName = name;
					break;
				}
			}
		}

		String previousUrl = request.getParameter("prev");

		PrintWriter out = response.getWriter();
		
		out.print( "\n<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.01 Transitional//EN\" \"http://www.w3.org/TR/html4/loose.dtd\">\n<html>\n	<head>\n		<META NAME=\"robots\" CONTENT=\"noindex,nofollow\"/>\n		<meta http-equiv=\"Content-Type\" content=\"text/html;charset=utf-8\" />\n		<title>Naml Editor - " );
		out.print( (rootNode.getSubject()) );
		out.print( "</title>\n		" );
 Shared.head(request, response, false); 
		out.print( "\n		<style type=\"text/css\">\n			div.main-content {\n				position:absolute;\n				top:5.2em;\n				bottom:1.3em;\n				right:.8em;\n				left:.8em;\n			}\n			div.big-title {\n				margin:.5em 0;\n			}\n			table.vertical-control {\n				border-collapse:collapse;\n				width:100%;\n				height:100%;\n				position:absolute;\n				top:0;\n				bottom:0;\n			}\n			table.vertical-control td {\n				padding:0;\n				vertical-align:top;\n			}\n			table.vertical-control td.options {\n				width:13em;\n				padding-top:.2em;\n			}\n			table.vertical-control td.details {\n				padding: .2em;\n			}\n			ul.vertical-control {\n				width:100%;\n				list-style-type:none;\n				padding:0;\n				margin:.5em 0;\n			}\n			ul.vertical-control li {\n				padding: .3em;\n				white-space:nowrap;\n				cursor: pointer;\n				font-weight:normal;\n				width:13em;\n				overflow: hidden;\n				border-top-left-radius:5px;\n				border-bottom-left-radius:5px;\n				-moz-border-radius-topleft:5px;\n				-moz-border-radius-bottomleft:5px;\n			}\n			ul.vertical-control li.selected {\n				cursor: default;\n				font-weight:bold;\n			}\n			#options-ul {\n				overflow-x:hidden !important;\n				overflow-y:auto !important;\n			}\n			div.more {\n				position:absolute;\n				left:0;\n				width:13em;\n				bottom:0;\n				border-top: 1px dotted #ddd;\n			}\n			img.toolbar-icon {\n				vertical-align: -25%;\n			}\n			div.default {\n				position: absolute;\n				top: .2em;\n				bottom: 0;\n				margin-bottom: .2em;\n				height: auto;\n				overflow: auto;\n			}\n			div.advanced-option-body {\n				padding: 0 1.5em 1.5em;\n			}\n\n			.CodeMirror-line-numbers {\n				font-family: verdana, arial, sans-serif;\n				font-size: 11pt;\n				width: 2.2em;\n				color: #aaa;\n				background-color: #eee;\n				text-align: right;\n				padding-right: .3em;\n				line-height: normal;\n			}\n\n			span.xml-tagname {color: #A0B;}\n			span.xml-attribute {color: #281;}\n			span.xml-punctuation {color: black;}\n			span.xml-attname {color: #00F;}\n			span.xml-comment {color: #A70;}\n			span.xml-cdata {color: #48A;}\n			span.xml-processing {color: #999;}\n			span.xml-entity {color: #A22;}\n			span.xml-error {color: #F00 !important;}\n			span.xml-text {color: black;}\n\n			div.status-label {\n				float:right;\n				font-weight:bold;\n				width:6em;\n				text-align:center;\n				line-height:1.2em;\n				margin:-.2em 0 0 .5em;\n				padding:.35em;\n			}\n			div.no-error {\n				color: #656565;\n				cursor: text;\n			}\n			div.has-error {\n				color: white;\n				cursor: pointer;\n				background: url('/gradients/v20_EE9999_EE5555') #EE5555 repeat-x;\n			}\n			#error-message {\n				position:absolute;\n				display:none;\n				z-index:1000;\n				padding:0 1em 1em;\n				margin-top:.2em;\n				top:5em;\n				left:10%;\n				bottom:10%;\n				right:.7em;\n				overflow:auto;\n				display:none;\n			}\n		</style>\n\n		<script src=\"/util/codemirror/js/codemirror.js\"></script>\n		<script src=\"/util/codemirror/js/highlight.js\"></script>\n		<script src=\"/util/codemirror/js/stringstream.js\"></script>\n		<script src=\"/util/codemirror/js/tokenize.js\"></script>\n		<script src=\"/util/codemirror/js/parsexml.js\"></script>\n\n		<script type=\"text/javascript\">\n			var configFileName = \"" );
		out.print( (ModuleManager.CONFIGURATION_TWEAK) );
		out.print( "\";\n			function setVisible(name, show) {\n				if (show) $('#'+name).show();\n				else $('#'+name).hide();\n			};\n			var modified = [];\n			function setModified(id) {\n				if (modified.indexOf(id) >= 0)\n					return;\n				modified.push(id);\n				if (id == 'removed')\n					return;\n				var $tab = $('#tab_'+id);\n				var html = $tab.html();\n				$tab.html(html.replace(/file\\.png/, 'file_modified.png'));\n				setVisible('modified-warning',true);\n			};\n			function resetModified() {\n				for (var i = 0; i < modified.length; i++) {\n					if (modified[i] == 'removed')\n						continue;\n					var $tab = $('#tab_'+modified[i]);\n					var html = $tab.html();\n					$tab.html(html.replace(/file_modified\\.png/, 'file.png'));\n				}\n				setVisible('modified-warning',false);\n				modified = [];\n			}\n			window.onbeforeunload = function() {\n				if (modified.length > 0)\n					return 'You have unsaved changes.\\nDo you really want to leave without saving?';\n			};\n			var fileImg = '<img src=\"/images/file.png\" style=\"vertical-align:-20%\"/> ';\n			Array.prototype.remove = function(s){\n				for(i=0; i<this.length; i++){\n					if (s == this[i])\n						this.splice(i, 1);\n				}\n			}\n			var advancedOptions = {\n				id: 'advanced_options',\n				name: 'Advanced Options',\n				content: null\n			};\n			function isDefaultTab(f) {\n				return f == advancedOptions;\n			};\n\n			var files = [];\n			" );

					int i = 0;
					for( Map.Entry<String,String> entry : tweaks.entrySet() ) {
						String name = entry.getKey();
						String content = entry.getValue();
						
		out.print( "\nfiles.push({\nid: " );
		out.print( (i++) );
		out.print( ",\nname: \"" );
		out.print( (HtmlUtils.javascriptStringEncode(name)) );
		out.print( "\",\ncontents: \"" );
		out.print( (HtmlUtils.javascriptStringEncode(content)) );
		out.print( "\"\n});\n" );
 } 
		out.print( "\nfunction addFile(file) {\n$('#options-ul').append('<li id=\"tab_'+file.id+'\">'+fileImg+file.name+'</li>');\nclickableTab(file);\n\nvar textareaId = 'textarea_'+file.id;\n$('#details').append('<div id=\"div_'+file.id+'\" style=\"display:none\"><textarea id=\"'+textareaId+'\"></textarea></div>');\nvar $textarea = $('#'+textareaId);\n$textarea.val(file.contents);\n\nfile.editor = CodeMirror.fromTextArea(textareaId, {\n	parserfile: \"parsexml.js\",\n	stylesheet: \"/util/codemirror/css/xmlcolors.css\",\n	path: \"/util/codemirror/js/\",\n	lineNumbers: true,\n	indentUnit: 4,\n	onChange: function() { setModified(file.id); }\n});\n\nvar $wrapping = $textarea.next();\n$wrapping.css({\n	position: 'absolute',\n	top: '2.5em',\n	bottom: 0,\n	marginBottom: '.2em',\n	height: 'auto'\n});\n$wrapping.attr('id', 'wrapping_'+file.id)\n$wrapping.addClass('no-bg-color');\n\n/* File Toolbar */\nvar toolbar = '<div id=\"toolbar_'+file.id+'\" class=\"shaded-bg-color\" style=\"padding:.2em .4em .3em;text-align:right\">';\ntoolbar += '<div class=\"float-left\" style=\"font-style:italic;padding:.3em\">'+file.name+'</div>';\ntoolbar += '<button class=\"toolbar\" style=\"padding:.1em .3em\" onclick=\"renameFile()\"><img class=\"toolbar-icon\" src=\"/images/edit_sm.png\"/> Rename</button> ';\ntoolbar += '<button class=\"toolbar\" style=\"padding:.1em .3em\" onclick=\"removeFile()\"><img class=\"toolbar-icon\" src=\"/images/remove_sm.png\"/> Remove</button>';\ntoolbar += '</div>';\n$textarea.after(toolbar);\n};\nfunction clickableTab(f) {\nvar $tab = $('#tab_'+f.id);\n$tab.click(function() {\n	select(f);\n});\ntabHover($tab);\n};\nfunction tabHover($tab) {\n$tab.hover(function() {\n		if (!$(this).hasClass('selected'))\n			$(this).addClass('light-bg-color');\n	},\n	function() {\n		$(this).removeClass('light-bg-color');\n	}\n);\n};\n\nvar initialized = [];\nfunction showFile(file) {\n$('#div_'+file.id).show();\nif (initialized.indexOf(file.id) == -1) {\n	var cc = Nabble.get('toolbar_'+file.id);\n	$('#wrapping_'+file.id).css('top', $(cc).outerHeight());\n	var frame = window.frames[window.frames.length-1];\n	var doc = window.document;\n	$(frame.document).click(function() { $(doc).click(); });\n	initialized.push(file.id);\n}\n};\n\nvar selected = null;\nfunction select(f) {\nif (selected != f) {\n	$('li').removeClass('selected dark-bg-color');\n	var $tab = $('#tab_'+f.id);\n	$tab.addClass('selected dark-bg-color');\n	$('#details').children().hide();\n	showFile(f);\n	selected = f;\n	layout();\n} else if (f == null || files.length == 0)\n	setVisible('no-selection',true);\n};\nfunction layout() {\nif (selected) {\n	var detailsWidth = $('#details').width();\n	if (!isDefaultTab(selected)) {\n		var $div = $('#div_'+selected.id);\n		var $lineNumbers = $('div.CodeMirror-line-numbers', $div);\n		var width = detailsWidth - $lineNumbers.width() - 5;\n		$('div.CodeMirror-wrapping', $div).width(Math.round(width)+'px');\n	} else {\n		$('div.default').width(Math.round(detailsWidth - 30)+'px');\n	}\n}\nvar $ul = $('#options-ul');\nvar uTop = $ul.position().top;\nvar mTop = $('#more').position().top;\n$ul.height(mTop-uTop-10);\n};\nfunction newFile() {\nvar name = prompt('Name of the file:');\nif (name == configFileName) {\n	alert('You cannot create a file with this name.');\n	return;\n}\nif (name) {\n	var f = {\n		id: new Date().getTime(),\n		name: name,\n		contents: ''\n	};\n	files.push(f);\n	addFile(f);\n	select(f);\n	setModified(f.id);\n}\n};\nfunction renameFile() {\nif (selected) {\n	name = prompt(\"Name of the file:\", selected.name);\n	if (name && name !='null' && Nabble.trim(name).length > 0) {\n		selected.name = Nabble.trim(name);\n		$tab = $('#tab_'+selected.id).html(fileImg + name);\n		$toolbar = $('#toolbar_'+selected.id+' div').html(name);\n		setModified(selected.id);\n	}\n}\n};\nfunction removeFile() {\nif (selected && confirm(\"Do you really want to delete \" + selected.name + \"?\")) {\n	var i = files.indexOf(selected);\n	files.remove(selected);\n	setModified('removed');\n	$('#tab_'+selected.id+',#div_'+selected.id).remove();\n	i = i > 0? i-1 : files.length > 0? 0 : -1;\n	if (i == -1)\n		setVisible('no-selection',true);\n	else\n		select(files[i]);\n}\n};\nfunction saveChanges() {\nnotice('Saving data... please wait');\nvar params = {};\nparams['fileCount'] = files.length;\nvar c = 0;\nfor (var i=0; i < files.length; i++) {\n	var name = files[i].name;\n	params['name'+c] = name;\n	params['contents'+c] = files[i].editor.getCode();\n	c++;\n}\n$.post(\"./NamlEditor$Save.jtp\", params,\n	function(data){\n		if (data == null || data.length == 0) {\n			setError(null);\n			notice('Data successfully saved, no errors found', 2000, 2000);\n			resetModified();\n		} else {\n			setError(data);\n			notice('Data NOT saved, please fix errors', 5000, 2000);\n		}\n	}\n);\n};\n\nfunction getOrCreateFile(name) {\nfor (var i=0; i<files.length; i++) {\n	if (files[i].name == name)\n		return files[i];\n}\nfiles.push({\n	id: new Date().getTime(),\n	name: name,\n	contents:''\n});\nreturn files[files.length-1];\n};\nfunction getLineNumber(s, token) {\nvar line = 1;\nvar pos = s.indexOf(token);\nfor (var i=0; i < pos; i++) {\n	if (s.charAt(i) == '\\n')\n		line++;\n}\nreturn line;\n};\nfunction startDefaultTabs() {\n/* Advanced options tab*/\nvar $tabs = $('#tab_advanced_options');\n$tabs.click(function() { select(advancedOptions); });\n};\nfunction setError(err) {\nvar $errorMsg = $('#error-message');\n$errorMsg.html(err? err : '');\nvar $status = $('#error-status');\nif (err) {\n	var visible = false;\n	function errorClick(e) {\n		e.stopPropagation();\n		if (visible) {\n			$errorMsg.slideUp('fast', function() { $errorMsg.hide() });\n			$status.html('View Error');\n		} else {\n			$errorMsg.slideDown('fast');\n			$status.html('Hide Error');\n		}\n		visible = !visible;\n	};\n	$status.addClass('has-error').removeClass('no-error').html('View Error').click(errorClick).click();\n	$(document).click(function(e) {\n		if ($(e.target).attr('id') == 'error-message')\n			return;\n		var $parents = $(e.target).parents();\n		if ($parents.hasClass('error-message'))\n			return;\n		visible = true;\n		$status.click();\n	});\n} else {\n	$status.addClass('no-error').removeClass('has-error').html('No Errors').unbind('click');\n	$(document).unbind('click');\n}\n};\nvar error = " );
		out.print( (exception == null? "null" : "\"" + HtmlUtils.javascriptStringEncode(getErrorMessage(exception)) + "\"") );
		out.print( ";\n\nfunction startEditors() {\nnotice('Starting editor... please wait');\nstartDefaultTabs();\nsetError(error);\nlayout();\nvar selectedFileName = " );
		out.print( (selectedFileName == null? "null" : "\""+selectedFileName+"\"") );
		out.print( ";\nvar selectedFile = files[0];\nvar macroDef = " );
		out.print( (macroDef == null? "null" : "\""+HtmlUtils.javascriptStringEncode(macroDef)+"\"") );
		out.print( ";\nvar overridenBody = " );
		out.print( (overridenBody == null? "null" : "\""+HtmlUtils.javascriptStringEncode(overridenBody)+"\"") );
		out.print( ";\nvar macroLineNumber = 0;\nif (selectedFileName != null) {\n	var f = getOrCreateFile(selectedFileName);\n	macroLineNumber = getLineNumber(f.contents,macroDef);\n} else if (overridenBody != null) {\n	var f = getOrCreateFile('tweaks');\n	selectedFileName = f.name;\n	f.contents = Nabble.trim(f.contents);\n	f.contents = (f.contents.length > 0? f.contents + '\\n\\n':'') + overridenBody;\n	macroLineNumber = getLineNumber(f.contents,macroDef);\n} else {\n	var f = files.length == 0? getOrCreateFile('tweaks') : files[0];\n	selectedFileName = f.name;\n}\nfor (var i=0; i < files.length; i++) {\n	addFile(files[i]);\n	if (files[i].name == selectedFileName)\n		selectedFile = files[i];\n}\nselect(selectedFile);\nif (selectedFile && macroLineNumber > 0) {\n	function goToLine() {\n		var $lines = $('#wrapping_'+selectedFile.id+' div.CodeMirror-line-numbers').children();\n		if (selectedFile.editor.editor && $lines.size() >= macroLineNumber) {\n			var c = 1;\n			$lines.each(function() {\n				var t = $(this).html();\n				if (c == macroLineNumber) {\n					$(this).css('font-weight','bold').addClass('highlight');\n					setTimeout(function(){\n						selectedFile.editor.jumpToLine(Number(macroLineNumber));\n						notice('Ready', 2000, 1000);\n					}, 150);\n					return false;\n				}\n				if (t != \"&nbsp;\")\n					c++;\n			});\n		} else\n			setTimeout(goToLine, 50);\n	};\n	goToLine();\n} else\n	notice('Ready', 2000, 1000);\n$(window).resize(layout);\n};\n\nvar started = false;\n$(document).ready(function() {\nif (!started) {\n	started = true;\n	startEditors();\n}\n});\n</script>\n</head>\n<body>\n<div id=\"notice\" class=\"notice rounded-bottom\"></div>\n" );
 Shared.minHeader(request, response, site.getRootNode(), null, false); 
		out.print( "\n\n<div class=\"shaded-bg-color\" style=\"padding:.5em\">\n<div id=\"error-status\" class=\"status-label rounded no-error\">No Errors</div>\n<div id=\"error-message\"class=\"border2 error-message drop-shadow\"></div>\n<span class=\"big-title second-font\">NAML Editor</span>\n</div>\n\n<div class=\"main-content\">\n<table class=\"vertical-control\">\n<tr>\n	<td class=\"options\">\n		<button class=\"toolbar\" onclick=\"newFile()\">Add New File</button>\n		<ul id=\"options-ul\" class=\"vertical-control\">\n		</ul>\n		<div id=\"more\" class=\"more\">\n			<ul class=\"vertical-control\">\n				<li id=\"tab_advanced_options\" class=\"weak-color\">\n					<img src=\"/images/tool.png\" width=\"16\" height=\"17\" style=\"vertical-align:-15%\"/>\n					Advanced Options\n				</li>\n				" );
 if (previousUrl != null) { 
		out.print( "\n				<li style=\"cursor:text\">\n					<img src=\"/images/arrowleft.png\" style=\"width:15px;height:15px;vertical-align:-15%;margin-right:1px\"/>\n					<a href=\"" );
		out.print( (previousUrl) );
		out.print( "\">Continue</a>\n				</li>\n				" );
 } 
		out.print( "\n			</ul>\n			<button class=\"toolbar action-button\" onclick=\"saveChanges()\" style=\"width:100%;padding:.4em 0\">Save Changes</button>\n		</div>\n	</td>\n	<td id=\"details\" class=\"details dark-bg-color\">\n		<div id=\"no-selection\" class=\"no-bg-color\" style=\"display:none;padding:.2em 1em\">\n			<div class=\"big-title second-font weak-color\">No Files</div>\n			<div class=\"weak-color\">Use the button on the left to create a new file.</div>\n		</div>\n		<div id=\"div_advanced_options\" class=\"no-bg-color default\" style=\"display:none;padding:.2em 1em\">\n			<div id=\"modified-warning\" class=\"info-message invisible\" style=\"padding: .2em .5em .5em\">\n				<div class=\"big-title important\">Warning -- You have unsaved changes!</div>\n				It is highly recommended that you save your changes before using the options below.\n				The download option will NOT see your modified files and the upload option will override your last changes. Click on the\n				\"Save Changes\" button first if you don't want to lose your modified files.\n			</div>\n			<div class=\"big-title second-font\">Download Changes</div>\n			<div class=\"advanced-option-body\">\n				Download a ZIP file with your custom NAML code and save a copy on your computer.\n				<div style=\"padding:.3em 0\">\n					<a href=\"" );
		out.print( (NamlDownload.getFilePath(site)) );
		out.print( "\">Download ZIP file</a>\n				</div>\n			</div>\n\n			<div class=\"big-title second-font\">Upload Changes</div>\n			<div class=\"advanced-option-body\">\n				Import a ZIP file with custom NAML code.<br/>\n				You may consider downloading a backup before uploading a new file.<br/>\n				<iframe id=\"upload-frame\" src=\"./NamlEditor$UploadForm.jtp\" frameborder=\"0\" width=\"100%\" scrolling=\"0\"></iframe>\n			</div>\n		</div>\n	</td>\n</tr>\n</table>\n</div>\n\n" );
 Shared.noFooter(request,response); 
		out.print( "\n" );
 Shared.analytics(request,response); 
		out.print( "\n</body>\n</html>\n" );

	}

	public static void save(HttpServletRequest request, HttpServletResponse response)
			throws ServletException, IOException
	{
		Site site = Jtp.getSite(request);
		int fileCount = Integer.valueOf(request.getParameter("fileCount"));
		Map<String, String> tweaks = new HashMap<String, String>();
		for (int i = 0; i < fileCount; i++) {
			String name = request.getParameter("name"+i);
			String contents = request.getParameter("contents"+i);
			tweaks.put(name, contents);
		}
		Map<String, String> previousTweaks = site.getCustomTweaks();
		site.setCustomTweaks(tweaks);
		try {
			site.getProgram().getTemplate("do_nothing");
		} catch (CompileException e) {
			response.getWriter().print(getErrorMessage(e));
			// reverting changes
			site.setCustomTweaks(previousTweaks);
		}
	}

	public static void uploadForm(HttpServletRequest request, HttpServletResponse response)
			throws ServletException, IOException
	{
		PrintWriter out = response.getWriter();
		
		out.print( "\n<html>\n	<head>\n		" );
 Shared.head(request, response, false); 
		out.print( "\n	</head>\n	<body style=\"margin-left:0\">\n		<form action=\"./NamlEditor$Upload.jtp\" method=\"POST\" enctype=\"multipart/form-data\">\n			<input name=\"file\" type=\"file\" size=\"20\" />\n			<div style=\"padding:.5em 0\">\n				<input type=\"radio\" name=\"type\" id=\"override\" value=\"override\" checked=\"true\"/> <label for=\"override\">Override my changes</label><br/>\n				<input type=\"radio\" name=\"type\" id=\"append\" value=\"append\"/> <label for=\"append\">Merge with my changes</label><br/>\n			</div>\n			<button type=\"submit\" class=\"toolbar second-font\" style=\"padding:.25em\">Upload ZIP File</button>\n		</form>\n	</body>\n</html>\n" );

	}

	private static final int SIZE_LIMIT = 1024 * 1024 * 2; // 2 Mb
	private static final CharsetEncoder CHARSET_ENCODER = Charset.forName("UTF-8").newEncoder();

	public static void upload(HttpServletRequest request, HttpServletResponse response)
			throws ServletException, IOException
	{
		final Map<String, FileItem> map;
		try {
			map = Jtp.getFileItems(request);
		} catch (FileUploadException e) {
			resultPage("Upload failed: " + e.getMessage(), request, response);
			return;
		}
		FileItem fi = map.get("file");
		if (fi == null) {
			resultPage("Image upload failed, please try again.", request, response);
			return;
		} else if (fi.getSize() > SIZE_LIMIT) {
			resultPage("The file you uploaded is too big. Please upload a smaller image (less than 2Mb).", request, response);
			return;
		}
		Site site = Jtp.getSite(request);
		Map<String,String> tweaks = new HashMap<String, String>(site.getCustomTweaks());

		String type = map.get("type").getString();
		boolean isOverride = "override".equals(type);
		if (isOverride)
			tweaks.clear();

		InputStream in = fi.getInputStream();
		ZipInputStream zis = new ZipInputStream(in);
		ZipEntry entry;
		int i = 0;
		while ((entry = zis.getNextEntry()) != null) {
			String name = entry.getName();
			if (name.endsWith(".naml")) {
				int size;
				byte[] buffer = new byte[2048];
				ByteArrayOutputStream baos = new ByteArrayOutputStream();
				while ((size = zis.read(buffer, 0, buffer.length)) != -1) {
					baos.write(buffer, 0, size);
				}
				if (CHARSET_ENCODER.canEncode(baos.toString())) {
					String contents = baos.toString("UTF-8");
					name = name.replaceAll("\\.naml", ""); // remove ".naml" ext
					if (tweaks.containsKey(name)) {
						String original = tweaks.get(name);
						tweaks.put(name, original + "\n\n----------- Merged -----------\n\n" + contents);
					} else
						tweaks.put(name, contents);
					baos.flush();
					baos.close();
					i++;
				}
			}
		}
		if (i == 0) {
			resultPage("Invalid ZIP file.", request, response);
			return;
		}
		// Saves tweaks and recompiles XML
		site.setCustomTweaks(tweaks);
		site.getTemplate("do_nothing");
		// Reloads the page (if there is any exception, it will be loaded and displayed)
		resultPage(null, request, response);
	}

	private static void resultPage(String message, HttpServletRequest request, HttpServletResponse response)
		throws IOException
	{
		PrintWriter out = response.getWriter();
		
		out.print( "\n<html>\n	<head>\n		" );
 Shared.head(request, response, false); 
		out.print( "\n	</head>\n	<body style=\"margin-left:0\">\n		" );
 if (message == null) { 
		out.print( "\n			<script type=\"text/javascript\">\n				parent.location = './NamlEditor.jtp';\n			</script>\n		" );
 } else { 
		out.print( "\n			Error: <span style=\"color:red\">" );
		out.print( (message) );
		out.print( "</span>\n			<br/><br/>\n			<a href=\"./NamlEditor$UploadForm.jtp\">Try again</a>\n		" );
 } 
		out.print( "\n	</body>\n</html>\n" );

	}

	private static String getErrorMessage(Exception exception) {
		StringBuilder errorMessage = new StringBuilder();
		if (exception != null) {
			errorMessage
				.append("<div class=\"important big-title second-font\">Error</div>")
				.append(exception.getMessage().replaceAll("\n","<br/>"))
				.append("<div class=\"bold\" style=\"padding:.5em 0 .3em\">Full Stack Trace</div>");
			StackTraceElement[] stack = exception.getStackTrace();
			for (StackTraceElement e : stack)
				errorMessage.append(e).append("<br/>");
		}
		return errorMessage.toString();
	}

	public static class Save extends HttpServlet {

		protected void service(HttpServletRequest request, HttpServletResponse response)
			throws ServletException, IOException
		{
			User user = Jtp.getUser(request);
			boolean isSiteAdmin = user != null && Permissions.isInGroup(user, Permissions.ADMINISTRATORS_GROUP);
			boolean isSysAdmin = user != null && Permissions.isSysAdmin(user);
			if ((isSiteAdmin || isSysAdmin) && "POST".equals(request.getMethod())) {
				save(request, response);
			} else
				response.getWriter().print("Please log in with your administrator account. Your may have logged out or your session has expired.");
		}
	}

	public static class UploadForm extends HttpServlet {

		protected void service(HttpServletRequest request, HttpServletResponse response)
			throws ServletException, IOException
		{
			uploadForm(request, response);
		}
	}

	public static class Upload extends HttpServlet {

		protected void service(HttpServletRequest request, HttpServletResponse response)
			throws ServletException, IOException
		{
			User user = Jtp.getUser(request);
			if (user == null) {
				resultPage("You are not logged in as an administrator.", request, response);
				return;
			}
			boolean isSiteAdmin = Permissions.isInGroup(user, Permissions.ADMINISTRATORS_GROUP);
			boolean isSysAdmin = Permissions.isSysAdmin(user);
			if (!isSiteAdmin && !isSysAdmin) {
				resultPage("You are not logged in as an administrator.", request, response);
				return;
			}

			upload(request, response);
		}
	}

	private static void simpleLoginPage(String message, HttpServletRequest request, HttpServletResponse response)
		throws ServletException, IOException
	{
		PrintWriter out = response.getWriter();
		
		out.print( "\n	<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.01 Transitional//EN\" \"http://www.w3.org/TR/html4/loose.dtd\">\n	<html>\n		<head>\n			" );
 Shared.loadJavascript(request, out); 
		out.print( "\n			<title>Naml Editor | Login</title>\n		</head>\n		<body style=\"text-align:center;font-family: Verdana,sans-serif;font-size:.84em;padding-top:2em\">\n			<img src=\"/images/naml.png\"/>\n			<p><b>" );
		out.print( (message) );
		out.print( "</b></p>\n			<form action=\"./NamlEditor$SimpleLogin.jtp\" method=\"post\">\n				<table style=\"margin:0 auto;font:inherit\">\n					<tr>\n						<td style=\"text-align:right\">Email</td>\n						<td><input type=\"text\" name=\"email\"/></td>\n					</tr>\n					<tr>\n						<td style=\"text-align:right\">Password</td>\n						<td><input type=\"password\" name=\"password\"/></td>\n					</tr>\n					<tr>\n						<td colspan=\"2\" style=\"padding: .5em 0 0;text-align:center\">\n							<input type=\"submit\" value=\"Login\" style=\"font-weight:bold;padding:.3em .4em\"/>\n							or <a href=\"/\">Cancel</a>\n						</td>\n					</tr>\n				</table>\n			</form>\n			<div style=\"margin:2em 15%;text-align:center;background:#eee;padding:.3em 0\">\n			Powered by <a href=\"" );
		out.print( (Jtp.homePage()) );
		out.print( "\">Nabble</a>\n			</div>\n		</body>\n	</html>\n" );

	}

	public static class SimpleLogin extends HttpServlet {

		protected void service(HttpServletRequest request, HttpServletResponse response)
			throws ServletException, IOException
		{
			Site site = Jtp.getSite(request);
			String email = request.getParameter("email");
			String password = request.getParameter("password");
			User user = site.getUserFromEmail(email);
			if (user != null && user.isRegistered() && user.checkPassword(password) && "post".equalsIgnoreCase(request.getMethod())) {
				Jtp.doLogin(request,response,user,true);
				DailyNumber.logins.inc();

				Shared.javascriptRedirect(request,response, "./NamlEditor.jtp");
			} else {
				simpleLoginPage("<span style=\"color:#C00\">Incorrect login!</span>", request, response);
			}
		}
	}

	public static class ViewFile extends HttpServlet {

		protected void service(HttpServletRequest request, HttpServletResponse response)
			throws ServletException, IOException
		{
			JtpContext jtpContext = (JtpContext)getServletContext().getAttribute(JtpContext.attrName);
			jtpContext.setEtag(request,response);

			Site site = Jtp.getSite(request);
			String file = request.getParameter("file");
			PrintWriter out = response.getWriter();
			List<Source> sources = site.getProgram().getSources();
			for (Source s : sources) {
				if (s.id.equals(file)) {
					file = file.replaceFirst("^.+:", "");
					file = file.endsWith(".naml")? file : file + ".naml";
					String code = HtmlUtils.htmlEncode(s.content);
					
		out.print( "\n	<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.01 Transitional//EN\" \"http://www.w3.org/TR/html4/loose.dtd\">\n	<html>\n		<head>\n			<title>" );
		out.print( (file) );
		out.print( " | Naml File</title>\n			" );
 Shared.head(request, response, false); 
		out.print( "\n		</head>\n		<body>\n			" );
 Shared.minHeader(request, response, site.getRootNode(), null, false); 
		out.print( "\n			<h1>" );
		out.print( (file) );
		out.print( "</h1>\n			<pre>" );
		out.print( (code) );
		out.print( "</pre>\n			" );
 Shared.footer(request, response); 
		out.print( "\n		</body>\n	</html>\n" );

					return;
				}
			}
			response.sendError(HttpServletResponse.SC_NOT_FOUND, "Source file not found.");
		}
	}
}