Java tutorial
/** * * Copyright 2011-2015 Martin Goellnitz * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. * */ package org.tangram.components.editor; import com.thoughtworks.xstream.XStream; import com.thoughtworks.xstream.io.xml.StaxDriver; import java.io.IOException; import java.io.Reader; import java.io.StringReader; import java.lang.reflect.Method; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Set; import javax.annotation.PostConstruct; import javax.inject.Inject; import javax.inject.Named; import javax.inject.Singleton; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.apache.commons.lang.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.tangram.Constants; import org.tangram.annotate.ActionParameter; import org.tangram.annotate.LinkAction; import org.tangram.annotate.LinkHandler; import org.tangram.annotate.LinkPart; import org.tangram.content.CodeResource; import org.tangram.content.Content; import org.tangram.controller.AbstractRenderingBase; import org.tangram.editor.AppEngineXStream; import org.tangram.link.Link; import org.tangram.link.LinkHandlerRegistry; import org.tangram.logic.ClassRepository; import org.tangram.mutable.MutableBeanFactory; import org.tangram.util.JavaBean; import org.tangram.view.PropertyConverter; import org.tangram.view.RequestParameterAccess; import org.tangram.view.TargetDescriptor; import org.tangram.view.Utils; import org.tangram.view.ViewUtilities; /** * Request handling component for the editing facility. * * This class does all the magic not found in the templates of the editor including URL generation and handling. */ @Named @Singleton @LinkHandler public class EditingHandler extends AbstractRenderingBase { private static final Logger LOG = LoggerFactory.getLogger(EditingHandler.class); public static final String EDIT_TARGET = "_tangram_editor"; public static final String EDIT_VIEW = "edit"; public static final String PARAMETER_CLASS_NAME = "cms.editor.class.name"; public static final String PARAMETER_ID = "cms.editor.id"; public static final String PARAMETER_PROPERTY = "cms.editor.property.name"; public static final String ATTRIBUTE_WRAPPER = "wrapper"; /** * writable properties which should not be altered by the upper layers or persisted */ public static final Set<String> SYSTEM_PROPERTIES; /** * editing actions triggered by URLs containing the obect's ID. */ public static final Collection<String> ID_URL_ACTIONS = new ArrayList<>(); /** * editing actions triggered only by parameters passed in http post requests. */ public static final Collection<String> PARAMETER_ACTIONS = new ArrayList<>(); @Inject private LinkHandlerRegistry registry; @Inject private PropertyConverter propertyConverter; @Inject private ClassRepository classRepository; @Inject private ViewUtilities viewUtilities; private boolean deleteMethodEnabled; static { SYSTEM_PROPERTIES = new HashSet<>(); // The groovy compiler seems to use this SYSTEM_PROPERTIES.add("metaClass"); SYSTEM_PROPERTIES.add("manager"); SYSTEM_PROPERTIES.add("beanFactory"); ID_URL_ACTIONS.add("delete"); ID_URL_ACTIONS.add("edit"); ID_URL_ACTIONS.add("store"); PARAMETER_ACTIONS.add("create"); PARAMETER_ACTIONS.add("link"); PARAMETER_ACTIONS.add("list"); } // static /** * Returns the class the developer modeled. * This isn't necessarily the same class as the system uses. * * @param cls class of an ORM obtained object * @return Class the developer implemented */ public static Class<? extends Object> getDesignClass(Class<? extends Content> cls) { return (cls.getName().indexOf('$') < 0) ? cls : cls.getSuperclass(); } // getDesignClass() /** * Returns a string note about the ORM class state. * * @param cls * @return note indicating the underlying ORM implementation and class modification state * @throws SecurityException */ public static String getOrmNote(Class<? extends Content> cls) throws SecurityException { Method[] methods = cls.getMethods(); String note = "Plain"; for (Method method : methods) { if (method.getName().startsWith("_ebean")) { note = "EBean enhanced"; } // if if (method.getName().startsWith("jdo")) { note = "DataNucleus JDO/JPA Enhanced"; } // if if (method.getName().startsWith("pc")) { note = "OpenJPA Enhanced"; } // if if (method.getName().startsWith("_persistence")) { note = "EclipseLink Woven (Weaved)"; } // if if (method.getName().startsWith("$$_hibernate")) { note = "Hibernate Enhanced"; } // if } // for if (cls.getName().startsWith("org.apache.openjpa.enhance")) { note = "OpenJPA Subclass"; } // if return note; } // getOrmNote() public void setDeleteMethodEnabled(boolean deleteMethodEnabled) { this.deleteMethodEnabled = deleteMethodEnabled; } // setDeleteMethodEnabled() private MutableBeanFactory getMutableBeanFactory() { return (MutableBeanFactory) getBeanFactory(); } // getMutableBeanFactory() private TargetDescriptor describeTarget(Content bean) throws IOException { return new TargetDescriptor(bean, null, EDIT_VIEW); } // redirect() @LinkAction("/store/id_(.*)") public TargetDescriptor store(@LinkPart(1) String id, HttpServletRequest request, HttpServletResponse response) throws Exception { if (request.getAttribute(Constants.ATTRIBUTE_ADMIN_USER) == null) { throw new Exception("User may not edit"); } // if Content bean = beanFactory.getBean(Content.class, id); JavaBean wrapper = new JavaBean(bean); Map<String, Object> newValues = new HashMap<>(); // List<String> deleteValues = new ArrayList<>(); RequestParameterAccess parameterAccess = viewUtilities.createParameterAccess(request); Map<String, String[]> parameterMap = parameterAccess.getParameterMap(); LOG.debug("store() # parameters {} for {}", parameterMap.size(), request.getClass().getName()); for (Map.Entry<String, String[]> entry : parameterMap.entrySet()) { Entry<String, String[]> parameter = entry; String key = parameter.getKey(); if (!key.startsWith("cms.editor")) { try { String[] values = parameter.getValue(); if (LOG.isInfoEnabled()) { StringBuilder msg = new StringBuilder(128); msg.append("store() "); msg.append(key); msg.append(": "); for (String value : values) { msg.append(value); msg.append(' '); } // for LOG.info(msg.toString()); } // if Class<? extends Object> cls = wrapper.getType(key); String valueString = values.length == 1 ? values[0] : ""; Object value = propertyConverter.getStorableObject(bean, valueString, cls, request); LOG.info("store() value={}", value); if (!(Content.class.isAssignableFrom(cls) && value == null)) { newValues.put(key, value); } else { LOG.info("store() not setting value"); } // if if (Content.class.isAssignableFrom(cls) && "".equals(valueString)) { newValues.put(key, null); } // if } catch (Exception e) { throw new Exception("Cannot set value for " + key, e); } // try/catch } // if } // for for (String key : parameterAccess.getBlobNames()) { try { if (!key.startsWith("cms.editor")) { Class<? extends Object> cls = wrapper.getType(key); if (propertyConverter.isBlobType(cls)) { byte[] octets = parameterAccess.getData(key); newValues.put(key, propertyConverter.createBlob(octets)); } // if } // if } catch (Exception e) { throw new Exception("Cannot set value for " + key, e); } // try/catch } // for getMutableBeanFactory().beginTransaction(); wrapper = new JavaBean(bean); Exception e = null; for (Map.Entry<String, Object> property : newValues.entrySet()) { try { wrapper.set(property.getKey(), property.getValue()); } catch (Exception ex) { e = new Exception("Cannot set value for " + property.getKey(), ex); } // try/catch } // for if (!getMutableBeanFactory().persist(bean)) { throw new Exception("Could not persist bean " + bean.getId()); } // if LOG.debug("store() id={}", id); if (e != null) { throw e; } // if return describeTarget(bean); } // store() private String getVariant(HttpServletRequest request) { String agent = request.getHeader("user-agent"); String variant = ""; if (agent.indexOf("iPhone") > 0) { variant = "$mobile"; } // if if (agent.indexOf("Android") > 0) { variant = "$mobile"; } // if if (agent.indexOf("Pre/1.0") > 0) { variant = "$mobile"; } // if if (agent.indexOf("MIDP") > 0) { variant = "$mobile"; } // if LOG.debug("getVariant() agent={} -> {}", agent, variant); return variant; } // getVariant() /** * Load a class for a given type name. * * Don't use a simple ClassLoader.loadClass since this classloader might not know about classes available * from the class repository, so use this simple iterator here. * * @param typeName * @return Class for the given type name or null */ private Class<? extends Content> loadClass(String typeName) { Class<? extends Content> cls = null; for (Class<? extends Content> c : getMutableBeanFactory().getClasses()) { if (c.getName().equals(typeName)) { cls = c; } // if } // for return cls; } // loadClass() @LinkAction("/create") public TargetDescriptor create(@ActionParameter(PARAMETER_CLASS_NAME) String typeName, HttpServletRequest request, HttpServletResponse response) throws Exception { LOG.info("create() creating new instance of type {}", typeName); if (request.getAttribute(Constants.ATTRIBUTE_ADMIN_USER) == null) { throw new Exception("User may not edit"); } // if Class<? extends Content> cls = loadClass(typeName); Content content = getMutableBeanFactory().createBean(cls); if (getMutableBeanFactory().persist(content)) { LOG.debug("create() content={}", content); LOG.debug("create() id={}", content.getId()); return describeTarget(content); } // if throw new Exception("Cannot persist new " + typeName); } // create() @LinkAction("/list") public TargetDescriptor list(@ActionParameter(PARAMETER_CLASS_NAME) String typeName, HttpServletRequest request, HttpServletResponse response) { LOG.info("list() listing instances of type '{}'", typeName); if (request.getAttribute(Constants.ATTRIBUTE_ADMIN_USER) == null) { throw new RuntimeException("User may not edit"); } // if Collection<Class<? extends Content>> classes = getMutableBeanFactory().getClasses(); Class<? extends Content> cls = null; // take first one of classes available if (!classes.isEmpty()) { cls = classes.iterator().next(); } // if // try to take class from provided classes if (StringUtils.isNotBlank(typeName)) { for (Class<? extends Content> c : classes) { if (c.getName().equals(typeName)) { cls = c; } // if } // for } // if List<? extends Content> contents = Collections.emptyList(); if (cls != null) { contents = beanFactory.listBeansOfExactClass(cls); try { Collections.sort(contents); } catch (Exception e) { LOG.error("list() error while sorting", e); } // try/catch } // if response.setContentType("text/html; charset=UTF-8"); request.setAttribute(Constants.THIS, contents); request.setAttribute(Constants.ATTRIBUTE_REQUEST, request); request.setAttribute(Constants.ATTRIBUTE_RESPONSE, response); request.setAttribute("classes", classes); request.setAttribute("canDelete", deleteMethodEnabled); request.setAttribute("prefix", Utils.getUriPrefix(request)); if (cls != null) { Class<? extends Object> designClass = (cls.getName().indexOf('$') < 0) ? cls : cls.getSuperclass(); request.setAttribute("designClass", designClass); request.setAttribute("designClassPackage", designClass.getPackage()); } // if return new TargetDescriptor(contents, "tangramEditorList" + getVariant(request), null); } // list() @LinkAction("/edit/id_(.*)") public TargetDescriptor edit(@LinkPart(1) String id, HttpServletRequest request, HttpServletResponse response) throws Exception { LOG.info("edit() editing {}", id); if (request.getAttribute(Constants.ATTRIBUTE_ADMIN_USER) == null) { throw new Exception("User may not edit"); } // if response.setContentType("text/html; charset=UTF-8"); Content content = beanFactory.getBean(id); if (content == null) { response.sendError(HttpServletResponse.SC_NOT_FOUND, "no content with id " + id + " in repository."); return null; } // if if (content instanceof CodeResource) { CodeResource code = (CodeResource) content; request.setAttribute("compilationErrors", classRepository.getCompilationErrors().get(code.getAnnotation())); } // if request.setAttribute("beanFactory", getMutableBeanFactory()); request.setAttribute("propertyConverter", propertyConverter); request.setAttribute("classes", getMutableBeanFactory().getClasses()); request.setAttribute("prefix", Utils.getUriPrefix(request)); Class<? extends Content> cls = content.getClass(); String note = getOrmNote(cls); Class<? extends Object> designClass = getDesignClass(cls); request.setAttribute("note", note); request.setAttribute("contentClass", cls); request.setAttribute("designClass", designClass); request.setAttribute("designClassPackage", designClass.getPackage()); return new TargetDescriptor(content, "edit" + getVariant(request), null); } // edit() @LinkAction("/link") public TargetDescriptor link(@ActionParameter(PARAMETER_CLASS_NAME) String typeName, @ActionParameter(PARAMETER_ID) String id, @ActionParameter(PARAMETER_PROPERTY) String propertyName, HttpServletRequest request, HttpServletResponse response) throws Exception { LOG.info("link() creating new instance of type {}", typeName); if (request.getAttribute(Constants.ATTRIBUTE_ADMIN_USER) == null) { throw new Exception("User may not edit"); } // if Class<? extends Content> cls = loadClass(typeName); Content content = getMutableBeanFactory().createBean(cls); if (getMutableBeanFactory().persist(content)) { LOG.debug("link() content={}", content); LOG.debug("link() id={}", content.getId()); // get bean here for update to avoid xg transactions where ever possible Content bean = getMutableBeanFactory().getBean(Content.class, id); getMutableBeanFactory().beginTransaction(); JavaBean wrapper = new JavaBean(bean); Object listObject = wrapper.get(propertyName); List<Content> list = convertList(listObject); list.add(content); wrapper.set(propertyName, list); getMutableBeanFactory().persist(bean); return describeTarget(content); } else { throw new Exception("could not create new instance of type " + typeName); } // if } // link() @LinkAction("/delete/id_(.*)") public TargetDescriptor delete(@LinkPart(1) String id, HttpServletRequest request, HttpServletResponse response) throws Exception { LOG.info("delete() trying to delete instance {}", id); if (request.getAttribute(Constants.ATTRIBUTE_ADMIN_USER) == null) { throw new Exception("User may not edit"); } // if if (!deleteMethodEnabled) { throw new Exception("Object deletion not activated"); } // if Content bean = getMutableBeanFactory().getBean(Content.class, id); if (bean == null) { throw new Exception("No object found for deletion of id " + id); } // if String typeName = bean.getClass().getName(); getMutableBeanFactory().beginTransaction(); getMutableBeanFactory().delete(bean); return list(typeName, request, response); } // delete() @LinkAction("/export") public TargetDescriptor contentExport(HttpServletRequest request, HttpServletResponse response) throws Exception { if (request.getAttribute(Constants.ATTRIBUTE_ADMIN_USER) == null) { throw new Exception("User may not execute action"); } // if response.setContentType("text/xml"); response.setCharacterEncoding("UTF-8"); // The pure reflection provider is used because of Google App Engines API limitations XStream xstream = new AppEngineXStream(new StaxDriver()); Collection<Class<? extends Content>> classes = getMutableBeanFactory().getClasses(); // Dig out root class of all this evil to find out where the id field is defined Class<? extends Object> oneClass = classes.iterator().next(); while (oneClass.getSuperclass() != Object.class) { oneClass = oneClass.getSuperclass(); } // while LOG.info("contentExport() root class to ignore fields in: {}", oneClass.getName()); xstream.omitField(oneClass, "id"); xstream.omitField(oneClass, "ebeanInternalId"); final Class<? extends Content> baseClass = getMutableBeanFactory().getBaseClass(); if (baseClass != oneClass) { LOG.info("contentExport() additional base class to ignore fields in: {}", oneClass.getName()); xstream.omitField(baseClass, "id"); xstream.omitField(baseClass, "beanFactory"); xstream.omitField(baseClass, "gaeBeanFactory"); xstream.omitField(baseClass, "ebeanInternalId"); } // if for (Class<? extends Content> c : classes) { LOG.info("contentExport() aliasing and ignoring fields for {}", c.getName()); xstream.omitField(c, "beanFactory"); xstream.omitField(c, "gaeBeanFactory"); xstream.omitField(c, "userServices"); xstream.alias(c.getSimpleName(), c); } // for Collection<Content> allContent = new ArrayList<>(); for (Class<? extends Content> c : classes) { try { allContent.addAll(beanFactory.listBeansOfExactClass(c)); } catch (Exception e) { LOG.error("contentExport()/list", e); } // try/catch } // for try { xstream.toXML(allContent, response.getWriter()); response.getWriter().flush(); } catch (IOException e) { LOG.error("contentExport()/toxml", e); } // try/catch return TargetDescriptor.DONE; } // contentExport() /** * Small helper method to heep areas with suppressed warnings small. * * @param contents * @return */ @SuppressWarnings("unchecked") private List<Content> convertList(Object contents) { return (List<Content>) contents; } // convertList() private TargetDescriptor doImport(Reader input, HttpServletRequest request) throws Exception { if (request.getAttribute(Constants.ATTRIBUTE_ADMIN_USER) == null) { throw new Exception("User may not execute action"); } // if getMutableBeanFactory().beginTransaction(); XStream xstream = new AppEngineXStream(new StaxDriver()); Collection<Class<? extends Content>> classes = getMutableBeanFactory().getClasses(); for (Class<? extends Content> c : classes) { xstream.alias(c.getSimpleName(), c); } // for Object contents = xstream.fromXML(input); LOG.info("read() {}", contents); if (contents instanceof List) { List<? extends Content> list = convertList(contents); for (Content o : list) { LOG.info("read() {}", o); getMutableBeanFactory().persistUncommitted(o); } // for } // if getMutableBeanFactory().commitTransaction(); return TargetDescriptor.DONE; } // doImport() @LinkAction("/import-text") public TargetDescriptor contentImport(@ActionParameter("xmltext") String xmltext, HttpServletRequest request) throws Exception { return doImport(new StringReader(xmltext), request); } // contentImport() @LinkAction("/import") public TargetDescriptor contentImport(HttpServletRequest request) throws Exception { RequestParameterAccess parameterAccess = viewUtilities.createParameterAccess(request); return doImport(new StringReader(new String(parameterAccess.getData("xmlfile"), "UTF-8")), request); } // contentImport() @LinkAction("/importer") public TargetDescriptor importer(@ActionParameter("xmltext") String xmltext, HttpServletRequest request, HttpServletResponse response) throws Exception { request.setAttribute("classes", getMutableBeanFactory().getClasses()); response.setContentType("text/html"); response.setCharacterEncoding("UTF-8"); return new TargetDescriptor(this, null, null); } // importer() private String getUrl(Object bean, String action, String view) { if ("edit".equals(view)) { action = view; } // if if (ID_URL_ACTIONS.contains(action)) { return "/" + action + "/id_" + ((Content) bean).getId(); } else { return PARAMETER_ACTIONS.contains(action) ? "/" + action : null; } // if } // getUrl() @Override public Link createLink(HttpServletRequest request, HttpServletResponse r, Object bean, String action, String view) { Link result = null; String url = getUrl(bean, action, view); if (url != null) { result = new Link(); result.setUrl(url); if ("edit".equals(action)) { result.setTarget(EDIT_TARGET); String jsOpenWindow = "window.open('" + result.getUrl() + "', '" + EDIT_TARGET + "', 'menubar=no,status=no,toolbar=no,resizable=yes, scrollbars=yes');"; result.addHandler("onclick", jsOpenWindow); } // if } // if return result; } // createLink() @PostConstruct public void afterPropertiesSet() { registry.registerLinkHandler(this); } // afterPropertiesSet() } // EditingHandler