You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@sling.apache.org by bd...@apache.org on 2007/10/11 15:56:27 UTC
svn commit: r583826 - in /incubator/sling/whiteboard/microsling/src/main:
java/org/apache/sling/microsling/api/
java/org/apache/sling/microsling/request/
java/org/apache/sling/microsling/requestcontext/
java/org/apache/sling/microsling/resource/ java/o...
Author: bdelacretaz
Date: Thu Oct 11 06:56:26 2007
New Revision: 583826
URL: http://svn.apache.org/viewvc?rev=583826&view=rev
Log:
SLING-47 - microsling: SlingPostServlet added, allows for simple content creation
Added:
incubator/sling/whiteboard/microsling/src/main/java/org/apache/sling/microsling/api/SlingException.java (with props)
incubator/sling/whiteboard/microsling/src/main/java/org/apache/sling/microsling/requestcontext/
incubator/sling/whiteboard/microsling/src/main/java/org/apache/sling/microsling/requestcontext/SlingRequestContext.java (with props)
incubator/sling/whiteboard/microsling/src/main/java/org/apache/sling/microsling/resource/JcrNodeResource.java (with props)
incubator/sling/whiteboard/microsling/src/main/java/org/apache/sling/microsling/slingservlets/SlingPostServlet.java (with props)
Modified:
incubator/sling/whiteboard/microsling/src/main/java/org/apache/sling/microsling/api/RequestFilter.java
incubator/sling/whiteboard/microsling/src/main/java/org/apache/sling/microsling/request/RequestAttributeNames.java
incubator/sling/whiteboard/microsling/src/main/java/org/apache/sling/microsling/resource/ResourceResolverFilter.java
incubator/sling/whiteboard/microsling/src/main/java/org/apache/sling/microsling/servlet/MicroSlingServlet.java
incubator/sling/whiteboard/microsling/src/main/java/org/apache/sling/microsling/slingservlets/DefaultSlingServlet.java
incubator/sling/whiteboard/microsling/src/main/webapp/index.html
incubator/sling/whiteboard/microsling/src/main/webapp/microsling.css
Modified: incubator/sling/whiteboard/microsling/src/main/java/org/apache/sling/microsling/api/RequestFilter.java
URL: http://svn.apache.org/viewvc/incubator/sling/whiteboard/microsling/src/main/java/org/apache/sling/microsling/api/RequestFilter.java?rev=583826&r1=583825&r2=583826&view=diff
==============================================================================
--- incubator/sling/whiteboard/microsling/src/main/java/org/apache/sling/microsling/api/RequestFilter.java (original)
+++ incubator/sling/whiteboard/microsling/src/main/java/org/apache/sling/microsling/api/RequestFilter.java Thu Oct 11 06:56:26 2007
@@ -23,5 +23,5 @@
* client making the request, etc., etc.
*/
public interface RequestFilter {
- void doFilter(HttpServletRequest req);
+ void doFilter(HttpServletRequest req) throws Exception;
}
Added: incubator/sling/whiteboard/microsling/src/main/java/org/apache/sling/microsling/api/SlingException.java
URL: http://svn.apache.org/viewvc/incubator/sling/whiteboard/microsling/src/main/java/org/apache/sling/microsling/api/SlingException.java?rev=583826&view=auto
==============================================================================
--- incubator/sling/whiteboard/microsling/src/main/java/org/apache/sling/microsling/api/SlingException.java (added)
+++ incubator/sling/whiteboard/microsling/src/main/java/org/apache/sling/microsling/api/SlingException.java Thu Oct 11 06:56:26 2007
@@ -0,0 +1,14 @@
+package org.apache.sling.microsling.api;
+
+/** Base class for Sling exceptions */
+public class SlingException extends Exception {
+ private static final long serialVersionUID = 1L;
+
+ public SlingException(String reason) {
+ super(reason);
+ }
+
+ public SlingException(String reason,Throwable cause) {
+ super(reason,cause);
+ }
+}
Propchange: incubator/sling/whiteboard/microsling/src/main/java/org/apache/sling/microsling/api/SlingException.java
------------------------------------------------------------------------------
svn:eol-style = native
Propchange: incubator/sling/whiteboard/microsling/src/main/java/org/apache/sling/microsling/api/SlingException.java
------------------------------------------------------------------------------
svn:keywords = Author Date Id Revision Rev URL
Modified: incubator/sling/whiteboard/microsling/src/main/java/org/apache/sling/microsling/request/RequestAttributeNames.java
URL: http://svn.apache.org/viewvc/incubator/sling/whiteboard/microsling/src/main/java/org/apache/sling/microsling/request/RequestAttributeNames.java?rev=583826&r1=583825&r2=583826&view=diff
==============================================================================
--- incubator/sling/whiteboard/microsling/src/main/java/org/apache/sling/microsling/request/RequestAttributeNames.java (original)
+++ incubator/sling/whiteboard/microsling/src/main/java/org/apache/sling/microsling/request/RequestAttributeNames.java Thu Oct 11 06:56:26 2007
@@ -21,4 +21,5 @@
String PREFIX = "sling.";
String REQUEST_ATTR_RESOURCE = PREFIX + "resource";
+ String REQUEST_ATTR_CONTEXT = PREFIX + "context";
}
Added: incubator/sling/whiteboard/microsling/src/main/java/org/apache/sling/microsling/requestcontext/SlingRequestContext.java
URL: http://svn.apache.org/viewvc/incubator/sling/whiteboard/microsling/src/main/java/org/apache/sling/microsling/requestcontext/SlingRequestContext.java?rev=583826&view=auto
==============================================================================
--- incubator/sling/whiteboard/microsling/src/main/java/org/apache/sling/microsling/requestcontext/SlingRequestContext.java (added)
+++ incubator/sling/whiteboard/microsling/src/main/java/org/apache/sling/microsling/requestcontext/SlingRequestContext.java Thu Oct 11 06:56:26 2007
@@ -0,0 +1,62 @@
+package org.apache.sling.microsling.requestcontext;
+
+import javax.jcr.Repository;
+import javax.jcr.Session;
+import javax.jcr.SimpleCredentials;
+import javax.servlet.ServletContext;
+import javax.servlet.http.HttpServletRequest;
+
+import org.apache.sling.microsling.api.SlingException;
+import org.apache.sling.microsling.request.RequestAttributeNames;
+
+public class SlingRequestContext {
+
+ private final Session session;
+ private final String contentPath;
+ private final String extension;
+
+ /** Get the SlingRequestContext from req */
+ public static SlingRequestContext getFromRequest(HttpServletRequest req) throws SlingException {
+ final SlingRequestContext ctx = (SlingRequestContext)req.getAttribute(RequestAttributeNames.REQUEST_ATTR_CONTEXT);
+ if(ctx == null) {
+ throw new SlingException("SlingRequestContext not found in HttpServletRequest attributes");
+ }
+ return ctx;
+ }
+
+ /** TODO parse the request to extract extension, selectors, etc. */
+ public SlingRequestContext(ServletContext sctx,HttpServletRequest req) throws Exception {
+ // Access our Repository and get a Session
+ final String repoAttr = Repository.class.getName();
+ final Repository rep = (Repository)sctx.getAttribute(repoAttr);
+ if(rep==null) {
+ throw new SlingException("Repository not available in ServletContext attribute " + repoAttr);
+ }
+
+ // TODO use the user's credentials
+ session = rep.login(new SimpleCredentials("admin", "admin".toCharArray()));
+
+ // TODO more precise parsing: selectors, suffix, etc.
+ final String path = req.getPathInfo();
+ final int lastDot = path.lastIndexOf('.');
+ if(lastDot < 0) {
+ contentPath = path;
+ extension = "";
+ } else {
+ contentPath = path.substring(0,lastDot);
+ extension = path.substring(lastDot + 1);
+ }
+ }
+
+ public Session getSession() {
+ return session;
+ }
+
+ public String getContentPath() {
+ return contentPath;
+ }
+
+ public String getExtension() {
+ return extension;
+ }
+}
Propchange: incubator/sling/whiteboard/microsling/src/main/java/org/apache/sling/microsling/requestcontext/SlingRequestContext.java
------------------------------------------------------------------------------
svn:eol-style = native
Propchange: incubator/sling/whiteboard/microsling/src/main/java/org/apache/sling/microsling/requestcontext/SlingRequestContext.java
------------------------------------------------------------------------------
svn:keywords = Author Date Id Revision Rev URL
Added: incubator/sling/whiteboard/microsling/src/main/java/org/apache/sling/microsling/resource/JcrNodeResource.java
URL: http://svn.apache.org/viewvc/incubator/sling/whiteboard/microsling/src/main/java/org/apache/sling/microsling/resource/JcrNodeResource.java?rev=583826&view=auto
==============================================================================
--- incubator/sling/whiteboard/microsling/src/main/java/org/apache/sling/microsling/resource/JcrNodeResource.java (added)
+++ incubator/sling/whiteboard/microsling/src/main/java/org/apache/sling/microsling/resource/JcrNodeResource.java Thu Oct 11 06:56:26 2007
@@ -0,0 +1,26 @@
+package org.apache.sling.microsling.resource;
+
+import javax.jcr.Node;
+import javax.jcr.RepositoryException;
+
+import org.apache.sling.microsling.api.Resource;
+
+class JcrNodeResource implements Resource {
+ final Node node;
+ final String path;
+
+ JcrNodeResource(javax.jcr.Session s,String path) throws RepositoryException {
+ node = (Node)s.getItem(path);
+ this.path = node.getPath();
+ }
+
+ public Object getData() {
+ return node;
+ }
+
+ public String getURI() {
+ return path;
+ }
+
+
+}
Propchange: incubator/sling/whiteboard/microsling/src/main/java/org/apache/sling/microsling/resource/JcrNodeResource.java
------------------------------------------------------------------------------
svn:eol-style = native
Propchange: incubator/sling/whiteboard/microsling/src/main/java/org/apache/sling/microsling/resource/JcrNodeResource.java
------------------------------------------------------------------------------
svn:keywords = Author Date Id Revision Rev URL
Modified: incubator/sling/whiteboard/microsling/src/main/java/org/apache/sling/microsling/resource/ResourceResolverFilter.java
URL: http://svn.apache.org/viewvc/incubator/sling/whiteboard/microsling/src/main/java/org/apache/sling/microsling/resource/ResourceResolverFilter.java?rev=583826&r1=583825&r2=583826&view=diff
==============================================================================
--- incubator/sling/whiteboard/microsling/src/main/java/org/apache/sling/microsling/resource/ResourceResolverFilter.java (original)
+++ incubator/sling/whiteboard/microsling/src/main/java/org/apache/sling/microsling/resource/ResourceResolverFilter.java Thu Oct 11 06:56:26 2007
@@ -21,25 +21,22 @@
import org.apache.sling.microsling.api.RequestFilter;
import org.apache.sling.microsling.api.Resource;
import org.apache.sling.microsling.request.RequestAttributeNames;
+import org.apache.sling.microsling.requestcontext.SlingRequestContext;
/** A RequestFilter that selects the Resource to process */
public class ResourceResolverFilter implements RequestFilter {
- public void doFilter(HttpServletRequest req) {
- final String path = req.getPathInfo();
+ public void doFilter(HttpServletRequest req) throws Exception {
- // TODO use the JCR Repository for this!
- final Resource r = new Resource() {
- public Object getData() {
- return "This is the Resource for path " + path;
- }
- public String getURI() {
- return path;
- }
- };
-
- // end requests with "notfound" to test the "not found" response
- if(!path.endsWith("notfound")) {
+ final SlingRequestContext ctx = SlingRequestContext.getFromRequest(req);
+
+ // TODO support OCM-mapped objects as well
+ Resource r = null;
+ if(ctx.getSession().itemExists(ctx.getContentPath())) {
+ r = new JcrNodeResource(ctx.getSession(),ctx.getContentPath());
+ }
+
+ if(r != null) {
req.setAttribute(RequestAttributeNames.REQUEST_ATTR_RESOURCE, r);
}
}
Modified: incubator/sling/whiteboard/microsling/src/main/java/org/apache/sling/microsling/servlet/MicroSlingServlet.java
URL: http://svn.apache.org/viewvc/incubator/sling/whiteboard/microsling/src/main/java/org/apache/sling/microsling/servlet/MicroSlingServlet.java?rev=583826&r1=583825&r2=583826&view=diff
==============================================================================
--- incubator/sling/whiteboard/microsling/src/main/java/org/apache/sling/microsling/servlet/MicroSlingServlet.java (original)
+++ incubator/sling/whiteboard/microsling/src/main/java/org/apache/sling/microsling/servlet/MicroSlingServlet.java Thu Oct 11 06:56:26 2007
@@ -17,6 +17,8 @@
package org.apache.sling.microsling.servlet;
import java.io.IOException;
+import java.io.PrintWriter;
+import java.io.StringWriter;
import java.util.LinkedList;
import java.util.List;
@@ -29,8 +31,11 @@
import org.apache.sling.microsling.api.RequestFilter;
import org.apache.sling.microsling.api.SlingServlet;
import org.apache.sling.microsling.etc.HttpConstants;
+import org.apache.sling.microsling.request.RequestAttributeNames;
+import org.apache.sling.microsling.requestcontext.SlingRequestContext;
import org.apache.sling.microsling.resource.ResourceResolverFilter;
import org.apache.sling.microsling.slingservlets.DefaultSlingServlet;
+import org.apache.sling.microsling.slingservlets.SlingPostServlet;
/** The main Microsling servlet: processes requests using the Sling filters
* to select a Resource and to apply processing to it.
@@ -50,6 +55,7 @@
requestFilters.add(new ResourceResolverFilter());
// TODO use OSGi to setup this list
+ servlets.add(new SlingPostServlet());
servlets.add(new DefaultSlingServlet());
}
@@ -60,6 +66,8 @@
throws ServletException, IOException {
try {
+ final SlingRequestContext ctx = new SlingRequestContext(getServletContext(),req);
+ req.setAttribute(RequestAttributeNames.REQUEST_ATTR_CONTEXT, ctx);
// let our RequestFilters enhance the request information
// (select a Resource to act on, etc)
for(RequestFilter rf : requestFilters) {
@@ -78,8 +86,9 @@
resp.sendError(hts.getStatusCode(),hts.getMessage());
} catch(Exception e) {
- // TODO log or add stack trace
- resp.sendError(500,e.getMessage());
+ final StringWriter sw = new StringWriter();
+ e.printStackTrace(new PrintWriter(sw,true));
+ resp.sendError(500,e.getMessage() + "\n" + sw.toString());
}
}
Modified: incubator/sling/whiteboard/microsling/src/main/java/org/apache/sling/microsling/slingservlets/DefaultSlingServlet.java
URL: http://svn.apache.org/viewvc/incubator/sling/whiteboard/microsling/src/main/java/org/apache/sling/microsling/slingservlets/DefaultSlingServlet.java?rev=583826&r1=583825&r2=583826&view=diff
==============================================================================
--- incubator/sling/whiteboard/microsling/src/main/java/org/apache/sling/microsling/slingservlets/DefaultSlingServlet.java (original)
+++ incubator/sling/whiteboard/microsling/src/main/java/org/apache/sling/microsling/slingservlets/DefaultSlingServlet.java Thu Oct 11 06:56:26 2007
@@ -19,6 +19,10 @@
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
+import javax.jcr.Node;
+import javax.jcr.Property;
+import javax.jcr.PropertyIterator;
+import javax.jcr.RepositoryException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@@ -40,15 +44,35 @@
throw new HttpStatusCodeException(404,"Resource not found: " + req.getPathInfo());
}
- final PrintWriter pw = new PrintWriter(new OutputStreamWriter(resp.getOutputStream()));
- pw.println("The DefaultSlingServlet is just a skeleton for now...");
- pw.println("servlet: " + getClass().getName());
- pw.println("resource data: " + r.getData());
- pw.flush();
+ final Object data = r.getData();
+ if(data instanceof Node) {
+ final PrintWriter pw = new PrintWriter(new OutputStreamWriter(resp.getOutputStream()));
+ dumpNode(pw,(Node)data);
+ pw.flush();
+
+ } else {
+ throw new HttpStatusCodeException(
+ 501,
+ "Not implemented: resource of type "
+ + data.getClass().getName()
+ + " cannot be dumped by " + getClass().getSimpleName()
+ );
+ }
}
public boolean canProcess(HttpServletRequest req) throws Exception {
return true;
+ }
+
+ protected void dumpNode(PrintWriter pw, Node n) throws RepositoryException {
+ pw.println("** Node dumped by " + getClass().getSimpleName() + "**");
+ pw.println("Node path:" + n.getPath());
+
+ pw.println("\n** Node properties **");
+ for(PropertyIterator pi = n.getProperties(); pi.hasNext(); ) {
+ final Property p = pi.nextProperty();
+ pw.println(p.getName() + ": " + p.getValue().getString());
+ }
}
}
Added: incubator/sling/whiteboard/microsling/src/main/java/org/apache/sling/microsling/slingservlets/SlingPostServlet.java
URL: http://svn.apache.org/viewvc/incubator/sling/whiteboard/microsling/src/main/java/org/apache/sling/microsling/slingservlets/SlingPostServlet.java?rev=583826&view=auto
==============================================================================
--- incubator/sling/whiteboard/microsling/src/main/java/org/apache/sling/microsling/slingservlets/SlingPostServlet.java (added)
+++ incubator/sling/whiteboard/microsling/src/main/java/org/apache/sling/microsling/slingservlets/SlingPostServlet.java Thu Oct 11 06:56:26 2007
@@ -0,0 +1,102 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.sling.microsling.slingservlets;
+
+import java.util.Enumeration;
+
+import javax.jcr.Node;
+import javax.jcr.RepositoryException;
+import javax.jcr.Session;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.apache.sling.microsling.etc.AbstractSlingServlet;
+import org.apache.sling.microsling.requestcontext.SlingRequestContext;
+
+/** Handle POST to create nodes easily over HTTP.
+ */
+public class SlingPostServlet extends AbstractSlingServlet {
+
+ public static final String SLING_EXTENSION = ".sling";
+ public static final String DISPLAY_EXTENSION = ".html";
+
+ @Override
+ public void doPost(HttpServletRequest req, HttpServletResponse resp) throws Exception {
+ final SlingRequestContext ctx = SlingRequestContext.getFromRequest(req);
+
+ // Create or update node under current path
+ final String nodePath = ctx.getContentPath();
+
+ final Session s = ctx.getSession();
+ Node current = null;
+ boolean createNewNode = false;
+ if(s.itemExists(nodePath)) {
+ current = (Node)s.getItem(nodePath);
+ // TODO this is simplistic...if the node has children we assume it's a folder,
+ // so we create a new node under it instead of updating it
+ createNewNode = current.hasNodes();
+ } else {
+ createNewNode = true;
+ }
+
+ if(createNewNode) {
+ final String newNodePath = nodePath + "/" + System.currentTimeMillis();
+ current = deepCreateNode(s,newNodePath);
+ }
+
+ // Copy request parameters to node properties and save
+ setPropertiesFromRequest(current,req);
+ s.save();
+
+ final String redirectPath = req.getServletPath() + current.getPath() + DISPLAY_EXTENSION;
+ resp.sendRedirect(redirectPath);
+ }
+
+ public boolean canProcess(HttpServletRequest req) throws Exception {
+ return req.getPathInfo().endsWith(SLING_EXTENSION);
+ }
+
+ /** Set node properties from current request (only handles Strings for now) */
+ protected void setPropertiesFromRequest(Node n,HttpServletRequest req) throws Exception {
+ for(Enumeration e = req.getParameterNames(); e.hasMoreElements(); ) {
+ final String name = (String)e.nextElement();
+ final String value = (String)req.getParameter(name);
+ n.setProperty(name, value);
+ }
+ }
+
+ /**
+ * Deep creates a node, parent-padding with nt:unstructured nodes
+ * @param path absolute path to node that needs to be deep-created
+ */
+ public Node deepCreateNode(Session s,String path) throws RepositoryException {
+ String[] pathelems = path.substring(1).split("/");
+ int i = 0;
+ String mypath = "";
+ Node parent = s.getRootNode();
+ while (i < pathelems.length) {
+ String name = pathelems[i];
+ mypath += "/" + name;
+ if (!s.itemExists(mypath)) {
+ parent.addNode(name);
+ }
+ parent = (Node) s.getItem(mypath);
+ i++;
+ }
+ return (parent);
+ }
+}
Propchange: incubator/sling/whiteboard/microsling/src/main/java/org/apache/sling/microsling/slingservlets/SlingPostServlet.java
------------------------------------------------------------------------------
svn:eol-style = native
Propchange: incubator/sling/whiteboard/microsling/src/main/java/org/apache/sling/microsling/slingservlets/SlingPostServlet.java
------------------------------------------------------------------------------
svn:keywords = Author Date Id Revision Rev URL
Modified: incubator/sling/whiteboard/microsling/src/main/webapp/index.html
URL: http://svn.apache.org/viewvc/incubator/sling/whiteboard/microsling/src/main/webapp/index.html?rev=583826&r1=583825&r2=583826&view=diff
==============================================================================
--- incubator/sling/whiteboard/microsling/src/main/webapp/index.html (original)
+++ incubator/sling/whiteboard/microsling/src/main/webapp/index.html Thu Oct 11 06:56:26 2007
@@ -28,29 +28,51 @@
<h2>Status</h2>
<p>
- The basic request filtering and SlingServlet selection mechanisms are in place, but nothing
- is connected to the JCR Repository yet.
+ The basic request filtering, SlingServlet and JCR Resource loading mechanisms are in place.
+ </p>
+ <p>
+ The SlingPostServlet allows nt:unstructured nodes to be created in the repository, for testing,
+ by POSTing to URLs ending in <em>.sling</em>. See the <em>Content creation form</em> below.
+ </p>
+
+ <h2>Microsling test links and forms</h2>
+ <p>
+ The <a href="microsling/testing-one-two.notfound">microsling/testing-one-two.notfound</a> URL
+ should return a 404
</p>
- <h2>Microsling test links</h2>
<p>
- <ul>
- <li>
- <a href="microsling/testing-one-two">microsling/testing-one-two</a> should return a plain text response
- </li>
- <li>
- <a href="microsling/testing-one-two.notfound">microsling/testing-one-two.notfound</a> should return a 404
- </li>
- </ul>
- <form action="microsling/whatever" method="POST">
+
+ <form class="testbox" action="microsling/content/testing.sling" method="POST">
+ <h1>Content creation form</h1>
+ <p>
+ Submitting this form should create a node under content/testing, and display it using
+ an <em>html</em> extension:
+ </p>
+ <p>
+ Title:
+ <br/>
+ <input name="title" type="text" size="40" value="Default title"/>
+ <br/>
+ Text:
+ <br/>
+ <input name="text" type="text" size="80" value="Default text"/>
+ <br/>
+ <input type="submit" value="Update node"/>
+ </p>
+ </form>
+
+ <form class="testbox" action="microsling/whatever" method="POST">
+ <h1>Non-Sling POST method form</h1>
<p>
Submitting this form should cause an error 501: <em>POST method is not supported by DefaultSlingServlet</em>.
</p>
<input type="submit"/>
</form>
+
</p>
- <h2>More Test links</h2>
+ <h2>More test links</h2>
<p>
<ul>
<li>
Modified: incubator/sling/whiteboard/microsling/src/main/webapp/microsling.css
URL: http://svn.apache.org/viewvc/incubator/sling/whiteboard/microsling/src/main/webapp/microsling.css?rev=583826&r1=583825&r2=583826&view=diff
==============================================================================
--- incubator/sling/whiteboard/microsling/src/main/webapp/microsling.css (original)
+++ incubator/sling/whiteboard/microsling/src/main/webapp/microsling.css Thu Oct 11 06:56:26 2007
@@ -13,3 +13,15 @@
font-weight:900;
padding:20px 4px 4px 0px;
}
+
+.testbox {
+ border: solid gray 1px;
+ margin: 1em;
+ padding: 0.5em;
+}
+
+form h1 {
+ padding: 1px;
+ margin: 1px;
+ font-size: 100%;
+}