package org.webslinger.modules.edit;
import java.io.IOException;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.TimeUnit;
import java.util.logging.Logger;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;
import javax.servlet.http.HttpSessionBindingEvent;
import javax.servlet.http.HttpSessionBindingListener;
import org.apache.commons.fileupload.FileItem;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.vfs.FileContent;
import org.apache.commons.vfs.FileObject;
import org.webslinger.Webslinger;
import org.webslinger.WebslingerServletContext;
import org.webslinger.collections.CollectionUtil;
import org.webslinger.container.CommonsVfsFileInfo;
import org.webslinger.container.FileInfo;
import org.webslinger.io.Charsets;
import org.webslinger.io.IOUtil;
import static org.webslinger.servlet.MultipartFormUtil.parseRequestAsList;
import static org.webslinger.servlet.MultipartFormUtil.createFileItem;
public class Editor implements HttpSessionBindingListener, Serializable {
private static final Logger logger = Logger.getLogger(Editor.class.getName());
protected final WebslingerServletContext servletContext;
protected final FileObject base;
protected final String prefix;
protected final HashMap contexts = new HashMap();
public Editor(WebslingerServletContext servletContext, FileObject base, String prefix) {
this.servletContext = servletContext;
this.base = base;
this.prefix = prefix;
}
public void valueBound(HttpSessionBindingEvent event) {
}
public void valueUnbound(HttpSessionBindingEvent event) {
Iterator it = contexts.values().iterator();
while (it.hasNext()) {
Context context = (Context) it.next();
context.clear();
}
contexts.clear();
}
public static Editor getEditor(Webslinger webslinger) throws IOException {
return getEditor(webslinger, webslinger.getWebslingerContainer().getRoot(), "www/");
}
public static Editor getEditor(Webslinger webslinger, FileObject root, String prefix) throws IOException {
return new Editor(webslinger.getWebslingerServletContext(), root, prefix);
}
public class Request {
protected final List items;
public final Context context;
public Request(String path, List items) throws EditorException {
this.items = items;
Iterator it = items.iterator();
while (it.hasNext()) {
FileItem item = (FileItem) it.next();
String name = item.getFieldName();
if (!name.equals("path")) continue;
try {
path = IOUtil.readString(item.getInputStream(), Charsets.UTF8);
item.delete();
it.remove();
} catch (IOException e) {
throw new EditorException("Couldn't process parameter(path)", e);
}
}
context = Editor.this.getContext(path);
}
public Request(String path, HttpServletRequest request) throws EditorException, IOException {
this(path, parseRequestAsList(request));
}
public void action_deleteAttributes(List errors) throws EditorException {
Iterator it = items.iterator();
while (it.hasNext()) {
FileItem item = (FileItem) it.next();
String name = item.getFieldName();
try {
if (name.startsWith("delete.")) {
context.deleteAttribute(name.substring(7));
} else if (name.equals("deleted")) {
context.deleteAttribute(IOUtil.readString(item.getInputStream(), Charsets.UTF8));
} else {
continue;
}
item.delete();
} catch (EditorException e) {
errors.add(e);
} catch (IOException e) {
errors.add(new EditorException("Couldn't process parameter(" + name + ")", e));
}
}
}
public void action_setAttributes(List errors) throws EditorException {
Iterator it = items.iterator();
String attrName = null, attrValue = null;
boolean doAdd = false;
while (it.hasNext()) {
FileItem item = (FileItem) it.next();
String name = item.getFieldName();
try {
if (name.equals("add")) {
doAdd = true;
} else if (name.startsWith("attr.")) {
context.setAttribute(name.substring(5), IOUtil.readString(item.getInputStream(), Charsets.UTF8));
} else if (name.equals("attrName")) {
attrName = IOUtil.readString(item.getInputStream(), Charsets.UTF8);
} else if (name.equals("attrValue")) {
attrValue = IOUtil.readString(item.getInputStream(), Charsets.UTF8);
} else {
continue;
}
item.delete();
} catch (EditorException e) {
errors.add(e);
} catch (IOException e) {
errors.add(new EditorException("Couldn't process parameter(" + name + ")", e));
}
}
if (doAdd) {
if (attrName == null) {
errors.add(new EditorException("Couldn't add attribute, name is null"));
} else if (attrValue == null) {
errors.add(new EditorException("Couldn't add attribute(" + attrName + "), value is null"));
} else {
context.setAttribute(attrName, attrValue);
}
}
}
protected FileItem checkItem(FileItem oldItem, FileItem newItem) {
if (oldItem != null) oldItem.delete();
return newItem;
}
public boolean action_content(List errors) throws EditorException {
Iterator it = items.iterator();
FileItem uploadedFile = null;
boolean doUpload = false;
while (it.hasNext()) {
FileItem item = (FileItem) it.next();
String name = item.getFieldName();
if (name.equals("content")) {
context.setContent(item);
} else if (name.equals("file")) {
if (uploadedFile != null) uploadedFile.delete();
uploadedFile = item;
} else if (name.equals("upload")) {
doUpload = true;
} else {
continue;
}
it.remove();
}
if (doUpload) {
context.setContent(uploadedFile);
context.setAttribute("content-type", uploadedFile.getContentType());
return true;
}
return false;
}
protected String checkCommand(String command, String newCommand) throws EditorException {
if (command == null || command.equals(newCommand)) return newCommand;
throw new EditorException("Tried to override command(" + command + ") with (" + newCommand + ")");
}
public void action_command(List errors, boolean autoSave) throws EditorException {
Iterator it = items.iterator();
String command = !autoSave ? null : "save";
while (it.hasNext()) {
FileItem item = (FileItem) it.next();
String name = item.getFieldName();
if (name.startsWith("cmd.")) {
command = checkCommand(command, name.substring(4));
} else if (name.equals("delete")) {
command = checkCommand(command, "delete");
} else if (name.equals("revert")) {
command = checkCommand(command, "revert");
} else if (name.equals("save")) {
command = checkCommand(command, "save");
} else {
continue;
}
item.delete();
it.remove();
}
if ("delete".equals(command)) {
context.delete();
} else if ("revert".equals(command)) {
context.revert();
} else if ("save".equals(command)) {
context.save();
} else {
}
}
}
public Request parseRequest(String path, HttpServletRequest request) throws EditorException {
try {
return new Request(path, request);
} catch (IOException e) {
throw new EditorException("Couldn't parse request", e);
}
}
public Request parseRequest(String path, List items) throws EditorException {
return new Request(path, items);
}
public String processRequest(String path, HttpServletRequest request) throws EditorException {
Request req = parseRequest(path, request);
ArrayList errors = new ArrayList();
processRequest(req, errors);
return req.context.filePath;
}
public void processRequest(Request request, List errors) throws EditorException {
request.action_setAttributes(errors);
request.action_deleteAttributes(errors);
boolean autoSave = request.action_content(errors);
request.action_command(errors, autoSave);
}
protected Context getContext(String path) throws EditorException {
synchronized (contexts) {
Context context = (Context) contexts.get(path);
if (context != null) return context;
context = new Context(path);
contexts.put(path, context);
return context;
}
}
public String getDefaultExtension() throws IOException {
return (String) servletContext.getConfigItem("Edit.DefaultExtension");
}
public String getDefaultEditor() throws IOException {
return (String) servletContext.getConfigItem("Edit.DefaultEditor");
}
protected FileInfo createFileInfo(String path) throws EditorException {
try {
return new CommonsVfsFileInfo(servletContext.getContainer(), base.resolveFile(path));
} catch (IOException e) {
throw new EditorException("Couldn't get file info(" + path + ")", e);
}
}
protected String findExtensionByMimeType(String contentType) throws EditorException {
try {
return servletContext.getContainer().findExtensionByMimeType(contentType);
} catch (IOException e) {
throw new EditorException("Couldn't find extension for(" + contentType + ")", e);
}
}
protected String[] getExtensionList() throws EditorException {
try {
return servletContext.getContainer().getExtensionList();
} catch (IOException e) {
throw new EditorException("Couldn't get extension list", e);
}
}
protected boolean checkForMigration(FileObject ptr, Map hookContext) throws EditorException {
try {
logger.fine("checkForMigration(" + ptr + ")");
String[] extensions = getExtensionList();
if (extensions == null || extensions.length == 0) return true;
if (ptr.exists() || ptr.getName().getPath().length() == 0) return false;
FileObject parent = ptr.getParent();
if (checkForMigration(parent, hookContext)) return true;
String baseName = ptr.getName().getBaseName();
FileObject targetDir = parent.resolveFile(baseName);
runHook("Migrate", hookContext, "createFolder", targetDir);
targetDir.createFolder();
List movePairs = new ArrayList(extensions.length);
for (int i = 0; i < extensions.length; i++) {
String extension = extensions[i];
FileObject src = parent.resolveFile(ptr.getName().getBaseName() + extension);
logger.finer("src=" + src);
if (!src.exists()) continue;
FileObject dest = targetDir.resolveFile("index" + extension);
movePairs.add(new FileObject[] {src, dest});
}
runHook("Migrate", hookContext, "preMove", movePairs);
for (int i = 0; i < movePairs.size(); i++) {
FileObject[] pair = (FileObject[]) movePairs.get(i);
pair[0].moveTo(pair[1]);
}
runHook("Migrate", hookContext, "postMove", movePairs);
return true;
} catch (IOException e) {
throw new EditorException("Couldn't migrate(" + ptr.getName().getPath() + ")", e);
} catch (ServletException e) {
throw new EditorException("Couldn't migrate(" + ptr.getName().getPath() + ")", e);
}
}
protected Object runHook(String hook, Map context) throws IOException, ServletException {
return runHook(hook, context, null, null);
}
protected Object runHook(String hook, Map context, String command, Object payload) throws IOException, ServletException {
return servletContext.run(servletContext.resolve("/WEB-INF/Events/Hooks/Edit/" + hook), context, command, payload);
}
private static class SyncFutureTask extends FutureTask {
public SyncFutureTask(Callable callable) {
super(callable);
}
public SyncFutureTask(Runnable runnable, Object result) {
super(runnable, result);
}
public Object get() throws ExecutionException, InterruptedException {
run();
return super.get();
}
public Object get(long timeout, TimeUnit unit) throws ExecutionException, InterruptedException, TimeoutException {
run();
return super.get(timeout, unit);
}
}
protected static class ContentFetcher implements Callable {
protected final Content content;
protected ContentFetcher(Content content) {
this.content = content;
}
public Object call() throws Exception {
content.fetchContent();
return content.content;
}
}
private static abstract class Content {
protected FileItem content;
private long lastModifiedTime;
private final FutureTask task;
protected Content() {
task = new SyncFutureTask(new ContentFetcher(this));
}
public void cancel() {
task.cancel(true);
if (content != null) content.delete();
}
protected abstract void fetchContent() throws Exception;
protected void setContent(FileItem content, long lastModifiedTime) {
this.content = content;
this.lastModifiedTime = lastModifiedTime;
}
protected boolean hasContent() {
return content != null;
}
protected FileItem getContent() throws EditorException {
try {
try {
return (FileItem) task.get();
} catch (ExecutionException e) {
throw e.getCause();
}
} catch (RuntimeException e) {
throw e;
} catch (Error e) {
throw e;
} catch (Throwable e) {
throw (EditorException) new EditorException(e.getMessage()).initCause(e);
}
}
protected long getLastModifiedTime() throws EditorException {
getContent();
return lastModifiedTime;
}
}
private static class ExistingContent extends Content {
private final FileObject file;
protected ExistingContent(FileObject file) {
this.file = file;
}
protected void fetchContent() throws Exception {
FileItem content = createFileItem("content", file.getContent().getContentInfo().getContentType(), false, null);
IOUtil.copy(file.getContent().getInputStream(), true, content.getOutputStream(), true);
long lastModifiedTime = file.getContent().getLastModifiedTime();
setContent(content, lastModifiedTime);
}
}
private static class UploadedContent extends Content {
private final FileItem content;
private long lastModifiedTime;
protected UploadedContent(FileItem content) {
this.content = content;
this.lastModifiedTime = System.currentTimeMillis();
}
protected boolean hasContent() {
return true;
}
protected void fetchContent() throws Exception {
setContent(content, lastModifiedTime);
}
}
public class Context implements Serializable {
public final String filePath;
protected final HashMap originalAttributes = new HashMap();
protected final HashMap attributes = new HashMap();
protected final HashSet deletedAttributes = new HashSet();
protected boolean exists;
protected boolean modified;
protected Content content;
protected final HashMap hookDatas = new HashMap();
protected Context(String filePath) throws EditorException {
this.filePath = filePath;
load();
}
// Hooks for EditorContentContext
FileItem getContent() throws EditorException {
return content.getContent();
}
public Map getHookData(String hookName) {
Map hookData = (Map) hookDatas.get(hookName);
if (hookData == null) {
hookData = new HashMap();
hookDatas.put(hookName, hookData);
}
return hookData;
}
public void setAttribute(String name, Object value) {
if (value == null) {
deleteAttribute(name);
return;
}
attributes.put(name, value);
deletedAttributes.remove(name);
}
public void setAllAttributes(Map toSet) {
Iterator it = toSet.entrySet().iterator();
while (it.hasNext()) {
Map.Entry entry = (Map.Entry) it.next();
setAttribute((String) entry.getKey(), entry.getValue());
}
}
public void deleteAttribute(String name) {
attributes.remove(name);
deletedAttributes.add(name);
}
public void deleteAllAttributes(Set toDelete) {
deletedAttributes.addAll(toDelete);
}
public Set getDeletedAttributes() {
return Collections.unmodifiableSet(deletedAttributes);
}
public Object getAttribute(String name) {
if (deletedAttributes.contains(name)) return null;
return attributes.get(name);
}
public Map getAttributes() {
return Collections.unmodifiableMap(attributes);
}
public boolean hasContent() {
return content.hasContent();
}
public boolean exists() {
return exists;
}
public boolean isModified() {
return modified;
}
public long getLastModifiedTime() throws EditorException {
return content.getLastModifiedTime();
}
public String getString() throws EditorException {
try {
return content != null ? IOUtil.readString(content.getContent().getInputStream()) : null;
} catch (EditorException e) {
throw e;
} catch (RuntimeException e) {
throw e;
} catch (Error e) {
throw e;
} catch (Throwable e) {
throw new EditorException("Couldn't get string from content(" + filePath + ")", e);
}
}
public String getContentType() throws EditorException {
return (String) getAttribute("content-type");
}
public String getContentEncoding() throws EditorException {
return (String) getAttribute("content-encoding");
}
protected FileObject getFile() throws IOException {
return getFileInfo().getFile();
}
void clearContent() {
if (content != null) {
content.cancel();
content = null;
}
}
public void setContent(FileItem content) {
modified = true;
clearContent();
this.content = new UploadedContent(content);
}
public void clear() {
attributes.clear();
clearContent();
deletedAttributes.clear();
originalAttributes.clear();
}
protected void load() throws EditorException {
try {
FileObject file = getFileInfo().getFile();
clearContent();
if (file.exists()) {
Iterator it = file.getContent().getAttributes().entrySet().iterator();
while (it.hasNext()) {
Map.Entry entry = (Map.Entry) it.next();
originalAttributes.put((String) entry.getKey(), entry.getValue());
}
attributes.putAll(originalAttributes);
this.content = new ExistingContent(file);
exists = true;
}
} catch (EditorException e) {
throw e;
} catch (IOException e) {
throw new EditorException("Couldn't load content(" + filePath + ")", e);
}
}
protected void checkForMigration(FileObject ptr) throws EditorException, IOException, ServletException {
Editor.this.checkForMigration(ptr, CollectionUtil.toMap("fileInfo", getFileInfo(), "filePath", filePath, "fileContext", this));
}
protected FileInfo checkHook(String hook, String command) throws EditorException, IOException, ServletException {
FileInfo fileInfo = getFileInfo();
Object result = runHook(hook, CollectionUtil.toMap("fileInfo", fileInfo, "filePath", filePath, "fileContext", this));
if (result instanceof Collection) {
Iterator it = ((Collection) result).iterator();
while (it.hasNext()) {
Object hookResult = it.next();
if (hookResult instanceof Boolean && Boolean.FALSE.equals(hookResult)) {
throw new EditorException("Couldn't " + command + "(" + filePath + "), hook disallowed");
}
}
}
return fileInfo;
}
protected void hook(String hook, FileInfo fileInfo) throws IOException, ServletException {
runHook(hook, CollectionUtil.toMap("fileInfo", fileInfo, "filePath", filePath, "fileContext", this));
}
public void delete() throws EditorException {
try {
FileInfo fileInfo = checkHook("PreDelete", "delete");
fileInfo.getFile().delete();
refresh(fileInfo);
hook("PostDelete", fileInfo);
} catch (EditorException e) {
throw e;
} catch (ServletException e) {
throw new EditorException("Couldn't delete file(" + filePath + ")", e);
} catch (IOException e) {
throw new EditorException("Couldn't delete file(" + filePath + ")", e);
}
clear();
}
public void revert() throws EditorException {
clear();
load();
}
public void save() throws EditorException {
hookDatas.clear();
try {
logger.info(this + ".save()");
FileInfo fileInfo = checkHook("PreSave", "save");
FileObject file = fileInfo.getFile();
checkForMigration(file.getParent());
FileContent fileContent = file.getContent();
if (hasContent()) IOUtil.copy(getContent().getInputStream(), true, fileContent.getOutputStream(), true);
Iterator it = deletedAttributes.iterator();
while (it.hasNext()) {
String name = (String) it.next();
if (fileContent.hasAttribute(name)) {
fileContent.removeAttribute(name);
} else {
fileContent.setAttribute(name, null);
}
}
Map defaultAttributes = fileInfo.getDefaultAttributes();
it = attributes.entrySet().iterator();
while (it.hasNext()) {
Map.Entry entry = (Map.Entry) it.next();
String name = (String) entry.getKey();
Object value = entry.getValue();
if (value.equals(defaultAttributes.get(name))) {
fileContent.removeAttribute(name);
} else {
fileContent.setAttribute(name, value);
}
}
refresh(fileInfo);
hook("PostSave", fileInfo);
clear();
load();
} catch (EditorException e) {
throw e;
} catch (ServletException e) {
throw new EditorException("Couldn't save(" + filePath + ")", e);
} catch (IOException e) {
throw new EditorException("Couldn't save(" + filePath + ")", e);
} finally {
hookDatas.clear();
}
}
protected void refresh(FileInfo fileInfo) throws IOException {
FileObject file = servletContext.getFile(filePath);
servletContext.getContainer().getFileInfo(file).refresh();
fileInfo.refresh();
}
protected FileInfo getFileInfo() throws IOException {
try {
FileInfo fileInfo = Editor.this.createFileInfo(prefix + filePath);
FileObject file = fileInfo.getFile();
String contentType = (String) attributes.get("content-type");
if (file.getType().hasChildren()) {
String extension;
if (contentType != null) {
extension = Editor.this.findExtensionByMimeType(contentType);
if (extension == null) extension = Editor.this.getDefaultExtension();
} else {
extension = Editor.this.getDefaultExtension();
}
String name = "index";
String defaultIndex = (String) file.getContent().getAttribute("default-index");
if (defaultIndex != null) name = defaultIndex;
FileObject base = file;
file = base.resolveFile(name);
if (file.exists()) {
fileInfo = Editor.this.createFileInfo(file.getName().getPath());
} else {
if (extension != null && extension.length() > 0) {
file = base.resolveFile(name + '.' + extension);
fileInfo = Editor.this.createFileInfo(file.getName().getPath());
}
}
} else {
if (file.getName().getExtension().length() == 0) {
// no extension
String extension;
if (contentType == null && !servletContext.getContainer().getDefaultMimeType().equals(fileInfo.getContentType())) {
contentType = (String) fileInfo.getAttribute("content-type");
}
if (contentType != null) {
extension = Editor.this.findExtensionByMimeType(contentType);
if (extension == null) extension = Editor.this.getDefaultExtension();
} else {
extension = Editor.this.getDefaultExtension();
}
if (extension != null && extension.length() > 0) {
file = file.getParent().resolveFile(file.getName().getBaseName() + '.' + extension);
fileInfo = Editor.this.createFileInfo(file.getName().getPath());
}
}
}
return fileInfo;
} catch (EditorException e) {
throw e;
} catch (IOException e) {
throw new EditorException("Couldn't find file info(" + filePath + ")", e);
}
}
public String getType() throws EditorException {
try {
String type = (String) attributes.get("editor-type");
if (type != null) return type;
FileInfo fileInfo = getFileInfo();
type = (String) fileInfo.getAttribute("editor-type");
if (type != null) return type;
return Editor.this.getDefaultEditor();
} catch (EditorException e) {
throw e;
} catch (IOException e) {
throw new EditorException("Couldn't find editor type(" + filePath + ")", e);
}
}
public String toString() {
return "EditorContext(" + filePath + ")";
}
}
}