From bd4a82a07d094a140183904676d3b494a04daf19 Mon Sep 17 00:00:00 2001 From: Pinaki Poddar Date: Thu, 9 Dec 2010 21:50:47 +0000 Subject: [PATCH] OPENJPA-1851: Add JEST to OpenJPA trunk git-svn-id: https://svn.apache.org/repos/asf/openjpa/trunk@1044138 13f79535-47bb-0310-9956-ffa450edef68 --- openjpa-jest/pom.xml | 60 + .../persistence/jest/AbstractCommand.java | 342 +++++ .../openjpa/persistence/jest/Closure.java | 107 ++ .../openjpa/persistence/jest/Constants.java | 141 ++ .../persistence/jest/DomainCommand.java | 60 + .../persistence/jest/ExceptionFormatter.java | 62 + .../openjpa/persistence/jest/FindCommand.java | 100 ++ .../apache/openjpa/persistence/jest/IOR.java | 45 + .../openjpa/persistence/jest/JESTCommand.java | 92 ++ .../openjpa/persistence/jest/JESTContext.java | 330 +++++ .../openjpa/persistence/jest/JESTServlet.java | 161 +++ .../persistence/jest/JPAServletContext.java | 82 ++ .../apache/openjpa/persistence/jest/JSON.java | 54 + .../openjpa/persistence/jest/JSONObject.java | 225 ++++ .../persistence/jest/JSONObjectFormatter.java | 296 +++++ .../persistence/jest/MetamodelHelper.java | 187 +++ .../persistence/jest/ObjectFormatter.java | 107 ++ .../persistence/jest/ProcessingException.java | 97 ++ .../persistence/jest/PropertiesCommand.java | 77 ++ .../persistence/jest/PropertiesFormatter.java | 49 + .../persistence/jest/PrototypeFactory.java | 126 ++ .../persistence/jest/QueryCommand.java | 98 ++ .../persistence/jest/TokenReplacedStream.java | 180 +++ .../persistence/jest/XMLFormatter.java | 516 ++++++++ .../persistence/jest/help/entity-name.html | 29 + .../persistence/jest/help/fetch-plan.html | 51 + .../openjpa/persistence/jest/help/query.html | 32 + .../jest/help/response-format.html | 28 + .../persistence/jest/images/arrow_right.jpg | Bin 0 -> 5892 bytes .../persistence/jest/images/domain.jpg | Bin 0 -> 999 bytes .../openjpa/persistence/jest/images/find.jpg | Bin 0 -> 1412 bytes .../openjpa/persistence/jest/images/help.jpg | Bin 0 -> 784 bytes .../openjpa/persistence/jest/images/home.jpg | Bin 0 -> 30414 bytes .../openjpa/persistence/jest/images/jest.jpg | Bin 0 -> 15565 bytes .../persistence/jest/images/monitor.jpg | Bin 0 -> 1640 bytes .../persistence/jest/images/properties.jpg | Bin 0 -> 1813 bytes .../persistence/jest/images/query2.png | Bin 0 -> 9589 bytes .../jest/images/underconstruction.jpg | Bin 0 -> 71314 bytes .../persistence/jest/jest-instance.xsd | 166 +++ .../apache/openjpa/persistence/jest/jest.css | 248 ++++ .../apache/openjpa/persistence/jest/jest.html | 329 +++++ .../apache/openjpa/persistence/jest/jest.js | 1146 +++++++++++++++++ .../persistence/jest/localizer.properties | 60 + 43 files changed, 5683 insertions(+) create mode 100644 openjpa-jest/pom.xml create mode 100644 openjpa-jest/src/main/java/org/apache/openjpa/persistence/jest/AbstractCommand.java create mode 100644 openjpa-jest/src/main/java/org/apache/openjpa/persistence/jest/Closure.java create mode 100644 openjpa-jest/src/main/java/org/apache/openjpa/persistence/jest/Constants.java create mode 100644 openjpa-jest/src/main/java/org/apache/openjpa/persistence/jest/DomainCommand.java create mode 100644 openjpa-jest/src/main/java/org/apache/openjpa/persistence/jest/ExceptionFormatter.java create mode 100644 openjpa-jest/src/main/java/org/apache/openjpa/persistence/jest/FindCommand.java create mode 100644 openjpa-jest/src/main/java/org/apache/openjpa/persistence/jest/IOR.java create mode 100644 openjpa-jest/src/main/java/org/apache/openjpa/persistence/jest/JESTCommand.java create mode 100644 openjpa-jest/src/main/java/org/apache/openjpa/persistence/jest/JESTContext.java create mode 100644 openjpa-jest/src/main/java/org/apache/openjpa/persistence/jest/JESTServlet.java create mode 100644 openjpa-jest/src/main/java/org/apache/openjpa/persistence/jest/JPAServletContext.java create mode 100644 openjpa-jest/src/main/java/org/apache/openjpa/persistence/jest/JSON.java create mode 100644 openjpa-jest/src/main/java/org/apache/openjpa/persistence/jest/JSONObject.java create mode 100644 openjpa-jest/src/main/java/org/apache/openjpa/persistence/jest/JSONObjectFormatter.java create mode 100644 openjpa-jest/src/main/java/org/apache/openjpa/persistence/jest/MetamodelHelper.java create mode 100644 openjpa-jest/src/main/java/org/apache/openjpa/persistence/jest/ObjectFormatter.java create mode 100644 openjpa-jest/src/main/java/org/apache/openjpa/persistence/jest/ProcessingException.java create mode 100644 openjpa-jest/src/main/java/org/apache/openjpa/persistence/jest/PropertiesCommand.java create mode 100644 openjpa-jest/src/main/java/org/apache/openjpa/persistence/jest/PropertiesFormatter.java create mode 100644 openjpa-jest/src/main/java/org/apache/openjpa/persistence/jest/PrototypeFactory.java create mode 100644 openjpa-jest/src/main/java/org/apache/openjpa/persistence/jest/QueryCommand.java create mode 100644 openjpa-jest/src/main/java/org/apache/openjpa/persistence/jest/TokenReplacedStream.java create mode 100644 openjpa-jest/src/main/java/org/apache/openjpa/persistence/jest/XMLFormatter.java create mode 100644 openjpa-jest/src/main/resources/org/apache/openjpa/persistence/jest/help/entity-name.html create mode 100644 openjpa-jest/src/main/resources/org/apache/openjpa/persistence/jest/help/fetch-plan.html create mode 100644 openjpa-jest/src/main/resources/org/apache/openjpa/persistence/jest/help/query.html create mode 100644 openjpa-jest/src/main/resources/org/apache/openjpa/persistence/jest/help/response-format.html create mode 100644 openjpa-jest/src/main/resources/org/apache/openjpa/persistence/jest/images/arrow_right.jpg create mode 100644 openjpa-jest/src/main/resources/org/apache/openjpa/persistence/jest/images/domain.jpg create mode 100644 openjpa-jest/src/main/resources/org/apache/openjpa/persistence/jest/images/find.jpg create mode 100644 openjpa-jest/src/main/resources/org/apache/openjpa/persistence/jest/images/help.jpg create mode 100644 openjpa-jest/src/main/resources/org/apache/openjpa/persistence/jest/images/home.jpg create mode 100644 openjpa-jest/src/main/resources/org/apache/openjpa/persistence/jest/images/jest.jpg create mode 100644 openjpa-jest/src/main/resources/org/apache/openjpa/persistence/jest/images/monitor.jpg create mode 100644 openjpa-jest/src/main/resources/org/apache/openjpa/persistence/jest/images/properties.jpg create mode 100644 openjpa-jest/src/main/resources/org/apache/openjpa/persistence/jest/images/query2.png create mode 100644 openjpa-jest/src/main/resources/org/apache/openjpa/persistence/jest/images/underconstruction.jpg create mode 100644 openjpa-jest/src/main/resources/org/apache/openjpa/persistence/jest/jest-instance.xsd create mode 100644 openjpa-jest/src/main/resources/org/apache/openjpa/persistence/jest/jest.css create mode 100644 openjpa-jest/src/main/resources/org/apache/openjpa/persistence/jest/jest.html create mode 100644 openjpa-jest/src/main/resources/org/apache/openjpa/persistence/jest/jest.js create mode 100644 openjpa-jest/src/main/resources/org/apache/openjpa/persistence/jest/localizer.properties diff --git a/openjpa-jest/pom.xml b/openjpa-jest/pom.xml new file mode 100644 index 000000000..44696e1ba --- /dev/null +++ b/openjpa-jest/pom.xml @@ -0,0 +1,60 @@ + + + + + + 4.0.0 + + + org.apache.openjpa + openjpa-parent + 2.2.0-SNAPSHOT + + + org.apache.openjpa + openjpa-jest + jar + OpenJPA JEST + + + + org.apache.openjpa + openjpa-kernel + ${project.version} + + + org.apache.geronimo.specs + geronimo-jpa_2.0_spec + + + org.apache.openjpa + openjpa-persistence + ${project.version} + + + javax.servlet + servlet-api + 2.4 + + + diff --git a/openjpa-jest/src/main/java/org/apache/openjpa/persistence/jest/AbstractCommand.java b/openjpa-jest/src/main/java/org/apache/openjpa/persistence/jest/AbstractCommand.java new file mode 100644 index 000000000..98f974865 --- /dev/null +++ b/openjpa-jest/src/main/java/org/apache/openjpa/persistence/jest/AbstractCommand.java @@ -0,0 +1,342 @@ +/* + * 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.openjpa.persistence.jest; + +import static java.net.HttpURLConnection.HTTP_BAD_REQUEST; +import static org.apache.openjpa.persistence.jest.Constants.QUALIFIER_FORMAT; +import static org.apache.openjpa.persistence.jest.Constants.QUALIFIER_PLAN; +import static org.apache.openjpa.persistence.jest.Constants._loc; + +import java.io.IOException; +import java.io.PrintWriter; +import java.net.URLDecoder; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.apache.openjpa.enhance.PersistenceCapable; +import org.apache.openjpa.kernel.BrokerImpl; +import org.apache.openjpa.kernel.OpenJPAStateManager; +import org.apache.openjpa.persistence.FetchPlan; +import org.apache.openjpa.persistence.JPAFacadeHelper; +import org.apache.openjpa.persistence.OpenJPAEntityManager; +import org.apache.openjpa.persistence.OpenJPAQuery; + +/** + * The abstract base class for all commands available to JEST. + * + * @author Pinaki Poddar + * + */ +abstract class AbstractCommand implements JESTCommand { + public static final char EQUAL = '='; + public static final String PATH_SEPARATOR = "/"; + public static final Collection EMPTY_LIST = Collections.emptySet(); + protected ObjectFormatter _formatter; + protected static PrototypeFactory> _ff = + new PrototypeFactory>(); + + private Map _qualifiers = new HashMap(); + private Map _args = new HashMap(); + private Map _margs = new HashMap(); + protected final JPAServletContext ctx; + + static { + _ff.register(Format.xml, XMLFormatter.class); + _ff.register(Format.json, JSONObjectFormatter.class); + } + protected AbstractCommand(JPAServletContext ctx) { + this.ctx = ctx; + } + + public String getMandatoryArgument(String key) { + return get(key, _margs); + } + + public String getArgument(String key) { + return get(key, _args); + } + + public boolean hasArgument(String key) { + return has(key, _args); + } + + public Map getArguments() { + return _args; + } + + public String getQualifier(String key) { + return get(key, _qualifiers); + } + + public boolean hasQualifier(String key) { + return has(key, _qualifiers); + } + + protected boolean isBooleanQualifier(String key) { + if (hasQualifier(key)) { + Object value = getQualifier(key); + return value == null || "true".equalsIgnoreCase(value.toString()); + } + return false; + } + + public Map getQualifiers() { + return _qualifiers; + } + + /** + * Parses HTTP Request for the qualifier and argument of a command. + *
+ * Each servlet path segment, except the first (which is the command name itself), is a qualifier. + * Each qualifier can be either a key or a key-value pair separated by a = sign. + *
+ * Each request parameter key-value pair is an argument. A concrete command may specify mandatory + * arguments (e.g. type must be mandatory argument for find command, + * or q for query. The mandatory arguments, if any, are not captured + * in the argument list. + *
+ * The qualifiers and arguments are immutable after parse. + */ + public void parse() throws ProcessingException { + HttpServletRequest request = ctx.getRequest(); + String path = request.getPathInfo(); + if (path != null) { + path = path.substring(1); + String[] segments = path.split(PATH_SEPARATOR); + for (int i = 1; i < segments.length; i++) { + String segment = segments[i]; + int idx = segment.indexOf(EQUAL); + if (idx == -1) { + _qualifiers.put(segment, null); + } else { + _qualifiers.put(segment.substring(0, idx), segment.substring(idx+1)); + } + } + } + _qualifiers = Collections.unmodifiableMap(_qualifiers); + + Enumeration names = request.getParameterNames(); + Collection mandatoryArgs = getMandatoryArguments(); + + while (names.hasMoreElements()) { + String key = names.nextElement().toString(); + if (key.startsWith("dojo.")) continue; + put(key, request.getParameter(key), mandatoryArgs.contains(key) ? _margs : _args); + } + _args = Collections.unmodifiableMap(_args); + _margs = Collections.unmodifiableMap(_margs); + + validate(); + } + + /** + * Gets the mandatory arguments. + * + * @return empty list by default. + */ + protected Collection getMandatoryArguments() { + return EMPTY_LIST; + } + + /** + * Gets the minimum number of arguments excluding the mandatory arguments. + * + * @return zero by default. + */ + protected int getMinimumArguments() { + return 0; + } + + /** + * Gets the maximum number of arguments excluding the mandatory arguments. + * + * @return Integer.MAX_VALUE by default. + */ + protected int getMaximumArguments() { + return Integer.MAX_VALUE; + } + + protected Format getDefaultFormat() { + return Format.xml; + } + + /** + * Gets the valid qualifiers. + * + * @return empty list by default. + */ + protected Collection getValidQualifiers() { + return EMPTY_LIST; + } + + /** + * Called post-parse to validate this command has requisite qualifiers and arguments. + */ + protected void validate() { + HttpServletRequest request = ctx.getRequest(); + Collection validQualifiers = getValidQualifiers(); + for (String key : _qualifiers.keySet()) { + if (!validQualifiers.contains(key)) { + throw new ProcessingException(ctx,_loc.get("parse-invalid-qualifier", this, key, validQualifiers), + HTTP_BAD_REQUEST); + } + } + Collection mandatoryArgs = getMandatoryArguments(); + for (String key : mandatoryArgs) { + if (request.getParameter(key) == null) { + throw new ProcessingException(ctx, _loc.get("parse-missing-mandatory-argument", this, key, + request.getParameterMap().keySet()), HTTP_BAD_REQUEST); + } + } + if (_args.size() < getMinimumArguments()) { + throw new ProcessingException(ctx, _loc.get("parse-less-argument", this, _args.keySet(), + getMinimumArguments()), HTTP_BAD_REQUEST); + } + if (_args.size() > getMaximumArguments()) { + throw new ProcessingException(ctx, _loc.get("parse-less-argument", this, _args.keySet(), + getMinimumArguments()), HTTP_BAD_REQUEST); + } + } + + private String get(String key, Map map) { + return map.get(key); + } + + private String put(String key, String value, Map map) { + return map.put(key, value); + } + + private boolean has(String key, Map map) { + return map.containsKey(key); + } + + public ObjectFormatter getObjectFormatter() { + if (_formatter == null) { + String rformat = getQualifier(QUALIFIER_FORMAT); + Format format = null; + if (rformat == null) { + format = getDefaultFormat(); + } else { + try { + format = Format.valueOf(rformat); + } catch (Exception e) { + throw new ProcessingException(ctx, _loc.get("format-not-supported", new Object[]{format, + ctx.getRequest().getPathInfo(), _ff.getRegisteredKeys()}), HTTP_BAD_REQUEST); + } + } + _formatter = _ff.newInstance(format); + if (_formatter == null) { + throw new ProcessingException(ctx, _loc.get("format-not-supported", new Object[]{format, + ctx.getRequest().getPathInfo(), _ff.getRegisteredKeys()}), HTTP_BAD_REQUEST); + } + } + return _formatter; + } + + protected OpenJPAStateManager toStateManager(Object obj) { + if (obj instanceof OpenJPAStateManager) + return (OpenJPAStateManager)obj; + if (obj instanceof PersistenceCapable) { + return (OpenJPAStateManager)((PersistenceCapable)obj).pcGetStateManager(); + } + return null; + } + + protected List toStateManager(Collection objects) { + List sms = new ArrayList(); + for (Object o : objects) { + OpenJPAStateManager sm = toStateManager(o); + if (sm != null) sms.add(sm); + } + return sms; + } + + protected void pushFetchPlan(Object target) { + if (!hasQualifier(QUALIFIER_PLAN)) + return; + OpenJPAEntityManager em = ctx.getPersistenceContext(); + FetchPlan plan = em.pushFetchPlan(); + BrokerImpl broker = (BrokerImpl)JPAFacadeHelper.toBroker(em); + if (target instanceof OpenJPAEntityManager) { + broker.setCacheFinderQuery(false); + } else if (target instanceof OpenJPAQuery) { + broker.setCachePreparedQuery(false); + } + + String[] plans = getQualifier(QUALIFIER_PLAN).split(","); + for (String p : plans) { + p = p.trim(); + if (p.charAt(0) == '-') { + plan.removeFetchGroup(p.substring(1)); + } else { + plan.addFetchGroup(p); + } + } + } + + protected void popFetchPlan(boolean finder) { + if (!hasQualifier(QUALIFIER_PLAN)) + return; + OpenJPAEntityManager em = ctx.getPersistenceContext(); + BrokerImpl broker = (BrokerImpl)JPAFacadeHelper.toBroker(em); + if (finder) { + broker.setCacheFinderQuery(false); + } else { + broker.setCachePreparedQuery(false); + } + } + + protected void debug(HttpServletRequest request, HttpServletResponse response, JPAServletContext ctx) + throws IOException { + response.setContentType(Constants.MIME_TYPE_PLAIN); + PrintWriter writer = response.getWriter(); + + writer.println("URI = [" + request.getRequestURI() + "]"); + writer.println("URL = [" + request.getRequestURL() + "]"); + writer.println("Servlet Path = [" + request.getServletPath() + "]"); // this is one we need + writer.println("Context Path = [" + request.getContextPath() + "]"); + writer.println("Translated Path = [" + request.getPathTranslated() + "]");// not decoded + writer.println("Path Info = [" + request.getPathInfo() + "]");// decoded + String query = request.getQueryString(); + if (query != null) { + query = URLDecoder.decode(request.getQueryString(),"UTF-8"); + } + writer.println("Query = [" + query + "]"); // and this one + int i = 0; + for (Map.Entry e : _qualifiers.entrySet()) { + writer.println("Qualifier [" + i + "] = [" + e.getKey() + ": " + e.getValue() + "]"); + i++; + } + i = 0; + for (Map.Entry e : _args.entrySet()) { + writer.println("Parameter [" + i + "] = [" + e.getKey() + ": " + e.getValue() + "]"); + i++; + } + } + +} diff --git a/openjpa-jest/src/main/java/org/apache/openjpa/persistence/jest/Closure.java b/openjpa-jest/src/main/java/org/apache/openjpa/persistence/jest/Closure.java new file mode 100644 index 000000000..d068109ba --- /dev/null +++ b/openjpa-jest/src/main/java/org/apache/openjpa/persistence/jest/Closure.java @@ -0,0 +1,107 @@ +/* + * 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.openjpa.persistence.jest; + +import java.util.BitSet; +import java.util.Collection; +import java.util.Collections; +import java.util.Iterator; +import java.util.LinkedHashSet; +import java.util.Map; +import java.util.Set; + +import org.apache.openjpa.enhance.PersistenceCapable; +import org.apache.openjpa.kernel.OpenJPAStateManager; +import org.apache.openjpa.meta.FieldMetaData; +import org.apache.openjpa.meta.JavaTypes; + +/** + * Computes closure of a collection of managed objects. + * + * @author Pinaki Poddar + * + */ +public class Closure implements Iterable { + private Set _visited = new LinkedHashSet(); + + public Closure(OpenJPAStateManager root) { + this(Collections.singleton(root)); + } + + public Closure(Collection roots) { + for (OpenJPAStateManager sm : roots) { + visit(sm); + } + } + + private void visit(OpenJPAStateManager sm) { + if (sm == null) + return; + boolean isVisited = !_visited.add(sm); + if (isVisited) return; + BitSet loaded = sm.getLoaded(); + FieldMetaData[] fmds = sm.getMetaData().getFields(); + for (FieldMetaData fmd : fmds) { + int idx = fmd.getIndex(); + if (!loaded.get(idx)) + continue; + if (fmd.getElement().getTypeMetaData() == null && fmd.getValue().getTypeMetaData() == null) + continue; + switch (fmd.getDeclaredTypeCode()) { + case JavaTypes.PC: + visit(toStateManager(sm.fetch(idx))); + break; + case JavaTypes.ARRAY: + Object[] values = (Object[])sm.fetch(idx); + for (Object o : values) + visit(toStateManager(o)); + break; + case JavaTypes.COLLECTION: + Collection members = (Collection)sm.fetch(idx); + for (Object o : members) + visit(toStateManager(o)); + break; + case JavaTypes.MAP: + Map map = (Map)sm.fetch(idx); + for (Map.Entry entry : map.entrySet()) { + visit(toStateManager(entry.getKey())); + visit(toStateManager(entry.getValue())); + } + break; + default: + } + } + } + + OpenJPAStateManager toStateManager(Object o) { + if (o instanceof PersistenceCapable) { + return (OpenJPAStateManager)((PersistenceCapable)o).pcGetStateManager(); + } + return null; + } + + public Iterator iterator() { + return _visited.iterator(); + } + + String ior(OpenJPAStateManager sm) { + return sm.getMetaData().getDescribedType().getSimpleName()+'-'+sm.getObjectId(); + } +} diff --git a/openjpa-jest/src/main/java/org/apache/openjpa/persistence/jest/Constants.java b/openjpa-jest/src/main/java/org/apache/openjpa/persistence/jest/Constants.java new file mode 100644 index 000000000..54e45242c --- /dev/null +++ b/openjpa-jest/src/main/java/org/apache/openjpa/persistence/jest/Constants.java @@ -0,0 +1,141 @@ +/* + * 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.openjpa.persistence.jest; + +import org.apache.openjpa.lib.util.Localizer; + +/** + * Static String constants + * + * @author Pinaki Poddar + * + */ +public interface Constants { + /** + * Command Qualifiers + */ + public static final String QUALIFIER_FORMAT = "format"; + public static final String QUALIFIER_PLAN = "plan"; + public static final String QUALIFIER_MAXRESULT = "max"; + public static final String QUALIFIER_FIRSTRESULT = "first"; + public static final String QUALIFIER_NAMED = "named"; + public static final String QUALIFIER_SINGLE = "single"; + + /** + * Command Arguments + */ + public static final String ARG_QUERY = "q"; + public static final String ARG_TYPE = "type"; + + + /** + * Mime Types + */ + public static final String MIME_TYPE_PLAIN = "text/plain"; + public static final String MIME_TYPE_JS = "text/javascript"; + public static final String MIME_TYPE_CSS = "text/css"; + public static final String MIME_TYPE_XML = "text/xml"; + public static final String MIME_TYPE_JSON = "application/json"; + + public static final String CONTEXT_ROOT = "/"; + public static final String JEST_TEMPLATE = "jest.html"; + + /** + * Servlet initialization parameters + */ + public static final String INIT_PARA_UNIT = "persistence.unit"; + public static final String INIT_PARA_STANDALONE = "standalone"; + + /** + * Dojo Toolkit URL and Themes + */ + public static final String DOJO_BASE_URL = "http://ajax.googleapis.com/ajax/libs/dojo/1.5"; + public static final String DOJO_THEME = "claro"; + + + + /** + * Root element of XML instances. Must match the name defined in _validQualifiers = Arrays.asList("format"); + + public DomainCommand(JPAServletContext ctx) { + super(ctx); + } + + protected Collection getValidQualifiers() { + return _validQualifiers; + } + + protected int getMaximumArguments() { + return 0; + } + + public String getAction() { + return "domain"; + } + + public void process() throws ProcessingException, IOException { + getObjectFormatter().writeOut(ctx.getPersistenceContext().getMetamodel(), + _loc.get("domain-title").toString(), _loc.get("domain-desc").toString(), ctx.getRequestURI(), + ctx.getResponse().getOutputStream()); + } +} diff --git a/openjpa-jest/src/main/java/org/apache/openjpa/persistence/jest/ExceptionFormatter.java b/openjpa-jest/src/main/java/org/apache/openjpa/persistence/jest/ExceptionFormatter.java new file mode 100644 index 000000000..2b7bbe63e --- /dev/null +++ b/openjpa-jest/src/main/java/org/apache/openjpa/persistence/jest/ExceptionFormatter.java @@ -0,0 +1,62 @@ +/* + * 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.openjpa.persistence.jest; + +import java.io.PrintWriter; +import java.io.StringWriter; + +import org.w3c.dom.Document; +import org.w3c.dom.Element; + + +/** + * Formats error stack trace. + * + * @author Pinaki Poddar + * + */ +class ExceptionFormatter extends XMLFormatter { + /** + * Creates a XML Document with given header and stack trace of the given error. + * @param header + * @param e + */ + public Document createXML(String header, Throwable e) { + Element root = newDocument(Constants.ROOT_ELEMENT_ERROR); + Document doc = root.getOwnerDocument(); + Element errorHeader = doc.createElement(Constants.ELEMENT_ERROR_HEADER); + Element errorMessage = doc.createElement(Constants.ELEMENT_ERROR_MESSAGE); + Element stackTrace = doc.createElement(Constants.ELEMENT_ERROR_TRACE); + + errorHeader.setTextContent(header); + errorMessage.appendChild(doc.createCDATASection(e.getMessage())); + + StringWriter buf = new StringWriter(); + e.printStackTrace(new PrintWriter(buf, true)); + stackTrace.appendChild(doc.createCDATASection(buf.toString())); + + root.appendChild(errorHeader); + root.appendChild(errorMessage); + root.appendChild(stackTrace); + + return doc; + } + +} diff --git a/openjpa-jest/src/main/java/org/apache/openjpa/persistence/jest/FindCommand.java b/openjpa-jest/src/main/java/org/apache/openjpa/persistence/jest/FindCommand.java new file mode 100644 index 000000000..a7d262491 --- /dev/null +++ b/openjpa-jest/src/main/java/org/apache/openjpa/persistence/jest/FindCommand.java @@ -0,0 +1,100 @@ +/* + * 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.openjpa.persistence.jest; +import static org.apache.openjpa.persistence.jest.Constants.*; + +import java.io.IOException; +import java.net.HttpURLConnection; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; +import java.util.Map; + +import javax.persistence.EntityManager; + +import org.apache.openjpa.kernel.OpenJPAStateManager; +import org.apache.openjpa.meta.ClassMetaData; +import org.apache.openjpa.persistence.OpenJPAEntityManager; +import org.apache.openjpa.util.ApplicationIds; + + +/** + * @author Pinaki Poddar + * + */ +class FindCommand extends AbstractCommand { + private static final List _mandatoryArgs = Arrays.asList(ARG_TYPE); + private static final List _validQualifiers = Arrays.asList("format", "plan"); + + public FindCommand(JPAServletContext ctx) { + super(ctx); + } + + @Override + protected Collection getMandatoryArguments() { + return _mandatoryArgs; + } + + @Override + protected int getMinimumArguments() { + return 1; + } + + protected Collection getValidQualifiers() { + return _validQualifiers; + } + + @Override + public void process() throws ProcessingException { + OpenJPAEntityManager em = ctx.getPersistenceContext(); + String type = getMandatoryArgument(ARG_TYPE); + ClassMetaData meta = ctx.resolve(type); + Map parameters = getArguments(); + Object[] pks = new Object[parameters.size()]; + Iterator> params = parameters.entrySet().iterator(); + for (int i = 0; i < parameters.size(); i++) { + pks[i] = params.next().getKey(); + } + Object oid = ApplicationIds.fromPKValues(pks, meta); + pushFetchPlan(em); + try { + Object pc = em.find(meta.getDescribedType(), oid); + if (pc != null) { + OpenJPAStateManager sm = toStateManager(pc); + ObjectFormatter formatter = getObjectFormatter(); + ctx.getResponse().setContentType(formatter.getMimeType()); + try { + formatter.writeOut(Collections.singleton(sm), em.getMetamodel(), + _loc.get("find-title").toString(), _loc.get("find-desc").toString(), ctx.getRequestURI(), + ctx.getResponse().getOutputStream()); + } catch (IOException e) { + throw new ProcessingException(ctx, e); + } + } else { + throw new ProcessingException(ctx, _loc.get("entity-not-found", type, Arrays.toString(pks)), + HttpURLConnection.HTTP_NOT_FOUND); + } + } finally { + popFetchPlan(true); + } + } +} diff --git a/openjpa-jest/src/main/java/org/apache/openjpa/persistence/jest/IOR.java b/openjpa-jest/src/main/java/org/apache/openjpa/persistence/jest/IOR.java new file mode 100644 index 000000000..96bceb816 --- /dev/null +++ b/openjpa-jest/src/main/java/org/apache/openjpa/persistence/jest/IOR.java @@ -0,0 +1,45 @@ +/* + * 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.openjpa.persistence.jest; + +import static org.apache.openjpa.persistence.jest.Constants.NULL_VALUE; + +import org.apache.openjpa.kernel.OpenJPAStateManager; + +/** + * String reference of a managed object. + * + * @author Pinaki Poddar + * + */ +public class IOR { + public static final char DASH = '-'; + /** + * Stringified representation of a managed instance identity. + * The simple Java type name and the persistent identity separated by a {@link Constants#DASH dash}. + * + * @param sm a managed instance. + * @return + */ + public static String toString(OpenJPAStateManager sm) { + if (sm == null) return NULL_VALUE; + return sm.getMetaData().getDescribedType().getSimpleName() + DASH + sm.getObjectId(); + } +} diff --git a/openjpa-jest/src/main/java/org/apache/openjpa/persistence/jest/JESTCommand.java b/openjpa-jest/src/main/java/org/apache/openjpa/persistence/jest/JESTCommand.java new file mode 100644 index 000000000..15c9a1760 --- /dev/null +++ b/openjpa-jest/src/main/java/org/apache/openjpa/persistence/jest/JESTCommand.java @@ -0,0 +1,92 @@ +/* + * 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.openjpa.persistence.jest; + +import java.io.IOException; +import java.util.Map; + +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import javax.servlet.http.HttpServletRequest; + + +/** + * Interface for JEST commands. A JEST command denotes a JPA operation such as find, + * query or domain. Besides signifying a JPA operation, a command may have + * zero or more qualifiers and arguments. + *
+ * A qualifier qualifies the action to be performed. For example, a query command may be qualified + * to return a single instance as its result, or limit its result to first 20 instances etc. + *
+ * An argument is an argument to the target JPA method. For example, find command has + * arguments for the type of the instance and the primary key. A query command has the + * query string as its argument. + *

+ * A concrete command instance is an outcome of parsing a {@link HttpServletRequest request}. + * The {@link HttpServletRequest#getPathInfo() path} segments are parsed for qualifiers. + * The {@link HttpServletRequest#getQueryString() query string} is parsed for the arguments. + *

+ * A JEST command often attaches special semantics to a standard URI syntax. For example, all JEST + * URI enforces that the first segment of a servlet path denotes the command moniker e.g. the URI
+ * http://www.jpa.com/jest/find/plan=myPlan?type=Person&1234
+ * with context root http://www.jpa.com/jest has the servlet path /find/plan=myPlan + * and query string type=Person&1234. + *
+ * The first path segment find will determine that the command is to find a + * persistent entity of type Person and primary key 1234 using a fetch plan + * named myPlan. + * + * @author Pinaki Poddar + * + */ +public interface JESTCommand { + /** + * Supported format monikers. + */ + public static enum Format {xml, json}; + + /** + * Parse the given request to populate qualifiers and parameters of this command. + * A command can interpret and consume certain path segments or parameters of the + * original request. During {@link #process(ServletRequest, ServletResponse, JPAServletContext) processing} + * phase, the parameters and qualifiers are accessed from the parsed command itself rather than + * from the + */ + public void parse() throws ProcessingException; + + /** + * Accessors for this command's arguments and qualifiers. + * @return + * @exception IllegalStateException if accessed prior to parsing. + */ + public Map getArguments(); + public String getArgument(String key); + public boolean hasArgument(String key); + public Map getQualifiers(); + public String getQualifier(String key); + public boolean hasQualifier(String key); + + /** + * Process the given request and write the output on to the given response in the given context. + * @throws ProcessingException + * + */ + public void process() throws ProcessingException, IOException; +} diff --git a/openjpa-jest/src/main/java/org/apache/openjpa/persistence/jest/JESTContext.java b/openjpa-jest/src/main/java/org/apache/openjpa/persistence/jest/JESTContext.java new file mode 100644 index 000000000..998da1c2c --- /dev/null +++ b/openjpa-jest/src/main/java/org/apache/openjpa/persistence/jest/JESTContext.java @@ -0,0 +1,330 @@ +/* + * 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.openjpa.persistence.jest; + +import static java.net.HttpURLConnection.HTTP_INTERNAL_ERROR; +import static java.net.HttpURLConnection.HTTP_NOT_FOUND; +import static org.apache.openjpa.persistence.jest.Constants.CONTEXT_ROOT; + +import java.io.ByteArrayOutputStream; +import java.io.CharArrayWriter; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.PrintWriter; +import java.net.URI; +import java.net.URISyntaxException; +import java.util.Calendar; +import java.util.Date; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.apache.openjpa.conf.OpenJPAConfiguration; +import org.apache.openjpa.lib.log.Log; +import org.apache.openjpa.lib.util.Localizer; +import org.apache.openjpa.meta.ClassMetaData; +import org.apache.openjpa.meta.MetaDataRepository; +import org.apache.openjpa.persistence.OpenJPAEntityManager; +import org.apache.openjpa.persistence.OpenJPAEntityManagerFactory; + +/** + * An operational context combines a {@link OpenJPAEntityManager persistence context} and a HTTP execution + * context expressed as a {@link HttpServletRequest request} and {@link HttpServletResponse response}. + *
+ * This context {@link #getAction(String) parses} the HTTP request URL to identity the command and then + * {@link #execute() executes} it. + * + * @author Pinaki Poddar + * + */ +public class JESTContext implements JPAServletContext { + private final String _unit; + private final OpenJPAEntityManagerFactory _emf; + private OpenJPAEntityManager _em; + private final HttpServletRequest _request; + private final HttpServletResponse _response; + protected MetaDataRepository _repos; + private String _rootResource; + protected Log _log; + protected static PrototypeFactory _cf = new PrototypeFactory(); + public static final Localizer _loc = Localizer.forPackage(JESTContext.class); + private static final String ONE_YEAR_FROM_NOW; + public static final char QUERY_SEPARATOR = '?'; + + /** + * Registers known commands in a {@link PrototypeFactory registry}. + * + */ + static { + _cf.register("find", FindCommand.class); + _cf.register("query", QueryCommand.class); + _cf.register("domain", DomainCommand.class); + _cf.register("properties", PropertiesCommand.class); + + Calendar now = Calendar.getInstance(); + now.add(Calendar.YEAR, 1); + ONE_YEAR_FROM_NOW = new Date(now.getTimeInMillis()).toString(); + } + + public JESTContext(String unit, OpenJPAEntityManagerFactory emf, HttpServletRequest request, + HttpServletResponse response) { + _unit = unit; + _emf = emf; + _request = request; + _response = response; + OpenJPAConfiguration conf = _emf.getConfiguration(); + _log = conf.getLog("JEST"); + _repos = conf.getMetaDataRepositoryInstance(); + } + + /** + * Gets the name of the persistence unit. + */ + public String getPersistenceUnitName() { + return _unit; + } + + /** + * Gets the persistence context. The persistence context is lazily constructed because all commands + * may not need it. + */ + public OpenJPAEntityManager getPersistenceContext() { + if (_em == null) { + _em = _emf.createEntityManager(); + } + return _em; + } + + /** + * Gets the request. + */ + public HttpServletRequest getRequest() { + return _request; + } + + /** + * + */ + public URI getRequestURI() { + StringBuffer buf = _request.getRequestURL(); + String query = _request.getQueryString(); + if (query != null) { + buf.append(QUERY_SEPARATOR).append(query); + } + try { + return new URI(buf.toString()); + } catch (URISyntaxException e) { + throw new ProcessingException(this, _loc.get("bad-uri", _request.getRequestURL()), HTTP_INTERNAL_ERROR); + } + + } + + /** + * Gets the response. + */ + public HttpServletResponse getResponse() { + return _response; + } + + /** + * Executes the request. + *
+ * Execution starts with parsing the {@link HttpServletRequest#getPathInfo() request path}. + * The {@linkplain #getAction(String) first path segment} is interpreted as action key, and + * if a action with the given key is registered then the control is delegated to the command. + * The command parses the entire {@link HttpServletRequest request} for requisite qualifiers and + * arguments and if the parse is successful then the command is + * {@linkplain JESTCommand#process() executed} in this context. + *
+ * If path is null, or no command is registered for the action or the command can not parse + * the request, then a last ditch attempt is made to {@linkplain #findResource(String) find} a resource. + * This fallback lookup is important because the response can contain hyperlinks to stylesheets or + * scripts. The browser will resolve such hyperlinks relative to the original response. + *
+ * For example, let the original request URL be:
+ * http://host:port/demo/jest/find?type=Actor&Robert + *
+ * The response to this request is a HTML page that contained a hyperlink to jest.css stylesheet + * in its <head> section.
+ * <link ref="jest.css" .....> + *
+ * The browser will resolve the hyperlink by sending back another request as
+ * http://host:port/demo/jest/find/jest.css + *
+ * + * @throws Exception + */ + public void execute() throws Exception { + String path = _request.getPathInfo(); + if (isContextRoot(path)) { + getRootResource(); + return; + } + String action = getAction(path); + JESTCommand command = _cf.newInstance(action, this); + if (command == null) { + findResource(path.substring(1)); + return; + } + try { + command.parse(); + command.process(); + } catch (ProcessingException e1) { + throw e1; + } catch (Exception e2) { + try { + findResource(path.substring(action.length()+1)); + } catch (ProcessingException e3) { + throw e2; + } + } + } + + /** + * Gets the action from the given path. + * + * @param path a string + * @return if null, returns context root i.e. '/' character. + * Otherwise, if the path starts with context root, then returns the substring before the + * next '/' character or end of the string, whichever is earlier. + * If the path does not start with context root, returns + * the substring before the first '/' character or end of the string, whichever is earlier. + */ + public static String getAction(String path) { + if (path == null) + return CONTEXT_ROOT; + if (path.startsWith(CONTEXT_ROOT)) + path = path.substring(1); + int idx = path.indexOf(CONTEXT_ROOT); + return idx == -1 ? path : path.substring(0, idx); + } + + + public ClassMetaData resolve(String alias) { + ClassLoader loader = Thread.currentThread().getContextClassLoader(); + return _repos.getMetaData(alias, loader, true); + } + + /** + * A resource is always looked up with respect to this class. + * + * @param rsrc + * @throws ProcessingException + */ + void findResource(String rsrc) throws ProcessingException { + _response.setHeader("Cache-Control", "public"); + _response.setHeader("Expires", ONE_YEAR_FROM_NOW); + InputStream in = getClass().getResourceAsStream(rsrc); + if (in == null) { // try again as a relative path + if (rsrc.startsWith(CONTEXT_ROOT)) { + in = getClass().getResourceAsStream(rsrc.substring(1)); + if (in == null) { + throw new ProcessingException(this, _loc.get("resource-not-found", rsrc), HTTP_NOT_FOUND); + } + } + } + try { + String mimeType = _request.getSession().getServletContext().getMimeType(rsrc); + _response.setContentType(mimeType); + OutputStream out = _response.getOutputStream(); + if (mimeType.startsWith("image/")) { + byte[] b = new byte[1024]; + int i = 0; + for (int l = 0; (l = in.read(b)) != -1;) { + out.write(b, 0, l); + i += l; + } + _response.setContentLength(i); + } else { + for (int c = 0; (c = in.read()) != -1;) { + out.write((char)c); + } + } + } catch (IOException e) { + throw new ProcessingException(this, e, _loc.get("resource-not-found", rsrc), HTTP_NOT_FOUND); + } + } + + + + private void log(String s) { + log((short)-1, s); + } + + public void log(short level, String message) { + switch (level) { + case Log.INFO: _log.info(message); break; + case Log.ERROR: _log.fatal(message); break; + case Log.FATAL: _log.fatal(message); break; + case Log.TRACE: _log.trace(message); break; + case Log.WARN: _log.warn(message); break; + default: _request.getSession().getServletContext().log(message); + + break; + } + } + + /** + * Is this path a context root? + * @param path + * @return + */ + boolean isContextRoot(String path) { + return (path == null || CONTEXT_ROOT.equals(path)); + } + + /** + * Root resource is a HTML template with deployment specific tokens such as name of the persistence unit + * or base url. On first request for this resource, the tokens in the templated HTML file gets replaced + * by the actual deployment specific value into a string. This string (which is an entire HTML file) + * is then written to the response. + * + * @see TokenReplacedStream + * @throws IOException + */ + private void getRootResource() throws IOException { + _response.setHeader("Cache-Control", "public"); + _response.setHeader("Expires", ONE_YEAR_FROM_NOW); + if (_rootResource == null) { + String[] tokens = { + "${persistence.unit}", getPersistenceUnitName(), + "${jest.uri}", _request.getRequestURL().toString(), + "${webapp.name}", _request.getContextPath().startsWith(CONTEXT_ROOT) + ? _request.getContextPath().substring(1) + : _request.getContextPath(), + "${servlet.name}", _request.getServletPath().startsWith(CONTEXT_ROOT) + ? _request.getServletPath().substring(1) + : _request.getServletPath(), + "${server.name}", _request.getServerName(), + "${server.port}", ""+_request.getServerPort(), + + "${dojo.base}", Constants.DOJO_BASE_URL, + "${dojo.theme}", Constants.DOJO_THEME, + + }; + InputStream in = getClass().getResourceAsStream(Constants.JEST_TEMPLATE); + CharArrayWriter out = new CharArrayWriter(); + new TokenReplacedStream().replace(in, out, tokens); + _rootResource = out.toString(); + } + _response.getOutputStream().write(_rootResource.getBytes()); + } +} diff --git a/openjpa-jest/src/main/java/org/apache/openjpa/persistence/jest/JESTServlet.java b/openjpa-jest/src/main/java/org/apache/openjpa/persistence/jest/JESTServlet.java new file mode 100644 index 000000000..586804123 --- /dev/null +++ b/openjpa-jest/src/main/java/org/apache/openjpa/persistence/jest/JESTServlet.java @@ -0,0 +1,161 @@ +/* + * 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.openjpa.persistence.jest; + +import static org.apache.openjpa.persistence.jest.Constants.INIT_PARA_UNIT; +import static org.apache.openjpa.persistence.jest.Constants.INIT_PARA_STANDALONE; +import static org.apache.openjpa.persistence.jest.Constants._loc; + +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; + +import javax.persistence.Persistence; +import javax.servlet.ServletConfig; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.apache.openjpa.kernel.AbstractBrokerFactory; +import org.apache.openjpa.kernel.BrokerFactory; +import org.apache.openjpa.persistence.JPAFacadeHelper; +import org.apache.openjpa.persistence.OpenJPAEntityManagerFactory; +import org.apache.openjpa.persistence.OpenJPAPersistence; + +/** + * A specialized HTTP servlet to interpret HTTP requests as Java Persistent API commands + * on a running persistence unit. The persistence unit is identified by the name of the + * unit and is supplied to this servlet during its initialization. The component using + * the persistent unit and this servlet must be within the same module scope. + *

+ * The syntax of the request URL is described in + * OpenJPA web site. + *

+ * The response to a resource request is represented in various format, namely + * XML, JSON or a JavaScript that will dynamically render in the browser. The format + * can be controlled via the initialization parameter response.format in + * <init-param> clause or per request basis via format=xml|dojo|json + * encoded in the path expression of the Request URI. + *

+ * Servlet initialization parameter + * + * + * + * + *
ParameterValue
persistence.unitName of the persistence unit. Mandatory
response.formatDefault format used for representation. Defaults to xml.
+ *
+ * @author Pinaki Poddar + * + */ +@SuppressWarnings("serial") +public class JESTServlet extends HttpServlet { + private String _unit; + private boolean _debug; + private OpenJPAEntityManagerFactory _emf; + + @Override + public void init(ServletConfig config) throws ServletException { + super.init(config); + _debug = "true".equalsIgnoreCase(config.getInitParameter("debug")); + _unit = config.getInitParameter(INIT_PARA_UNIT); + if (_unit == null) { + throw new ServletException(_loc.get("no-persistence-unit-param").toString()); + } + boolean standalone = "true".equalsIgnoreCase(config.getInitParameter(INIT_PARA_STANDALONE)); + if (standalone) { + createPersistenceUnit(); + } + if (findPersistenceUnit()) { + config.getServletContext().log(_loc.get("servlet-init", _unit).toString()); + } else { + config.getServletContext().log(_loc.get("servlet-not-init", _unit).toString()); + } + } + + /** + * Peeks into the servlet path of the request to create appropriate {@link JESTCommand JEST command}. + * Passes the request on to the command which is responsible for generating a response. + */ + @Override + public void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { + debug(request); + if (findPersistenceUnit()) { + JESTContext ctx = new JESTContext(_unit, _emf, request, response); + try { + ctx.execute(); + } catch (Exception e) { + handleError(ctx, e); + } + } else { + throw new ServletException(_loc.get("no-persistence-unit", _unit).toString()); + } + } + + protected void createPersistenceUnit() throws ServletException { + try { + Map map = new HashMap(); + map.put("openjpa.EntityManagerFactoryPool", true); + _emf = OpenJPAPersistence.cast(Persistence.createEntityManagerFactory(_unit, map)); + } catch (Exception e) { + throw new ServletException(_loc.get("no-persistence-unit").toString(), e); + } + } + protected boolean findPersistenceUnit() { + if (_emf == null) { + BrokerFactory bf = AbstractBrokerFactory.getPooledFactoryForKey(_unit); + if (bf != null) { + _emf = (OpenJPAEntityManagerFactory)bf.getUserObject(JPAFacadeHelper.EMF_KEY); + } + } + return _emf != null; + } + + protected void handleError(JPAServletContext ctx, Throwable t) throws IOException { + if (t instanceof ProcessingException) { + ((ProcessingException)t).printStackTrace(); + } else { + new ProcessingException(ctx, t).printStackTrace(); + } + } + + @Override + public void destroy() { + _emf = null; + _unit = null;; + } + + private void debug(HttpServletRequest r) { + if (!_debug) return; +// log("-----------------------------------------------------------"); + log(r.getRemoteUser() + "@" + r.getRemoteHost() + ":" + r.getRemotePort() + "[" + r.getPathInfo() + "]"); +// log("Request URL = [" + request.getRequestURL() + "]"); +// log("Request URI = [" + request.getRequestURI() + "]"); +// log("Servlet Path = [" + request.getServletPath() + "]"); +// log("Context Path = [" + request.getContextPath() + "]"); +// log("Path Info = [" + request.getPathInfo() + "]"); +// log("Path Translated = [" + request.getPathTranslated() + "]"); + } + + public void log(String s) { + System.err.println(s); + super.log(s); + } +} diff --git a/openjpa-jest/src/main/java/org/apache/openjpa/persistence/jest/JPAServletContext.java b/openjpa-jest/src/main/java/org/apache/openjpa/persistence/jest/JPAServletContext.java new file mode 100644 index 000000000..327d306f2 --- /dev/null +++ b/openjpa-jest/src/main/java/org/apache/openjpa/persistence/jest/JPAServletContext.java @@ -0,0 +1,82 @@ +/* + * 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.openjpa.persistence.jest; + +import java.net.URI; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.apache.openjpa.lib.log.Log; +import org.apache.openjpa.meta.ClassMetaData; +import org.apache.openjpa.persistence.OpenJPAEntityManager; + +/** + * An operating context provides a {@link EntityManage persistence context} and utility functions within + * which all JEST commands execute. + * + * @author Pinaki Poddar + * + */ +public interface JPAServletContext { + /** + * Get the persistence context of the operational context. + */ + public OpenJPAEntityManager getPersistenceContext(); + + /** + * Get the persistence unit name. + */ + public String getPersistenceUnitName(); + + /** + * Get the HTTP Request. + */ + public HttpServletRequest getRequest(); + + /** + * Get the HTTP Response. + */ + public HttpServletResponse getResponse(); + + /** + * Get the requested URI. + * @return + */ + public URI getRequestURI(); + + /** + * Resolve the given alias to meta-data of the persistent type. + * @param alias a moniker for the Java type. It can be fully qualified type name or entity name + * or simple name of the actual persistent Java class. + * + * @return meta-data for the given name. + * @exception raises runtime exception if the given name can not be identified to a persistent + * Java type. + */ + public ClassMetaData resolve(String alias); + + /** + * Logging message. + * @param level OpenJPA defined {@link Log#INFO log levels}. Invalid levels will print the message on console. + * @param message a printable message. + */ + public void log(short level, String message); +} diff --git a/openjpa-jest/src/main/java/org/apache/openjpa/persistence/jest/JSON.java b/openjpa-jest/src/main/java/org/apache/openjpa/persistence/jest/JSON.java new file mode 100644 index 000000000..51145b370 --- /dev/null +++ b/openjpa-jest/src/main/java/org/apache/openjpa/persistence/jest/JSON.java @@ -0,0 +1,54 @@ +/* + * 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.openjpa.persistence.jest; + +/** + * A generic interface for a JSON encoded instance. + * + * @author Pinaki Poddar + * + */ +public interface JSON { + /** + * Render into a string buffer. + * + * @param level level at which this instance is being rendered + * @return a mutable buffer + */ + public StringBuilder asString(int level); + + public static final char FIELD_SEPARATOR = ','; + public static final char MEMBER_SEPARATOR = ','; + public static final char VALUE_SEPARATOR = ':'; + public static final char IOR_SEPARTOR = '-'; + public static final char QUOTE = '"'; + public static final char SPACE = ' '; + public static final char OBJECT_START = '{'; + public static final char OBJECT_END = '}'; + public static final char ARRAY_START = '['; + public static final char ARRAY_END = ']'; + + public static final String NEWLINE = "\r\n"; + public static final String NULL_LITERAL = "null"; + public static final String REF_MARKER = "$ref"; + public static final String ID_MARKER = "$id"; + public static final String ARRAY_EMPTY = "[]"; + +} diff --git a/openjpa-jest/src/main/java/org/apache/openjpa/persistence/jest/JSONObject.java b/openjpa-jest/src/main/java/org/apache/openjpa/persistence/jest/JSONObject.java new file mode 100644 index 000000000..28e518487 --- /dev/null +++ b/openjpa-jest/src/main/java/org/apache/openjpa/persistence/jest/JSONObject.java @@ -0,0 +1,225 @@ +/* + * 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.openjpa.persistence.jest; + +import java.io.PrintWriter; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +/** + * A JSON instance for persistence. + *
+ * Persistent instances have a persistent identity that extends beyond the process lifetime unlike other common + * identity such as {@linkplain System#identityHashCode(Object) identity hash code} for a Java instance in a JVM. + *
+ * A JSONObject instance must need such a persistent identity. + * + * @author Pinaki Poddar + * + */ +public class JSONObject implements JSON { + private final String _type; + private final String _id; + private final boolean _ref; + private final Map _values; + + public JSONObject(String type, Object id, boolean ref) { + _type = type; + _id = id.toString(); + _ref = ref; + _values = new LinkedHashMap(); + } + + public void set(String key, Object value) { + _values.put(key, value); + } + + public void write(PrintWriter writer) { + writer.println(toString()); + } + public String toString() { + return asString(0).toString(); + } + + public StringBuilder asString(int indent) { + StringBuilder buf = new StringBuilder().append(OBJECT_START); + buf.append(encodeField(_ref ? REF_MARKER : ID_MARKER, ior(), 0)); + if (_ref) { + return buf.append(OBJECT_END); + } + StringBuilder tab = newIndent(indent+1); + for (Map.Entry e : _values.entrySet()) { + buf.append(FIELD_SEPARATOR).append(NEWLINE); + buf.append(tab).append(encodeField(e.getKey(), e.getValue(), indent+1)); + } + buf.append(NEWLINE) + .append(newIndent(indent)) + .append(OBJECT_END); + return buf; + } + + /** + * Encoding a JSON field is a quoted field name, followed by a :, followed by a value (which itself can be JSON) + * @param field + * @param value + * @param indent + * @return + */ + private static StringBuilder encodeField(String field, Object value, int indent) { + return new StringBuilder() + .append(quoteFieldName(field)) + .append(VALUE_SEPARATOR) + .append(quoteFieldValue(value, indent)); + } + + private static StringBuilder newIndent(int indent) { + char[] tabs = new char[indent*4]; + Arrays.fill(tabs, SPACE); + return new StringBuilder().append(tabs); + } + + + StringBuilder ior() { + return new StringBuilder(_type).append('-').append(_id); + } + + private static StringBuilder quoteFieldName(String s) { + return new StringBuilder().append(QUOTE).append(s).append(QUOTE); + } + + /** + * Creates a StringBuilder for the given value. + * If the value is null, outputs null without quote + * If the value is Number, outputs the value without quote + * If the value is JSON, outputs the string rendition of value + * Otherwise quoted value + * @param o + * @return + */ + private static StringBuilder quoteFieldValue(Object o, int indent) { + if (o == null) return new StringBuilder(NULL_LITERAL); + if (o instanceof Number) return new StringBuilder(o.toString()); + if (o instanceof JSON) return ((JSON)o).asString(indent); + return quoted(o.toString()); + } + + private static StringBuilder quoted(Object o) { + if (o == null) return new StringBuilder(NULL_LITERAL); + return new StringBuilder().append(QUOTE).append(o.toString()).append(QUOTE); + } + + public static class Array implements JSON { + private List _members = new ArrayList(); + + public void add(Object o) { + _members.add(o); + } + public String toString() { + return asString(0).toString(); + } + + public StringBuilder asString(int indent) { + StringBuilder buf = new StringBuilder().append(ARRAY_START); + StringBuilder tab = JSONObject.newIndent(indent+1); + for (Object o : _members) { + if (buf.length() > 1) buf.append(MEMBER_SEPARATOR); + buf.append(NEWLINE); + if (o instanceof JSON) + buf.append(tab).append(((JSON)o).asString(indent+1)); + else + buf.append(tab).append(o); + } + buf.append(NEWLINE) + .append(JSONObject.newIndent(indent)) + .append(ARRAY_END); + + return buf; + } + } + + public static class KVMap implements JSON { + private Map _entries = new LinkedHashMap(); + + public void put(Object k, Object v) { + _entries.put(k,v); + } + + public String toString() { + return asString(0).toString(); + } + + public StringBuilder asString(int indent) { + StringBuilder buf = new StringBuilder().append(ARRAY_START); + StringBuilder tab = JSONObject.newIndent(indent+1); + for (Map.Entry e : _entries.entrySet()) { + if (buf.length()>1) buf.append(MEMBER_SEPARATOR); + buf.append(NEWLINE); + Object key = e.getKey(); + if (key instanceof JSON) + buf.append(tab).append(((JSON)key).asString(indent+1)); + else + buf.append(tab).append(key); + buf.append(VALUE_SEPARATOR); + Object value = e.getValue(); + if (value instanceof JSON) + buf.append(((JSON)value).asString(indent+2)); + else + buf.append(value); + + } + buf.append(NEWLINE) + .append(JSONObject.newIndent(indent)) + .append(ARRAY_END); + return buf; + } + } + + public static void main(String[] args) throws Exception { + JSONObject o = new JSONObject("Person", 1234, false); + JSONObject r = new JSONObject("Person", 1234, true); + JSONObject f = new JSONObject("Person", 2345, false); + Array a = new Array(); + a.add(f); + a.add(3456); + a.add(null); + a.add(r); + a.add(null); + KVMap map = new KVMap(); + map.put("k1", r); + map.put("k2", f); + map.put("k3", null); + map.put("k4", 3456); + map.put(null, 6789); + + f.set("name", "Mary"); + f.set("age", 30); + f.set("friend", r); + o.set("name", "John"); + o.set("age", 20); + o.set("friend", f); + o.set("friends", a); + o.set("map", map); + + System.err.println(o); + } +} diff --git a/openjpa-jest/src/main/java/org/apache/openjpa/persistence/jest/JSONObjectFormatter.java b/openjpa-jest/src/main/java/org/apache/openjpa/persistence/jest/JSONObjectFormatter.java new file mode 100644 index 000000000..d7fd4dcd4 --- /dev/null +++ b/openjpa-jest/src/main/java/org/apache/openjpa/persistence/jest/JSONObjectFormatter.java @@ -0,0 +1,296 @@ +/* + * 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.openjpa.persistence.jest; + +import java.io.BufferedReader; +import java.io.CharArrayWriter; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.io.PrintWriter; +import java.io.Reader; +import java.net.URI; +import java.util.Arrays; +import java.util.BitSet; +import java.util.Collection; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import javax.persistence.metamodel.Attribute; +import javax.persistence.metamodel.Metamodel; + +import org.apache.openjpa.kernel.OpenJPAStateManager; +import org.apache.openjpa.kernel.StoreContext; +import org.apache.openjpa.meta.FieldMetaData; +import org.apache.openjpa.meta.JavaTypes; +import org.apache.openjpa.persistence.meta.Members; +import static org.apache.openjpa.persistence.jest.Constants.MIME_TYPE_JSON; + +/** + * Marshals a root instance and its persistent closure as JSON object. + * The closure is resolved against the persistence context that contains the root instance. + * The JSON format introduces a $id and $ref to address reference that pure JSON does not. + * + * @author Pinaki Poddar + * + */ +public class JSONObjectFormatter implements ObjectFormatter { + + public String getMimeType() { + return MIME_TYPE_JSON; + } + + public void encode(Object obj, JPAServletContext ctx) { + if (obj instanceof OpenJPAStateManager) { + try { + JSON result = encodeManagedInstance((OpenJPAStateManager)obj, + ctx.getPersistenceContext().getMetamodel()); + PrintWriter writer = ctx.getResponse().getWriter(); + writer.println(result.toString()); + } catch (Exception e) { + throw new ProcessingException(ctx, e); + } + } else { + throw new RuntimeException(this + " does not know how to encode " + obj); + } + return; + } + + public JSON writeOut(Collection sms, Metamodel model, String title, String desc, + URI uri, OutputStream out) throws IOException { + JSON json = encode(sms,model); + out.write(json.toString().getBytes()); + return json; + } + + public JSON encode(Collection sms, Metamodel model) { + return encodeManagedInstances(sms, model); + } + + /** + * Encodes the given managed instance into a new XML element as a child of the given parent node. + * + * @param sm a managed instance, can be null. + * @param parent the parent node to which the new node be attached. + */ + private JSON encodeManagedInstance(final OpenJPAStateManager sm, Metamodel model) { + return encodeManagedInstance(sm, new HashSet(), 0, false, model); + } + + private JSON encodeManagedInstances(final Collection sms, Metamodel model) { + JSONObject.Array result = new JSONObject.Array(); + for (OpenJPAStateManager sm : sms) { + result.add(encodeManagedInstance(sm, new HashSet(), 0, false, model)); + } + return result; + } + + + /** + * Encodes the closure of a persistent instance into a XML element. + * + * @param sm the managed instance to be encoded. Can be null. + * @param parent the parent XML element to which the new XML element be added. Must not be null. Must be + * owned by a document. + * @param visited the persistent instances that had been encoded already. Must not be null or immutable. + * + * @return the new element. The element has been appended as a child to the given parent in this method. + */ + private JSONObject encodeManagedInstance(final OpenJPAStateManager sm, final Set visited, + int indent, boolean indentPara, Metamodel model) { + if (visited == null) { + throw new IllegalArgumentException("null closure for encoder"); + } + if (sm == null) { + return null; + } + + boolean ref = !visited.add(sm); + JSONObject root = new JSONObject(typeOf(sm), sm.getObjectId(), ref);; + if (ref) { + return root; + } + + BitSet loaded = sm.getLoaded(); + StoreContext ctx = (StoreContext)sm.getGenericContext(); + List> attrs = MetamodelHelper.getAttributesInOrder(sm.getMetaData(), model); + + for (int i = 0; i < attrs.size(); i++) { + FieldMetaData fmd = ((Members.Member) attrs.get(i)).fmd; + if (!loaded.get(fmd.getIndex())) + continue; + Object value = sm.fetch(fmd.getIndex()); + switch (fmd.getDeclaredTypeCode()) { + case JavaTypes.BOOLEAN: + case JavaTypes.BYTE: + case JavaTypes.CHAR: + case JavaTypes.DOUBLE: + case JavaTypes.FLOAT: + case JavaTypes.INT: + case JavaTypes.LONG: + case JavaTypes.SHORT: + + case JavaTypes.BOOLEAN_OBJ: + case JavaTypes.BYTE_OBJ: + case JavaTypes.CHAR_OBJ: + case JavaTypes.DOUBLE_OBJ: + case JavaTypes.FLOAT_OBJ: + case JavaTypes.INT_OBJ: + case JavaTypes.LONG_OBJ: + case JavaTypes.SHORT_OBJ: + + case JavaTypes.BIGDECIMAL: + case JavaTypes.BIGINTEGER: + case JavaTypes.DATE: + case JavaTypes.NUMBER: + case JavaTypes.CALENDAR: + case JavaTypes.LOCALE: + case JavaTypes.STRING: + case JavaTypes.ENUM: + root.set(fmd.getName(),value); + break; + + case JavaTypes.PC: + if (value == null) { + root.set(fmd.getName(), null); + } else { + root.set(fmd.getName(),encodeManagedInstance(ctx.getStateManager(value), visited, + indent+1, false, model)); + } + break; + + case JavaTypes.ARRAY: + Object[] values = (Object[])value; + value = Arrays.asList(values); + // no break; + case JavaTypes.COLLECTION: + if (value == null) { + root.set(fmd.getName(), null); + break; + } + Collection members = (Collection)value; + JSONObject.Array array = new JSONObject.Array(); + root.set(fmd.getName(), array); + if (members.isEmpty()) { + break; + } + boolean basic = fmd.getElement().getTypeMetaData() == null; + for (Object o : members) { + if (o == null) { + array.add(null); + } else { + if (basic) { + array.add(o); + } else { + array.add(encodeManagedInstance(ctx.getStateManager(o), visited, indent+1, true, + model)); + } + } + } + break; + case JavaTypes.MAP: + if (value == null) { + root.set(fmd.getName(), null); + break; + } + Set entries = ((Map)value).entrySet(); + JSONObject.KVMap map = new JSONObject.KVMap(); + root.set(fmd.getName(), map); + if (entries.isEmpty()) { + break; + } + + boolean basicKey = fmd.getElement().getTypeMetaData() == null; + boolean basicValue = fmd.getValue().getTypeMetaData() == null; + for (Map.Entry e : entries) { + Object k = e.getKey(); + Object v = e.getValue(); + if (!basicKey) { + k = encodeManagedInstance(ctx.getStateManager(k), visited, indent+1, true, model); + } + if (!basicValue) { + v = encodeManagedInstance(ctx.getStateManager(e.getValue()), visited, + indent+1, false, model); + } + map.put(k,v); + } + break; + + case JavaTypes.INPUT_STREAM: + case JavaTypes.INPUT_READER: + root.set(fmd.getName(), streamToString(value)); + break; + + case JavaTypes.PC_UNTYPED: + case JavaTypes.OBJECT: + case JavaTypes.OID: + root.set(fmd.getName(), "***UNSUPPORTED***"); + } + } + return root; + } + + + String typeOf(OpenJPAStateManager sm) { + return sm.getMetaData().getDescribedType().getSimpleName(); + } + + + /** + * Convert the given stream (either an InutStream or a Reader) to a String + * to be included in CDATA section of a XML document. + * + * @param value the field value to be converted. Can not be null + * @return + */ + String streamToString(Object value) { + Reader reader = null; + if (value instanceof InputStream) { + reader = new BufferedReader(new InputStreamReader((InputStream)value)); + } else if (value instanceof Reader) { + reader = (Reader)value; + } else { + throw new RuntimeException(); + } + CharArrayWriter writer = new CharArrayWriter(); + try { + for (int c; (c = reader.read()) != -1;) { + writer.write(c); + } + } catch (IOException ex) { + throw new RuntimeException(ex); + } + return writer.toString(); + } + + @Override + public JSON encode(Metamodel model) { + // TODO Auto-generated method stub + return null; + } + + @Override + public JSON writeOut(Metamodel model, String title, String desc, URI uri, OutputStream out) throws IOException { + throw new UnsupportedOperationException(); + } +} diff --git a/openjpa-jest/src/main/java/org/apache/openjpa/persistence/jest/MetamodelHelper.java b/openjpa-jest/src/main/java/org/apache/openjpa/persistence/jest/MetamodelHelper.java new file mode 100644 index 000000000..cd42ea999 --- /dev/null +++ b/openjpa-jest/src/main/java/org/apache/openjpa/persistence/jest/MetamodelHelper.java @@ -0,0 +1,187 @@ +/* + * 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.openjpa.persistence.jest; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; + +import javax.persistence.metamodel.Attribute; +import javax.persistence.metamodel.ManagedType; +import javax.persistence.metamodel.MapAttribute; +import javax.persistence.metamodel.Metamodel; +import javax.persistence.metamodel.PluralAttribute; +import javax.persistence.metamodel.SingularAttribute; + +import org.apache.openjpa.meta.ClassMetaData; +import org.apache.openjpa.meta.JavaTypes; +import org.apache.openjpa.persistence.meta.Members; +import static org.apache.openjpa.persistence.jest.Constants.*; + +/** + * @author Pinaki Poddar + * + */ +public class MetamodelHelper { + public static final char DASH = '-'; + public static final char UNDERSCORE = '_'; + + /** + * Attribute Category makes a finer distinction over PersistentAttributeType declared in + * {@link Attribute.PersistentAttributeType} such as id, version, lob or enum. + *
+ * Important: The name of the enumerated elements is important because + * a) some of these names are same as in Attribute.PersistentAttributeType enumeration + * b) names are used by XML serialization with underscores replaced by dash and decapitalized + * + */ + public static enum AttributeCategory { + ID, VERSION, BASIC, ENUM, EMBEDDED, LOB, + ONE_TO_ONE, MANY_TO_ONE, ONE_TO_MANY, ELEMENT_COLLECTION, MANY_TO_MANY + } + + public static List> getAttributesInOrder(Class cls, Metamodel model) { + return getAttributesInOrder(model.managedType(cls)); + } + + public static List> getAttributesInOrder(ClassMetaData meta, Metamodel model) { + return getAttributesInOrder(meta.getDescribedType(), model); + } + + /** + * Gets the attributes of the given type in defined order. + * @param type + * @return + */ + public static List> getAttributesInOrder(ManagedType type) { + List> list = new ArrayList>(type.getAttributes()); + Collections.sort(list, new AttributeComparator()); + return list; + } + + public static boolean isId(Attribute a) { + if (a instanceof SingularAttribute) + return ((SingularAttribute)a).isId(); + return false; + } + + public static boolean isVersion(Attribute a) { + if (a instanceof SingularAttribute) + return ((SingularAttribute)a).isVersion(); + return false; + } + + public static boolean isEnum(Attribute a) { + if (a instanceof Members.Member) { + int type = ((Members.Member)a).fmd.getDeclaredTypeCode(); + return type == JavaTypes.ENUM; + } + return false; + } + + public static boolean isLob(Attribute a) { + if (a instanceof Members.Member) { + int type = ((Members.Member)a).fmd.getDeclaredTypeCode(); + return type == JavaTypes.INPUT_READER || type == JavaTypes.INPUT_STREAM; + } + return false; + } + + /** + * Gets a ordinal value of enumerated persistent attribute category. + * + * @param attr + * @return + */ + public static AttributeCategory getAttributeCategory(Attribute attr) { + if (isId(attr)) + return AttributeCategory.ID; + if (isVersion(attr)) + return AttributeCategory.VERSION; + if (isLob(attr)) + return AttributeCategory.LOB; + if (isEnum(attr)) + return AttributeCategory.ENUM; + switch (attr.getPersistentAttributeType()) { + case BASIC : + return AttributeCategory.BASIC; + case EMBEDDED: + return AttributeCategory.EMBEDDED; + case ONE_TO_ONE: + return AttributeCategory.ONE_TO_ONE; + case MANY_TO_ONE: + return AttributeCategory.MANY_TO_ONE; + case ONE_TO_MANY: + case ELEMENT_COLLECTION: + return AttributeCategory.ONE_TO_MANY; + case MANY_TO_MANY: + return AttributeCategory.MANY_TO_MANY; + } + throw new RuntimeException(attr.toString()); + } + + public static String getTagByAttributeType(Attribute attr) { + return getAttributeCategory(attr).name().replace(UNDERSCORE, DASH).toLowerCase(); + } + + /** + * Gets name of the attribute type. For collection and map type attribute, the name is + * appended with generic type argument names. + * @param attr + * @return + */ + public static String getAttributeTypeName(Attribute attr) { + StringBuilder name = new StringBuilder(attr.getJavaType().getSimpleName()); + switch (attr.getPersistentAttributeType()) { + case ONE_TO_MANY: + case ELEMENT_COLLECTION: + name.append("<") + .append(((PluralAttribute)attr).getBindableJavaType().getSimpleName()) + .append(">"); + break; + case MANY_TO_MANY: + name.append("<") + .append(((MapAttribute)attr).getKeyJavaType().getSimpleName()) + .append(',') + .append(((MapAttribute)attr).getBindableJavaType().getSimpleName()) + .append(">"); + break; + default: + } + return name.toString(); + } + + /** + * Compares attribute by their category and within the same category by name. + * + */ + public static class AttributeComparator implements Comparator> { + public int compare(Attribute a1, Attribute a2) { + AttributeCategory t1 = getAttributeCategory(a1); + AttributeCategory t2 = getAttributeCategory(a2); + if (t1.equals(t2)) { + return a1.getName().compareTo(a2.getName()); + } else { + return t1.compareTo(t2); + } + } + } +} diff --git a/openjpa-jest/src/main/java/org/apache/openjpa/persistence/jest/ObjectFormatter.java b/openjpa-jest/src/main/java/org/apache/openjpa/persistence/jest/ObjectFormatter.java new file mode 100644 index 000000000..2d8b3a3ce --- /dev/null +++ b/openjpa-jest/src/main/java/org/apache/openjpa/persistence/jest/ObjectFormatter.java @@ -0,0 +1,107 @@ +/* + * 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.openjpa.persistence.jest; + +import java.io.IOException; +import java.io.OutputStream; +import java.net.URI; +import java.text.SimpleDateFormat; +import java.util.Collection; + +import javax.persistence.metamodel.Metamodel; +import javax.servlet.http.HttpServletResponse; + +import org.apache.openjpa.kernel.OpenJPAStateManager; + +/** + * A parameterized interface defines the protocol for converting {@link OpenJPAStateManager managed} persistence + * instances or a persistent {@link Metamodel domain model} into a form suitable for transport to a language-neutral + * client such as an web browser. + *

+ * The interface prefers that the resultant resource as a complete representation i.e. all the references + * contained in the resource can be resolved within the same resource itself. As the intended recipient of this + * resource is a remote client, an incomplete resource will require the client to request further for + * any (unresolved) reference resulting in a chatty protocol. + *

+ * This interface also defines methods for writing the representation into an output stream e.g. + * {@link HttpServletResponse#getOutputStream() response output stream} of a HTTP Servlet. + *

+ * Implementation Note: Each concrete formatter type is registered with {@linkplain PrototypeFactory factory} + * that requires the implementation to have a no-argument constructor. + * + * @param the type of encoded output + * + * @author Pinaki Poddar + * + */ +public interface ObjectFormatter { + public static final SimpleDateFormat dateFormat = new SimpleDateFormat("MMM dd, yyyy"); + + /** + * Gets the mime type produced by this formatter. + */ + public String getMimeType(); + + /** + * Encode the {@link Closure persistent closure} of the given collection of managed instances as a + * resource e.g a XML or HTML document or an interactive document with JavaScript or a JSON array. + * Exact nature of the output type is the generic parameter of this interface. + * + * @param objs a collection of managed instances + * @param model domain model + * + * @return an encoded object e.g. a XML or HTML Document or a JSON object. + */ + public T encode(Collection objs, Metamodel model); + + /** + * Encode the given domain model in to a object. + * + * @param model a meta-model of managed types + * + * @return an encoded object e.g. a XML or HTML Document or a JSON object. + */ + public T encode(Metamodel model); + + /** + * Encodes the {@link Closure persistent closure} of the given collection of objects, then write it into + * the given output stream. + * + * @param objs the collection of objects to be formatted. + * @param model a meta-model of managed types, provided for easier introspection if necessary + * @param title TODO + * @param desc TODO + * @param uri TODO + * @param writer a text-oriented output stream + * @throws IOException + */ + public T writeOut(Collection objs, Metamodel model, + String title, String desc, URI uri, OutputStream out) throws IOException; + + /** + * Encodes the given domain model, then write it into the given output stream. + * + * @param model a meta-model of managed types + * @param writer a text-oriented output stream + * + * @throws IOException + */ + public T writeOut(Metamodel model, String title, String desc, URI uri, OutputStream out) throws IOException; +} diff --git a/openjpa-jest/src/main/java/org/apache/openjpa/persistence/jest/ProcessingException.java b/openjpa-jest/src/main/java/org/apache/openjpa/persistence/jest/ProcessingException.java new file mode 100644 index 000000000..46a1f24d9 --- /dev/null +++ b/openjpa-jest/src/main/java/org/apache/openjpa/persistence/jest/ProcessingException.java @@ -0,0 +1,97 @@ +/* + * 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.openjpa.persistence.jest; + +import static java.net.HttpURLConnection.HTTP_INTERNAL_ERROR; +import static org.apache.openjpa.persistence.jest.Constants.MIME_TYPE_XML; + +import java.io.IOException; +import java.net.URLDecoder; + +import javax.servlet.http.HttpServletResponse; + +import org.apache.openjpa.lib.util.Localizer.Message; +import org.w3c.dom.Document; + +/** + * Specialized RuntimException thrown by JEST commands. + * The exception can be serialized to the output stream of a HTTP Servlet response as a HTML page. + * + * @author Pinaki Poddar + * + */ +@SuppressWarnings("serial") +public class ProcessingException extends RuntimeException { + private final JPAServletContext ctx; + private final int _errorCode; + + public ProcessingException(JPAServletContext ctx, Throwable error) { + this(ctx, error, HTTP_INTERNAL_ERROR); + } + + public ProcessingException(JPAServletContext ctx, Throwable error, int errorCode) { + super(error); + this.ctx = ctx; + this._errorCode = errorCode; + } + + public ProcessingException(JPAServletContext ctx, Message message, int errorCode) { + super(message.toString()); + this.ctx = ctx; + this._errorCode = errorCode; + } + + public ProcessingException(JPAServletContext ctx, Throwable error, Message message) { + this(ctx, error, message, HTTP_INTERNAL_ERROR); + } + + public ProcessingException(JPAServletContext ctx, Throwable error, Message message, int errorCode) { + super(message.toString(), error); + this.ctx = ctx; + this._errorCode = errorCode; + } + + /** + * Prints the stack trace in a HTML format on the given response output stream. + * + * @param response + * @throws IOException + */ + public void printStackTrace() { + HttpServletResponse response = ctx.getResponse(); + response.setContentType(MIME_TYPE_XML); + response.setStatus(_errorCode); + + String uri = ctx.getRequestURI().toString(); + try { + uri = URLDecoder.decode(uri, "UTF-8"); + } catch (Exception e) { + } + Throwable t = this.getCause() == null ? this : getCause(); + ExceptionFormatter formatter = new ExceptionFormatter(); + Document xml = formatter.createXML("Request URI: " + uri, t); + try { + formatter.write(xml, response.getOutputStream()); + } catch (IOException e) { + e.printStackTrace(); + throw new RuntimeException("Request URI: " + uri, e); + } + } +} diff --git a/openjpa-jest/src/main/java/org/apache/openjpa/persistence/jest/PropertiesCommand.java b/openjpa-jest/src/main/java/org/apache/openjpa/persistence/jest/PropertiesCommand.java new file mode 100644 index 000000000..b5056bd52 --- /dev/null +++ b/openjpa-jest/src/main/java/org/apache/openjpa/persistence/jest/PropertiesCommand.java @@ -0,0 +1,77 @@ +/* + * 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.openjpa.persistence.jest; + +import static org.apache.openjpa.persistence.jest.Constants.*; + +import java.io.IOException; +import java.io.PrintWriter; +import java.net.HttpURLConnection; +import java.util.Iterator; +import java.util.Map; + +import javax.servlet.http.HttpServletResponse; + +import org.w3c.dom.Document; + + +/** + * Represents configuration properties in HTML. + * + * @author Pinaki Poddar + * + */ +public class PropertiesCommand extends AbstractCommand { + private static final char DOT = '.'; + + public PropertiesCommand(JPAServletContext ctx) { + super(ctx); + } + + protected int getMaximumArguments() { + return 0; + } + + @Override + public void process() throws ProcessingException, IOException { + HttpServletResponse response = ctx.getResponse(); + response.setContentType(MIME_TYPE_XML); + + Map properties = ctx.getPersistenceContext().getProperties(); + removeBadEntries(properties); + PropertiesFormatter formatter = new PropertiesFormatter(); + String caption = _loc.get("properties-caption", ctx.getPersistenceUnitName()).toString(); + Document xml = formatter.createXML(caption, "", "", properties); + formatter.write(xml, response.getOutputStream()); + response.setStatus(HttpURLConnection.HTTP_OK); + } + + private void removeBadEntries(Map map) { + Iterator keys = map.keySet().iterator(); + for (; keys.hasNext();) { + if (keys.next().indexOf(DOT) == -1) keys.remove(); + } + } + + @Override + protected Format getDefaultFormat() { + return Format.xml; + } +} diff --git a/openjpa-jest/src/main/java/org/apache/openjpa/persistence/jest/PropertiesFormatter.java b/openjpa-jest/src/main/java/org/apache/openjpa/persistence/jest/PropertiesFormatter.java new file mode 100644 index 000000000..6b4df9e6d --- /dev/null +++ b/openjpa-jest/src/main/java/org/apache/openjpa/persistence/jest/PropertiesFormatter.java @@ -0,0 +1,49 @@ +/* + * 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.openjpa.persistence.jest; + +import java.util.Arrays; +import java.util.Map; + +import org.w3c.dom.Document; +import org.w3c.dom.Element; + +/** + * Formats a key-value pair in a HTML Document. + * + * @author Pinaki Poddar + * + */ +class PropertiesFormatter extends XMLFormatter { + public Document createXML(String title, String tkey, String tvalue, Map properties) { + Element root = newDocument(Constants.ROOT_ELEMENT_PROPERTIES); + for (Map.Entry entry : properties.entrySet()) { + Element property = root.getOwnerDocument().createElement("property"); + Object value = entry.getValue(); + String v = value == null + ? Constants.NULL_VALUE + : value.getClass().isArray() ? Arrays.toString((Object[])value) : value.toString(); + property.setAttribute(Constants.ATTR_PROPERTY_KEY, entry.getKey()); + property.setAttribute(Constants.ATTR_PROPERTY_VALUE, v); + root.appendChild(property); + } + return root.getOwnerDocument(); + } +} diff --git a/openjpa-jest/src/main/java/org/apache/openjpa/persistence/jest/PrototypeFactory.java b/openjpa-jest/src/main/java/org/apache/openjpa/persistence/jest/PrototypeFactory.java new file mode 100644 index 000000000..7f4e70c65 --- /dev/null +++ b/openjpa-jest/src/main/java/org/apache/openjpa/persistence/jest/PrototypeFactory.java @@ -0,0 +1,126 @@ +/* + * 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.openjpa.persistence.jest; + +import java.lang.reflect.Constructor; +import java.util.Collections; +import java.util.Map; +import java.util.Set; +import java.util.TreeMap; + +import org.apache.openjpa.kernel.Filters; + +/** + * A factory for a specific type of objects registered by a key. + * The client registers a type indexed by name. + * The client can get a new instance of the registered type. + * The requested registered type not necessarily have to have a no-arg + * constructor. The constructor arguments can be passed during + * {@link #newInstance(Class, Object...) new instance} request. Based on the + * arguments, a matching constructor, if any, is located and invoked. + * + * type of key for this registry + * base type of the objects to construct + * + * @author Pinaki Poddar + * + */ +public class PrototypeFactory { + private Map> _registry = new TreeMap>(); + + /** + * Register the given class with the given key. + * + * @param key a non-null key. + * @param prototype a type. + */ + public void register(K key, Class prototype) { + _registry.put(key, prototype); + } + + /** + * Create a new instance of the type {@linkplain #register(Object, Class) registered} before + * with the given key, if any. + * The given arguments are used to identify a constructor of the registered type and + * passed to the constructor of the registered type. + * + * @param key a key to identify a registered type. + * @param args arguments to pass to the constructor of the type. + * + * @return null if no type has been registered against the given key. + */ + public T newInstance(K key, Object... args) { + return _registry.containsKey(key) ? newInstance(_registry.get(key), args) : null; + } + + /** + * Gets the keys registered in this factory. + * + * @return immutable set of registered keys. + */ + public Set getRegisteredKeys() { + return Collections.unmodifiableSet(_registry.keySet()); + } + + private T newInstance(Class type, Object... args) { + try { + return findConstructor(type, getConstructorParameterTypes(args)).newInstance(args); + } catch (Exception e) { + throw new RuntimeException(); + } + } + + Class[] getConstructorParameterTypes(Object... args) { + if (args == null || args.length == 0) { + return new Class[0]; + } + Class[] types = new Class[args.length]; + for (int i = 0; i < args.length; i++) { + types[i] = args[i] == null ? Object.class : args[i].getClass(); + } + return types; + } + + /** + * Finds a constructor of the given class with given argument types. + */ + Constructor findConstructor(Class cls, Class[] types) { + try { + return cls.getConstructor(types); + } catch (Exception e) { + Constructor[] constructors = cls.getConstructors(); + for (Constructor cons : constructors) { + Class[] paramTypes = cons.getParameterTypes(); + boolean match = false; + if (paramTypes.length == types.length) { + for (int i = 0; i < paramTypes.length; i++) { + match = paramTypes[i].isAssignableFrom(Filters.wrap(types[i])); + if (!match) + break; + } + } + if (match) { + return (Constructor)cons; + } + } + } + throw new RuntimeException();//_loc.get("fill-ctor-none", cls, Arrays.toString(types)).getMessage()); + } +} diff --git a/openjpa-jest/src/main/java/org/apache/openjpa/persistence/jest/QueryCommand.java b/openjpa-jest/src/main/java/org/apache/openjpa/persistence/jest/QueryCommand.java new file mode 100644 index 000000000..b2582b9a7 --- /dev/null +++ b/openjpa-jest/src/main/java/org/apache/openjpa/persistence/jest/QueryCommand.java @@ -0,0 +1,98 @@ +/* + * 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.openjpa.persistence.jest; + +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import static org.apache.openjpa.persistence.jest.Constants.*; + +import javax.persistence.EntityManager; +import javax.persistence.Query; + +import org.apache.openjpa.persistence.ArgumentException; +import org.apache.openjpa.persistence.OpenJPAEntityManager; + +import static java.net.HttpURLConnection.HTTP_BAD_REQUEST;; + +/** + * Executes query. + * + * @author Pinaki Poddar + * + */ +class QueryCommand extends AbstractCommand { + private static final List _mandatoryArgs = Arrays.asList(ARG_QUERY); + private static final List _validQualifiers = Arrays.asList( + QUALIFIER_FORMAT, QUALIFIER_PLAN, QUALIFIER_NAMED, QUALIFIER_SINGLE, + QUALIFIER_FIRSTRESULT, QUALIFIER_MAXRESULT); + + public QueryCommand(JPAServletContext ctx) { + super(ctx); + } + + @Override + protected Collection getMandatoryArguments() { + return _mandatoryArgs; + } + + @Override + protected int getMinimumArguments() { + return 0; + } + + protected Collection getValidQualifiers() { + return _validQualifiers; + } + + @Override + public void process() throws ProcessingException { + String spec = getMandatoryArgument(ARG_QUERY); + OpenJPAEntityManager em = ctx.getPersistenceContext(); + try { + Query query = isBooleanQualifier(QUALIFIER_NAMED) ? em.createNamedQuery(spec) : em.createQuery(spec); + if (hasQualifier(QUALIFIER_FIRSTRESULT)) + query.setFirstResult(Integer.parseInt(getQualifier(QUALIFIER_FIRSTRESULT))); + if (hasQualifier(QUALIFIER_MAXRESULT)) + query.setMaxResults(Integer.parseInt(getQualifier(QUALIFIER_MAXRESULT))); + pushFetchPlan(query); + + Map args = getArguments(); + for (Map.Entry entry : args.entrySet()) { + query.setParameter(entry.getKey(), entry.getValue()); + } + getObjectFormatter() + .writeOut(toStateManager(isBooleanQualifier(QUALIFIER_SINGLE) + ? Collections.singleton(query.getSingleResult()) : query.getResultList()), + em.getMetamodel(), + _loc.get("query-title").toString(), _loc.get("query-desc").toString(), ctx.getRequestURI(), + ctx.getResponse().getOutputStream()); + } catch (ArgumentException e1) { + throw new ProcessingException(ctx, e1, _loc.get("query-execution-error", spec), HTTP_BAD_REQUEST); + } catch (Exception e) { + throw new ProcessingException(ctx, e, _loc.get("query-execution-error", spec)); + } finally { + popFetchPlan(false); + } + } + +} diff --git a/openjpa-jest/src/main/java/org/apache/openjpa/persistence/jest/TokenReplacedStream.java b/openjpa-jest/src/main/java/org/apache/openjpa/persistence/jest/TokenReplacedStream.java new file mode 100644 index 000000000..28c5ac15d --- /dev/null +++ b/openjpa-jest/src/main/java/org/apache/openjpa/persistence/jest/TokenReplacedStream.java @@ -0,0 +1,180 @@ +/* + * 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.openjpa.persistence.jest; + +import java.io.IOException; +import java.io.InputStream; +import java.io.Writer; +import java.util.Arrays; + +/** + * Reads from an input stream and writes to an output stream after replacing matched tokens + * by their counterpart. + * + * + * @author Pinaki Poddar + * + */ +public class TokenReplacedStream { + /** + * Read the given input stream and replaces the tokens as it reads. The replaced stream is written to the + * given output stream. + * + * @param in a non-null input stream + * @param out a character oriented writer + * @param replacements an even number of Strings. Any occurrence of the even-indexed i-th String in the + * input stream will be replaced by the (i+1)-th String in the output writer. + */ + public void replace(InputStream in, Writer out, String... prs) throws IOException { + if (prs.length%2 != 0) + throw new IllegalArgumentException("Even number of pattern/string pairs: " + Arrays.toString(prs) + + ". Must be even number of arguments."); + Pattern[] patterns = new Pattern[prs.length/2]; + for (int i = 0; i < prs.length; i += 2) { + patterns[i/2] = new Pattern(prs[i], prs[i+1]); + } + + StringBuilder tmp = new StringBuilder(); + for (int c = 0; (c = in.read()) != -1;) { + int cursor = match((char)c, patterns); + if (cursor < 0) { // no pattern recognized at all + if (tmp.length() > 0) { // append partial match then discard partial memory + for (int j = 0; j < tmp.length(); j++) { + out.write(tmp.charAt(j)); + } + tmp.delete(0, tmp.length()); + } + out.write((char)c); // directly output + } else { + Pattern p = matched(patterns); // has any pattern matched completely + if (p != null) { // a pattern matched completely + char[] replace = p.replace().toCharArray(); + for (int j = 0; j < replace.length; j++) { + out.write(replace[j]); + } + reset(patterns); + tmp.delete(0, tmp.length()); + } else { + tmp.append((char)c); // remember partial match + } + } + } + } + + /** + * Match the given character to all patterns and return the index of highest match. + * @param c a character to match + * @param patterns an array of patterns + * @return -1 if character matched no pattern + */ + int match(char c, Pattern...patterns) { + if (patterns == null) + return -1; + int result = -1; + for (Pattern p : patterns) { + result = Math.max(result, p.match(c)); + } + return result; + } + + /** + * Gets the pattern if any in matched state + * @param patterns + * @return + */ + Pattern matched(Pattern...patterns) { + if (patterns == null) + return null; + for (Pattern p : patterns) { + if (p.isMatched()) return p; + } + return null; + } + + /** + * Resets all the patterns. + * @param patterns + */ + void reset(Pattern...patterns) { + if (patterns == null) + return; + for (Pattern p : patterns) { + p.reset(); + } + } + + public static class Pattern { + private final char[] chars; + private final String _replace; + private int _cursor; + + /** + * Construct a pattern and its replacement. + */ + public Pattern(String s, String replace) { + if (s == null || s.length() == 0) + throw new IllegalArgumentException("Pattern [" + s + "] can not be empty or null "); + if (replace == null) + throw new IllegalArgumentException("Replacement [" + replace + "] is null for pattern [" + s + "]"); + chars = s.toCharArray(); + _cursor = -1; + _replace = replace; + } + + /** + * Match the given character with the current cursor and advance the matching length. + * @param c + * @return the matching length. -1 denotes the pattern did not match the character. + */ + public int match(char c) { + if (c != chars[++_cursor]) { + reset(); + } + return _cursor; + } + + /** + * Reset the cursor. Subsequent matching will begin at start. + */ + public void reset() { + _cursor = -1; + } + + /** + * Is this pattern matched fully? + * A pattern is fully matched when the matching length is equal to the length of the pattern string. + */ + public boolean isMatched() { + return _cursor == chars.length-1; + } + + /** + * Gets the string to be replaced. + */ + public String replace() { + return _replace; + } + + public String toString() { + return new String(chars) + ":" + _cursor; + } + } + +} diff --git a/openjpa-jest/src/main/java/org/apache/openjpa/persistence/jest/XMLFormatter.java b/openjpa-jest/src/main/java/org/apache/openjpa/persistence/jest/XMLFormatter.java new file mode 100644 index 000000000..c09c369c8 --- /dev/null +++ b/openjpa-jest/src/main/java/org/apache/openjpa/persistence/jest/XMLFormatter.java @@ -0,0 +1,516 @@ +/* + * 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.openjpa.persistence.jest; + +import static org.apache.openjpa.persistence.jest.Constants.*; + +import java.io.BufferedReader; +import java.io.CharArrayWriter; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.io.Reader; +import java.io.Writer; +import java.net.URI; +import java.net.URLEncoder; +import java.text.SimpleDateFormat; +import java.util.Arrays; +import java.util.BitSet; +import java.util.Collection; +import java.util.Collections; +import java.util.Date; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import javax.persistence.metamodel.Attribute; +import javax.persistence.metamodel.ManagedType; +import javax.persistence.metamodel.MapAttribute; +import javax.persistence.metamodel.Metamodel; +import javax.persistence.metamodel.PluralAttribute; +import javax.persistence.metamodel.SingularAttribute; +import javax.xml.XMLConstants; +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.transform.OutputKeys; +import javax.xml.transform.Source; +import javax.xml.transform.Transformer; +import javax.xml.transform.TransformerFactory; +import javax.xml.transform.dom.DOMSource; +import javax.xml.transform.stream.StreamResult; +import javax.xml.transform.stream.StreamSource; +import javax.xml.validation.Schema; +import javax.xml.validation.SchemaFactory; +import javax.xml.validation.Validator; + +import org.apache.openjpa.kernel.OpenJPAStateManager; +import org.apache.openjpa.kernel.StoreContext; +import org.apache.openjpa.meta.ClassMetaData; +import org.apache.openjpa.meta.FieldMetaData; +import org.apache.openjpa.meta.JavaTypes; +import org.apache.openjpa.meta.ValueMetaData; +import org.apache.openjpa.persistence.meta.Members; +import org.apache.openjpa.util.InternalException; +import org.w3c.dom.CDATASection; +import org.w3c.dom.Document; +import org.w3c.dom.Element; + +/** + * Marshals a root instance and its persistent closure as an XML element. + * The closure is resolved against the persistence context that contains the root instance. + * The XML document adheres to the jest-instance.xsd schema. + * + * @author Pinaki Poddar + * + */ +public class XMLFormatter implements ObjectFormatter { + + public static final Schema _xsd; + private static final DocumentBuilder _builder; + private static final Transformer _transformer; + private static final String EMPTY_TEXT = " "; + + static { + try { + _builder = DocumentBuilderFactory.newInstance().newDocumentBuilder(); + _transformer = TransformerFactory.newInstance().newTransformer(); + SchemaFactory factory = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI); + InputStream xsd = XMLFormatter.class.getResourceAsStream(JEST_INSTANCE_XSD); + _xsd = factory.newSchema(new StreamSource(xsd)); + + _transformer.setOutputProperty(OutputKeys.METHOD, "xml"); + _transformer.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "no"); + _transformer.setOutputProperty(OutputKeys.INDENT, "yes"); + _transformer.setOutputProperty(OutputKeys.STANDALONE, "no"); + _transformer.setOutputProperty(OutputKeys.ENCODING, "UTF-8"); + _transformer.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "2"); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + public String getMimeType() { + return MIME_TYPE_XML; + } + + /** + * Encodes the closure of given collection of managed instance into a new XML document + * according to JEST Instance XML Schema. + * + * @param sm a collection of managed instances. + * @param parent the parent node to which the new node be attached. + */ + public Document encode(final Collection sms, Metamodel model) { + Element root = newDocument(ROOT_ELEMENT_INSTANCE); + Closure closure = new Closure(sms); + for (OpenJPAStateManager sm : closure) { + encodeManagedInstance(sm, root, false, model); + } + return root.getOwnerDocument(); + } + + /** + * Encodes the given meta-model into a new XML document according to JEST Domain XML Schema. + * + * @param model a persistent domain model. Must not be null. + */ + public Document encode(Metamodel model) { + Element root = newDocument(ROOT_ELEMENT_MODEL); + for (ManagedType t : model.getManagedTypes()) { + encodeManagedType(t, root); + } + return root.getOwnerDocument(); + } + + /** + * Create a new document with the given tag as the root element. + * + * @param rootTag the tag of the root element + * + * @return the document element of a new document + */ + public Element newDocument(String rootTag) { + Document doc = _builder.newDocument(); + Element root = doc.createElement(rootTag); + doc.appendChild(root); + String[] nvpairs = new String[] { + "xmlns:xsi", XMLConstants.W3C_XML_SCHEMA_INSTANCE_NS_URI, +// "xsi:noNamespaceSchemaLocation", JEST_INSTANCE_XSD, + ATTR_VERSION, "1.0", + }; + for (int i = 0; i < nvpairs.length; i += 2) { + root.setAttribute(nvpairs[i], nvpairs[i+1]); + } + return root; + } + + + @Override + public Document writeOut(Collection objs, Metamodel model, String title, String desc, + URI uri, OutputStream out) throws IOException { + Document doc = encode(objs, model); + decorate(doc, title, desc, uri); + write(doc, out); + return doc; + } + + @Override + public Document writeOut(Metamodel model, String title, String desc, URI uri, OutputStream out) + throws IOException { + Document doc = encode(model); + decorate(doc, title, desc, uri); + write(doc, out); + return doc; + } + + Document decorate(Document doc, String title, String desc, URI uri) { + Element root = doc.getDocumentElement(); + Element instance = (Element)root.getElementsByTagName(ELEMENT_INSTANCE).item(0); + Element uriElement = doc.createElement(ELEMENT_URI); + uriElement.setTextContent(uri == null ? NULL_VALUE : uri.toString()); + Element descElement = doc.createElement(ELEMENT_DESCRIPTION); + descElement.setTextContent(desc == null ? NULL_VALUE : desc); + root.insertBefore(uriElement, instance); + root.insertBefore(descElement, instance); + return doc; + } + + public void write(Document doc, OutputStream out) throws IOException { + try { + _transformer.transform(new DOMSource(doc), new StreamResult(out)); + } catch (Exception e) { + throw new IOException(e); + } + } + + public void write(Document doc, Writer writer) throws IOException { + try { + _transformer.transform(new DOMSource(doc), new StreamResult(writer)); + } catch (Exception e) { + throw new IOException(e); + } + } + + /** + * Encodes the closure of a persistent instance into a XML element. + * + * @param sm the managed instance to be encoded. Can be null. + * @param parent the parent XML element to which the new XML element be added. Must not be null. Must be + * owned by a document. + * @param visited the persistent instances that had been encoded already. Must not be null or immutable. + * + * @return the new element. The element has been appended as a child to the given parent in this method. + */ + private Element encodeManagedInstance(final OpenJPAStateManager sm, final Element parent, + boolean isRef, Metamodel model) { + if (parent == null) + throw new InternalException(_loc.get("format-xml-null-parent")); + Document doc = parent.getOwnerDocument(); + if (doc == null) + throw new InternalException(_loc.get("format-xml-null-doc")); + + if (sm == null || isRef) { + return encodeRef(parent, sm); + } + Element root = doc.createElement(ELEMENT_INSTANCE); + parent.appendChild(root); + root.setAttribute(ATTR_ID, ior(sm)); + Element child = null; + BitSet loaded = sm.getLoaded(); + StoreContext ctx = (StoreContext)sm.getGenericContext(); + List> attrs = MetamodelHelper.getAttributesInOrder(sm.getMetaData(), model); + for (int i = 0; i < attrs.size(); child = null, i++) { + Members.Member attr = (Members.Member) attrs.get(i); + FieldMetaData fmd = attr.fmd; + if (!loaded.get(fmd.getIndex())) + continue; + String tag = MetamodelHelper.getTagByAttributeType(attr); + Object value = sm.fetch(fmd.getIndex()); + switch (fmd.getDeclaredTypeCode()) { + case JavaTypes.BOOLEAN: + case JavaTypes.BYTE: + case JavaTypes.CHAR: + case JavaTypes.DOUBLE: + case JavaTypes.FLOAT: + case JavaTypes.INT: + case JavaTypes.LONG: + case JavaTypes.SHORT: + + case JavaTypes.BOOLEAN_OBJ: + case JavaTypes.BYTE_OBJ: + case JavaTypes.CHAR_OBJ: + case JavaTypes.DOUBLE_OBJ: + case JavaTypes.FLOAT_OBJ: + case JavaTypes.INT_OBJ: + case JavaTypes.LONG_OBJ: + case JavaTypes.SHORT_OBJ: + + case JavaTypes.BIGDECIMAL: + case JavaTypes.BIGINTEGER: + case JavaTypes.DATE: + case JavaTypes.NUMBER: + case JavaTypes.CALENDAR: + case JavaTypes.LOCALE: + case JavaTypes.STRING: + case JavaTypes.ENUM: + child = doc.createElement(tag); + child.setAttribute(ATTR_NAME, fmd.getName()); + if (value == null) { + encodeNull(child); + } else { + encodeBasic(child, value, fmd.getDeclaredType()); + } + break; + + case JavaTypes.OID: + child = doc.createElement(ELEMENT_REF); + child.setAttribute(ATTR_NAME, fmd.getName()); + if (value == null) { + encodeNull(child); + } else { + encodeBasic(child, value, fmd.getDeclaredType()); + } + break; + + case JavaTypes.PC: + child = doc.createElement(tag); + child.setAttribute(ATTR_NAME, fmd.getName()); + child.setAttribute(ATTR_TYPE, typeOf(fmd)); + OpenJPAStateManager other = ctx.getStateManager(value); + encodeManagedInstance(other, child, true, model); + break; + + case JavaTypes.ARRAY: + Object[] values = (Object[])value; + value = Arrays.asList(values); + // no break; + case JavaTypes.COLLECTION: + child = doc.createElement(tag); + child.setAttribute(ATTR_NAME, fmd.getName()); + child.setAttribute(ATTR_TYPE, typeOf(fmd)); + child.setAttribute(ATTR_MEMBER_TYPE, typeOf(fmd.getElement().getDeclaredType())); + if (value == null) { + encodeNull(child); + break; + } + Collection members = (Collection)value; + boolean basic = fmd.getElement().getTypeMetaData() == null; + for (Object o : members) { + Element member = doc.createElement(ELEMENT_MEMBER); + child.appendChild(member); + if (o == null) { + encodeNull(member); + } else { + if (basic) { + encodeBasic(member, o, o.getClass()); + } else { + encodeManagedInstance(ctx.getStateManager(o), member, true, model); + } + } + } + break; + case JavaTypes.MAP: + child = doc.createElement(tag); + child.setAttribute(ATTR_NAME, fmd.getName()); + child.setAttribute(ATTR_TYPE, typeOf(fmd)); + child.setAttribute(ATTR_KEY_TYPE, typeOf(fmd.getElement().getDeclaredType())); + child.setAttribute(ATTR_VALUE_TYPE, typeOf(fmd.getValue().getDeclaredType())); + if (value == null) { + encodeNull(child); + break; + } + Set entries = ((Map)value).entrySet(); + boolean basicKey = fmd.getElement().getTypeMetaData() == null; + boolean basicValue = fmd.getValue().getTypeMetaData() == null; + for (Map.Entry e : entries) { + Element entry = doc.createElement(ELEMENT_ENTRY); + Element entryKey = doc.createElement(ELEMENT_ENTRY_KEY); + Element entryValue = doc.createElement(ELEMENT_ENTRY_VALUE); + entry.appendChild(entryKey); + entry.appendChild(entryValue); + child.appendChild(entry); + if (e.getKey() == null) { + encodeNull(entryKey); + } else { + if (basicKey) { + encodeBasic(entryKey, e.getKey(), e.getKey().getClass()); + } else { + encodeManagedInstance(ctx.getStateManager(e.getKey()), entryKey, true, model); + } + } + if (e.getValue() == null) { + encodeNull(entryValue); + } else { + if (basicValue) { + encodeBasic(entryValue, e.getValue(), e.getValue().getClass()); + } else { + encodeManagedInstance(ctx.getStateManager(e.getValue()), entryValue, true, model); + } + } + } + break; + + case JavaTypes.INPUT_STREAM: + case JavaTypes.INPUT_READER: + child = doc.createElement(tag); + child.setAttribute(ATTR_NAME, fmd.getName()); + child.setAttribute(ATTR_TYPE, typeOf(fmd)); + if (value == null) { + encodeNull(child); + } else { + CDATASection data = doc.createCDATASection(streamToString(value)); + child.appendChild(data); + } + break; + + case JavaTypes.PC_UNTYPED: + case JavaTypes.OBJECT: + System.err.println("Not handled " + fmd.getName() + " of type " + fmd.getDeclaredType()); + } + + if (child != null) { + root.appendChild(child); + } + } + return root; + } + + /** + * Sets the given value element as null. The null attribute is set to true. + * + * @param element the XML element to be set + */ + private void encodeNull(Element element) { + element.setAttribute(ATTR_NULL, "true"); + } + + private Element encodeRef(Element parent, OpenJPAStateManager sm) { + Element ref = parent.getOwnerDocument().createElement(sm == null ? ELEMENT_NULL_REF : ELEMENT_REF); + if (sm != null) + ref.setAttribute(ATTR_ID, ior(sm)); + // IMPORTANT: for xml transformer not to omit the closing tag, otherwise dojo is confused + ref.setTextContent(EMPTY_TEXT); + parent.appendChild(ref); + return ref; + } + + + /** + * Sets the given value element. The type is set to the given runtime type. + * String form of the given object is set as the text content. + * + * @param element the XML element to be set + * @param obj value of the element. Never null. + */ + private void encodeBasic(Element element, Object obj, Class runtimeType) { + element.setAttribute(ATTR_TYPE, typeOf(runtimeType)); + if (obj instanceof Date) + element.setTextContent(dateFormat.format(obj)); + else + element.setTextContent(obj == null ? NULL_VALUE : obj.toString()); + } + + + + /** + * Convert the given stream (either an InutStream or a Reader) to a String + * to be included in CDATA section of a XML document. + * + * @param value the field value to be converted. Can not be null + * @return + */ + private String streamToString(Object value) { + Reader reader = null; + if (value instanceof InputStream) { + reader = new BufferedReader(new InputStreamReader((InputStream)value)); + } else if (value instanceof Reader) { + reader = (Reader)value; + } else { + throw new RuntimeException(); + } + CharArrayWriter writer = new CharArrayWriter(); + try { + for (int c; (c = reader.read()) != -1;) { + writer.write(c); + } + } catch (IOException ex) { + throw new RuntimeException(ex); + } + return writer.toString(); + } + + + private void encodeManagedType(ManagedType type, Element parent) { + Document doc = parent.getOwnerDocument(); + Element root = doc.createElement(type.getPersistenceType().toString().toLowerCase()); + parent.appendChild(root); + root.setAttribute(ATTR_NAME, type.getJavaType().getSimpleName()); + List> attributes = MetamodelHelper.getAttributesInOrder(type); + for (Attribute a : attributes) { + String tag = MetamodelHelper.getTagByAttributeType(a); + + Element child = doc.createElement(tag); + root.appendChild(child); + child.setAttribute(ATTR_TYPE, typeOf(a.getJavaType())); + if (a instanceof PluralAttribute) { + if (a instanceof MapAttribute) { + child.setAttribute(ATTR_KEY_TYPE, typeOf(((MapAttribute)a).getKeyJavaType())); + child.setAttribute(ATTR_VALUE_TYPE, typeOf(((MapAttribute)a).getBindableJavaType())); + } else { + child.setAttribute(ATTR_MEMBER_TYPE, typeOf(((PluralAttribute)a).getBindableJavaType())); + } + } + child.setTextContent(a.getName()); + } + } + + void validate(Document doc) throws Exception { + Validator validator = _xsd.newValidator(); + validator.validate(new DOMSource(doc)); + } + + String ior(OpenJPAStateManager sm) { + return typeOf(sm) + "-" + sm.getObjectId(); + } + + String typeOf(OpenJPAStateManager sm) { + return sm.getMetaData().getDescribedType().getSimpleName(); + } + + String typeOf(Class cls) { + return cls.getSimpleName(); + } + + String typeOf(ClassMetaData meta) { + return meta.getDescribedType().getSimpleName(); + } + + String typeOf(ValueMetaData vm) { + if (vm.getTypeMetaData() == null) + return typeOf(vm.getType()); + return typeOf(vm.getTypeMetaData()); + } + + String typeOf(FieldMetaData fmd) { + return fmd.getType().getSimpleName(); + } +} diff --git a/openjpa-jest/src/main/resources/org/apache/openjpa/persistence/jest/help/entity-name.html b/openjpa-jest/src/main/resources/org/apache/openjpa/persistence/jest/help/entity-name.html new file mode 100644 index 000000000..82ea5a7a1 --- /dev/null +++ b/openjpa-jest/src/main/resources/org/apache/openjpa/persistence/jest/help/entity-name.html @@ -0,0 +1,29 @@ + + + + + +

Entity Name

+ +Specify simple name of a persistent entity. +JEST will resolve the name to the fully-qualified name of the entity. + + + \ No newline at end of file diff --git a/openjpa-jest/src/main/resources/org/apache/openjpa/persistence/jest/help/fetch-plan.html b/openjpa-jest/src/main/resources/org/apache/openjpa/persistence/jest/help/fetch-plan.html new file mode 100644 index 000000000..9f0f92b9a --- /dev/null +++ b/openjpa-jest/src/main/resources/org/apache/openjpa/persistence/jest/help/fetch-plan.html @@ -0,0 +1,51 @@ + + + + + +

Dynamic Fetch Plan

+ +Specify one or more named fetch plan. Separate multiple names with comma, e.g. +

+myPlanA, mYPlanB +

+There are two pre-defined plans named: default and all. The default plan that fetches +all properties of basic type (i.e. String, int, Date etc.) and uni-cardinality relations, are +active by default. As the plans are additive, to exclude the default plan, you can specify +

+myPlanA, -default +

+ + +Fetch Plan determines which properties will be fetched when an entity instance is accessed from the data store. +A plan can traverse relationship path into other entities at arbitrary depth. By default, when an entity +is fetched, all properties of basic type (i.e. String, int, Date etc.) and uni-cardinality relations are +fetched. +
+JPA specification allows @Fetch.LAZY and @Fetch.EAGER annotation on persistent properties to control +fetch behavior. But only statically i.e. at class definition. +

+OpenJPA, on the other hand, provides a far richer syntax and semantics to its user to define the +properties to be fetched through its dynamic Fetch Plan facility. And, more importantly, these +fetch plans can be modified dynamically per use case basis. +

+To learn more aboout Fetch Plan, Refer OpenJPA documentation. + + \ No newline at end of file diff --git a/openjpa-jest/src/main/resources/org/apache/openjpa/persistence/jest/help/query.html b/openjpa-jest/src/main/resources/org/apache/openjpa/persistence/jest/help/query.html new file mode 100644 index 000000000..24c06c23a --- /dev/null +++ b/openjpa-jest/src/main/resources/org/apache/openjpa/persistence/jest/help/query.html @@ -0,0 +1,32 @@ + + + + + +

JPQL Query

+ +Specify a JPQL query or name of a pre-defined NamedQuery. +
+Both queries can accept query bind parameters. The type of the parameters are guessed by JEST +from the string you specify in this web form. +

+If using named query, check the Named Query Box. + + \ No newline at end of file diff --git a/openjpa-jest/src/main/resources/org/apache/openjpa/persistence/jest/help/response-format.html b/openjpa-jest/src/main/resources/org/apache/openjpa/persistence/jest/help/response-format.html new file mode 100644 index 000000000..0025291f2 --- /dev/null +++ b/openjpa-jest/src/main/resources/org/apache/openjpa/persistence/jest/help/response-format.html @@ -0,0 +1,28 @@ + + + + + +

Response Format

+ +Select the response format as XML or JSON. By default, JEST responses are in XML format. + + + \ No newline at end of file diff --git a/openjpa-jest/src/main/resources/org/apache/openjpa/persistence/jest/images/arrow_right.jpg b/openjpa-jest/src/main/resources/org/apache/openjpa/persistence/jest/images/arrow_right.jpg new file mode 100644 index 0000000000000000000000000000000000000000..da368a27b80144e4785bc10e419ce7d103fe77c9 GIT binary patch literal 5892 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 Bp8@~? literal 0 HcmV?d00001 diff --git a/openjpa-jest/src/main/resources/org/apache/openjpa/persistence/jest/images/domain.jpg b/openjpa-jest/src/main/resources/org/apache/openjpa/persistence/jest/images/domain.jpg new file mode 100644 index 0000000000000000000000000000000000000000..2136b3cc57222addeb9ec031d6f8849e24607e8f GIT binary patch literal 999 zcmex=5Vk)D~c1Vq{`rV_=8NGcpMZF|#NJ zvI-kI7EV-BHcBj7cyVJ8$RtJvCMISibAY~QVqs=w1DT{I2sDYAnVE$dZVe+7v!IZ$ zVxWlOK^E4eLN>?5|F;-;fCexLG7B=;GyFaB^u&{UE#{)Ht{uEFDO`Og_e9o1kHv$2 z$f_Av_`VA$pG({ zuU70kzu@SPMOQOJucz;HXV&am`tdyP(fU)R^@qb&D;?_E_VmrUG@-s(`)=8CmFgrv zR2R=lnrrfTV{7-3jY=tpEC?_she()%L_WSM0f%7I|dWXYF3^M~D8*|1I?Q`P@e`j_0a1%=7o{UsN#l zrd8pc+mC+m3-dn=Ei8?`yvwbe?ey6s&1jWHbF*fx4R?Cad)~$)EH>%I%lmu1%e#Ya zvwe)194B%jpDQWY@KMzgKIP?$W-VGTT(4H&`7GboUZ{DR$d%CO?nSZZ6)TU|Io-*2 z{8KYq-ebL3#GbhDJ#odeTK9O1&#as$STbj!%BokFW94t`tk{#aIkr=K-SMKz(st?K zbzAex{43)=R?Lm=I@*0`X58o3oR*$$T5ETyv`cQgSmS9)=gg+l6Ekjj@Eacwk@uKC!E1DJN3n@t~b*cFrDCQ04C_0 E0EaPziU0rr literal 0 HcmV?d00001 diff --git a/openjpa-jest/src/main/resources/org/apache/openjpa/persistence/jest/images/find.jpg b/openjpa-jest/src/main/resources/org/apache/openjpa/persistence/jest/images/find.jpg new file mode 100644 index 0000000000000000000000000000000000000000..cf8927cf5fcdbe1096c1409a382b5dbc31f8ec2d GIT binary patch literal 1412 zcmex=^(PF6}rMnOeST|r4lSw=>~TvNxu(8R<c1}I=;VrF4wW9Q)H;sz?% zD!{d!pzFb!U9xX3zTPI5o8roG<0MW4oqZMDikqloVbuf*=gfJ(V&YTRE(2~ znmD<{#3dx9RMpfqG__1j&CD$#!}nhriyFAPA|JKOCAXe;ymQ6;d(t`H1I)^E zS1jN8pMmk(PVdz>)hABPlMMO$Joxke$2Y3Eb~v8ADS36ZRN=FU*ScSHzWC4Z{j9Id zN{v$nria$ePSn?#l>hI?%CWn2+Zp<^)r}ndnbm(xe|>37Nz{!) z5<5I!m79c^eXjiY?u9!`^_8oQ*F*wc8H#89Is11pOY)DzH2ENhfAiNCY@41cz*Kww z%GY(Tj~eXB%wMtK5mQj`i?#hTelxK@@GYoXJNcH^b%T}Jkc4}Jgh+1Ax3 zr#-)_+tlppT39H$cFT0(9eo;CcJD4>C^c!%o>F=??y&2nr@O91R7%|5ENLYCN1J*cd&Wvq>toY#{~Z>k{R%Z}Pwo0-VQZT`;p6l!^^bhZE^pM%l?&tO|DeC} zyRH9!hNxZn%~H{xr7N@bmS?Tl+A;5WTmIi^J60w=wYsz6#uu#zi~ZV7{^`GVUhHf2 z{_OL^w^ROo=(DM--WY9GrF!L!)Soqht zE>Qo64@U*I=32`Q8*4cMCpO!!Wg=x=!hKKP@apv(tS%soS9Gg0iaW!9a;xE2X zU3@2e&6S55pWJ7@JAFL-`E;e5+^!h149cXUY4&D{`_)6<9~+O dqp@w#*MM|Z=a=dBm)2f%J)p6u%c1`NO#rAQWD)=X literal 0 HcmV?d00001 diff --git a/openjpa-jest/src/main/resources/org/apache/openjpa/persistence/jest/images/help.jpg b/openjpa-jest/src/main/resources/org/apache/openjpa/persistence/jest/images/help.jpg new file mode 100644 index 0000000000000000000000000000000000000000..b79358055cd3b5595e542dcf6d4a6fa96b30fc61 GIT binary patch literal 784 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 literal 0 HcmV?d00001 diff --git a/openjpa-jest/src/main/resources/org/apache/openjpa/persistence/jest/images/jest.jpg b/openjpa-jest/src/main/resources/org/apache/openjpa/persistence/jest/images/jest.jpg new file mode 100644 index 0000000000000000000000000000000000000000..3df6e9e40dc2dce1c8074dc5a9af527395293566 GIT binary patch literal 15565 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 literal 0 HcmV?d00001 diff --git a/openjpa-jest/src/main/resources/org/apache/openjpa/persistence/jest/images/monitor.jpg b/openjpa-jest/src/main/resources/org/apache/openjpa/persistence/jest/images/monitor.jpg new file mode 100644 index 0000000000000000000000000000000000000000..cf07c9913b4e033ed50a4bdba297db7cef1d47b6 GIT binary patch literal 1640 zcmbW!doKuH!ry1^EJla|IxV{lk37LUgfWL4y3Wn^U4l$7OFG}I|8G}OstOT;5ry535v{;Jh9`Z|W0IdhAZwT(%RPE(b?74KQQ=VNHF|yWPD=k5E{GWLP-TmHl&EgXcHW%6!1H>Zc%ozN-D=eVpQrr>Iyy;rdHIO{BOeAc#dt+2kAo0@KJ z?4V{o@ebsH%bL=1yWM=O zCH(~3aqws`K{#0Hka)YcH?U*{TymJxJH_dpZms5cbTmzhhbP6Ow4T*5y9t-GZO1~r z2j!0+om-rC+<7%5P87fH%PqEYkYF}vDzi5rAwZR?3p39*WZL9)+4}MFj~iAMQW_Bu zPE?KQEGj}k085<2olF)qs?zIeoQCw~IU{=cs59xaVp(Nh?CYWuPsZ>}SZRF4d5A7} zt_e?Ba3a4<8O%fOr2WxkwZ!WjmgAeu@5!)Q8sj(|)wchR?H!M7t%=1B{2^LyJjzhgx$>}-82ngmk-mGg~-sIFhK_u#Uuk($(SLb3-w<7G<{Lw4rpGXL3 zEUn~P(iql()ZbEIJ<5P}(Bqy3^szcp;_~Q1W&$gkGdQdnE~Hy{?K+_KxHtwfINP4; zYSCX3JYyBnv%8a5k*iwcACO5BGN-dHB?T*++4w!lH*0w0?fb5 zbnRmCZ!@ASx37cyT{9MqEMQZ&j$1e_6=(DXC{%D2KI~ z*+O6Hf@FjdsiKEf5Bpl$K65k@m;X})1x98>obp@YTHS-`4741G=kM>8&;36F+;#* zkK42ROc0>sj*UFG@BRq$iA&)8G2<#tO}k}%PcArWiu!XC_D1i?wGXni)a8-V>aT3f zOKf*=VyYzC98r^J*Sx2k^WwIsxEizw3E7;(M4_i%58>8*9pa%46>P3Pq`qAVSGjFI z3;{*vS*CmUy+VNDe64sTJpM458%T_?ymHb{BzF=4-L42wkJtAdWQ_|RWVbEdM)fn^ vs2Zj4eLpO&-NBj>c#3_*>DMr6&%HUklSb-$WPIn@BdgqF%B)qKA*AyU3)bQN literal 0 HcmV?d00001 diff --git a/openjpa-jest/src/main/resources/org/apache/openjpa/persistence/jest/images/properties.jpg b/openjpa-jest/src/main/resources/org/apache/openjpa/persistence/jest/images/properties.jpg new file mode 100644 index 0000000000000000000000000000000000000000..fb17ca0d4eabebee068278fc11996fac35ec678a GIT binary patch literal 1813 zcmbW$c{JPU8VB(Amp$l&G=>t?(ne|Fgq^RYZpcqTF z)x}n=Rl+;vJl!(OrTKc5@ z8oHW9qP8(v_bWpxm8wQEF*`yr)iiWGICI^_>X(hB4--~_OT zLmB`k2f^haNe58dxhE3(E#TjQU~mKyg~ni|q<03`vH%9b;V=Xoi9{fFrek;d03nCO zs~cLP_R{^(8W99aT;_d@rd3UwyxYK%mXUvCJXY#U1;u?z2M&_7$%m+i|6+W^;Soe#Va6dZEt@)br@bjh9@%F*wA1Gdwc-c8oVQ&7YZ_n-}~lT>kWVWp!&i*^>e_V1qE*Js1l8*L^*sDQ_!w{@8@7J_p zHI3Ysui=ya&Z=`Mr07)u`T~a*X`LD)qw4{JreNDPOan^RJ*qNkg+lE zGvG2Ua(~dA8hD5m1>LDAaj{V9JDzEm?Oy!uSG6E5+_&4`vz>H_qbXyUSli%#8PjhlhbJ0g_K@lJ1Xn3DJq3VWYGIa;Iz$ zI*{(PA_|KOIn7X~vvN#FQlV4sd;nYdM68d+rZLYsPQ%NqZNIUb*_-Z)1x^c+h}tZ^<(IbJx%Ayrhxi`jrRv24;X-kmeYphaAxIe;p_eDj z6B-5d2ka)E-UsU_(#xEdZV6DWvYW!CWZMnqzIekAbzG9w%3GuEu}s@H|`9@=5AFLW1@LnAlpe{>K}n4`DevqB3j1=~GauI4!jrGm9)o9c?7Aw^PM zTGcr#CS(5gjUPA`N(s*On@#d#(Vp#f>yNOe&(HQdCFUgKPlsAuWHQX!v$@l1u2MOU zqb%2_ji-mryXu1`|MByW=gTT3;MDieZe1-X%nN49>ZrUVQxdo+kTFN|PNybmrBhGZ zKprR0f};>E782@@^j{&+kF#PHPSg)C5Zq)oGAoL7`ZP61=RTLD4>C?1Y(7*}%$V1t zuP-y{o>lz|-q(6FvOjXVSH$F^8yLQ-rOG@kjoWZkFR@eH=AT5M*l&5oeT>oiTn#R+|4@(sze#u=S=H`Ty37mZ4N{6Xh^m$#nSEKD3>HjLX^YS}cN@nzilb|_V7Wa`Z6 z>=%8=#(oIej0xA{SWHSlW(%)Ug{{0^Ig&`ye>Xwrj?@TNPp#4w7tY$N_a37&;$7WW z)+(5oAWf$so1ugH7odDK-syIc1c)A&RNbzV5xCFgbsGF~e>$+KFWe{BVB*Z1=c(G& z_RI0RpYEZPYWwC>{A6MLar8_<3k_UdjEWo)ZEe1@BC=oR>s%A2bpYH&w4z1Hm2Jg- z{wH2b`oLl+y%uax+H3mC8omvC#}hL|thGkNNph{2b^$YBNCwQ$Khlz3bO20GM&%Z2Kv^@ dn_r*3rEK3m$5LPYj+^u0t|@^|15C-=e*qfwGdTbN literal 0 HcmV?d00001 diff --git a/openjpa-jest/src/main/resources/org/apache/openjpa/persistence/jest/images/query2.png b/openjpa-jest/src/main/resources/org/apache/openjpa/persistence/jest/images/query2.png new file mode 100644 index 0000000000000000000000000000000000000000..5801616b52fe82fa591ae5c5af47948b1d621462 GIT binary patch literal 9589 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 literal 0 HcmV?d00001 diff --git a/openjpa-jest/src/main/resources/org/apache/openjpa/persistence/jest/jest-instance.xsd b/openjpa-jest/src/main/resources/org/apache/openjpa/persistence/jest/jest-instance.xsd new file mode 100644 index 000000000..69a5c99bd --- /dev/null +++ b/openjpa-jest/src/main/resources/org/apache/openjpa/persistence/jest/jest-instance.xsd @@ -0,0 +1,166 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/openjpa-jest/src/main/resources/org/apache/openjpa/persistence/jest/jest.css b/openjpa-jest/src/main/resources/org/apache/openjpa/persistence/jest/jest.css new file mode 100644 index 000000000..159a0345d --- /dev/null +++ b/openjpa-jest/src/main/resources/org/apache/openjpa/persistence/jest/jest.css @@ -0,0 +1,248 @@ +/* + * 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. + */ + +/** ----------------------------------------------------------------------- + * Basic layout consists of a left menu section (class='menu')and two + * vertical sections for command specification (class='highlight') and + * JEST response (class='canvas') respectively. + * --------------------------------------------------------------------- */ + +/** ----------------------------------------------------------------------- + * div section containing the command menu. + * Lodged on the top-left corner + * -------------------------------------------------------------------- */ +.menu { + position:absolute; + left:0em; + top:80px; + width:10em; +} +/** ----------------------------------------------------------------------- + * Highlighted blocks appear relative to the menu. The same top position + * of the menu and shifted to left of the menu by the menu's width+padding. + * --------------------------------------------------------------------- */ +.highlight { + position:relative; + left:10em; + top:0em; +// background-color:#F9F9F9; + width:60em; + +// border:1px solid black; +// padding:0em 1em 1em 1em; // top, right, bottom, left + +} + +.logo { + font-size:2em; + font-weight:bold; + font-family: "Palatino Linotype, Times Roman" +} + +/** ----------------------------------------------------------------------- + * Canvas section appears relative to the menu and the highlighted section. + * The top position is relative to the highlighted section and left position + * is relative to the menu section. + * --------------------------------------------------------------------- */ +.canvas { + position:relative; + left:10em; + top:1em; + width:60em; + padding:0em 1em 0em 1em; // top, right, bottom, left +} + +.help { + float:right; + text-align:right; + margin-right:0.1em; + margin-top: -0.1em; +} +/** ----------------------------------------------------------------------- + * Visible/Invisible divisions + * --------------------------------------------------------------------- */ +.open { + display:block; +} +.close { + display:none; +} + +/** ----------------------------------------------------------------------- + * Hyperlinks + * --------------------------------------------------------------------- */ +.url { + color:blue; + font-size:1.1em; + font-family:"Courier New", Arial; + border:0px; + cursor:pointer; +} + +.url-invalid { + color:red; + font-size:0.9em; + font-family:"Courier New", Arial; +} + + + +/** ----------------------------------------------------------------------- + * XML Tag + * --------------------------------------------------------------------- */ +pre, code { + background-color: white; +} +.tag { + color:green; + font-family:"Courier New"; + font-weight:bold; + border:0px; +} +/** ----------------------------------------------------------------------- + * Data Table used for Tabular HTML + * --------------------------------------------------------------------- */ +table.data td th { + width : 70%; + border-collapse:collapse; + border:2px solid black; + margin: 10px 10px 10px 10px; +} +tr { + vertical-align:top; +} +th { + background-color:#444444; + color:white; + text-align:left; +} +caption { + background-color:#000000; + color:white; + font-size:1.2em; + font-weight:bold; + padding:5px 5px 5px 5px; +} +/** ----------------------------------------------------------------------- + * Alternate Table Row + * --------------------------------------------------------------------- */ +tr.even td { + background-color: #FFFFFF; color: black; + padding:2px 20px; + border:2px solid black; + vertical-align:top; +} +tr.odd td { + background-color: #EEEEEE; color: black; + padding:2px 20px; + border:2px solid black; + vertical-align:top; +} +td.mandatory { + font-weight:bold; + color:red; +} + +td input { + width : 99%; +} +td select { + width : 99%; +} + +/** ----------------------------------------------------------------------- + * Paragraph with smaller line breaks + * --------------------------------------------------------------------- */ +p.small { + line-height:60%; +} + +/** ----------------------------------------------------------------------- + * Error Page + * --------------------------------------------------------------------- */ +.error-header { + color:red; + font-size:2em; + font-weight:bold; +} +.error-message { + color:red; + font-size:1.2em; +} + +/* + * JPA styles +*/ +.id { + color:red; + font-weight:bold; +} +.enum { + color:magenta; + font-weight:bold; +} +.basic { + color:green; + font-weight:bold; +} +.one-to-one { + color:lightblue; + font-weight:bold; +} +.one-to-many { + color:darkblue; + font-weight:bold; +} + +.ref { + color : blue; +} +.null { + color : red; +} + +.delimiter { + color:lightgray; + font-weight:bold; +} +.attr-name { + color:gray; +} +.attr-value { + color:green; +} +.node-value { + font-weight:bold; +} +.entity { + font-weight:bold; +} +.metamodel { + font-weight:bold; +} + +.instances { + font-weight:bold; +} +.instance { + font-weight:bold; +} + + + \ No newline at end of file diff --git a/openjpa-jest/src/main/resources/org/apache/openjpa/persistence/jest/jest.html b/openjpa-jest/src/main/resources/org/apache/openjpa/persistence/jest/jest.html new file mode 100644 index 000000000..5196683f1 --- /dev/null +++ b/openjpa-jest/src/main/resources/org/apache/openjpa/persistence/jest/jest.html @@ -0,0 +1,329 @@ + + + + + + + + + + +JEST: REST on OpenJPA + + + + + + + + + + + + + + + +JEST logo + +
+ + + + + + + + + + + + + + + + + + +
+ + +
    +
  • Deployed as a HTTP servlet in any web or enterprise application module using +OpenJPA as its JPA persistence provider. +
  • + +
  • Completely metadata-driven and hence generic as opposed to specific to any +application domain. +
  • + +
  • Introduces a URI syntax. Interprets the HTTP request as a JPA resource based on +that syntax. The response is in XML or JSON format.
  • + +
  • Representational state for persistent instances is coherent. +The response always contains the closure of the persistent instances. +
  • + +
  • Connects to the persistent unit of an application in the same module. Or + instantiates its own persistence unit. +
  • +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + \ No newline at end of file diff --git a/openjpa-jest/src/main/resources/org/apache/openjpa/persistence/jest/jest.js b/openjpa-jest/src/main/resources/org/apache/openjpa/persistence/jest/jest.js new file mode 100644 index 000000000..af7f6420c --- /dev/null +++ b/openjpa-jest/src/main/resources/org/apache/openjpa/persistence/jest/jest.js @@ -0,0 +1,1146 @@ +/* + * 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. + */ + +dojo.require("dijit.form.Button"); +dojo.require("dijit.TitlePane"); +dojo.require("dojox.xml.parser"); +dojo.require("dijit.Dialog"); + + +/** ----------------------------------------------------------------------------------------- + * Navigation functions + * + * Available commands appear on left menu. Clicking on any command makes a corresponding + * highlighted section to appear at a fixed position. These command windows are identified + * and these identifiers are used to make only one of the section to be visible while + * hiding the rest. + * + * The jest.html document will use these same identifiers in their command section. + * ------------------------------------------------------------------------------------------ + * + * The identifiers of every element to be shown or hidden. + * --------------------------------------------------------------------------------------- */ + +var menuIds = new Array('home', 'deploy', 'find', 'query', 'domain', 'properties'); + +/** + * Opening a menu implies clearing the canvas, hiding the current menu item and making + * the given menu identifier visible. + * + * @param id menu section identifier + */ +function openMenu(/*HTML element id*/id) { + clearElement('canvas'); + switchId(id, menuIds); + document.location = "#top"; +} +/** + * Show the division identified by the given id, and hide all others + */ +function switchId(/*string*/id, /*string[]*/highlightedSections) { + for (var i=0; i < highlightedSections.length; i++){ + var section = document.getElementById(highlightedSections[i]); + if (section != null) { + section.style.display = 'none'; + } + } + var div = document.getElementById(id); + if (div != null) { + div.style.display = 'block'; + div.focus(); + } +} + +/** ----------------------------------------------------------------------------------------- + * Generic Command handling functions + * + * All available JEST commands are enumerated by their qualifiers and arguments. + * + * Specification of a JEST Command requires the following: + * a) a name + * b) zero or more Qualifiers. Qualifiers are not ordered. All Qualifiers are optional. + * c) zero or more Arguments. Arguments are ordered. Some Arguments can be mandatory. + * + * Command, Qualifier and Argument are 'objects' -- in a curious JavaScript sense. They + * are responsible to harvest their state value from the HTML element such as a input + * text box or a check box etc. The aim of harvesting the values is to construct a + * relative URI that can be passed to the server via a HTTP get request. + * + * jest.html attaches change event handlers to all input elements and the on change + * event handler updates the URI. + * + * The complexity is added because some commands may take variable number of arguments. + * Hence the input text boxes to enter arbitrary number of key-value pair can be created + * or removed by the user. + * + * A lot of implicit naming convention for the element identifiers are used in this + * script. These naming conventions are documented in jest.html. + * --------------------------------------------------------------------------------------- */ +var findCommand = new Command('find', + new Array( // + new Qualifier("plan", "plan", false), + new Qualifier("format", "format", false), + new Qualifier("ignoreCache", "ignoreCache", true) + ), + new Array( // Order of arguments is significant + new Argument("type", "type", true), + new Argument("pk", null, true) + )); + +var queryCommand = new Command('query', + new Array( // Qualifiers are not ordered + new Qualifier("plan", "plan", false), + new Qualifier("format", "format", false), + new Qualifier("single", "single", true), + new Qualifier("named", "named", true), + new Qualifier("ignoreCache", "ignoreCache", true) + ), + new Array( // Order of arguments is significant + new Argument("q", "q", true) + )); + +var domainCommand = new Command('domain', new Array(), new Array()); + +var propertiesCommand = new Command('properties', new Array(), new Array()); + +var commands = new Array(findCommand, queryCommand, domainCommand, propertiesCommand); + +/** ----------------------------------------------------------------------------------------- + * Creates a relative URI for the given commandName by reading the content of the given HTML + * element and its children. + * The URI is written to the HTML anchor element identified by {commandName} + '.uri'. + * + * @param commandName name of the command. All source HTML element are identified by this + * command name as prefix. + * --------------------------------------------------------------------------------------- */ +function toURI(commandName) { + var command = null; + switch (commandName) { + case 'find' : command = findCommand; break; + case 'query' : command = queryCommand; break; + case 'domain' : command = domainCommand; break; + case 'properties' : command = propertiesCommand; break; + } + if (command != null) + command.toURI(); +} + +/** ----------------------------------------------------------------------------------------- + * Adds columns to the given row for entering a variable argument key-value pair. + * A remove button is created as well to remove the row. + * A new row is created to invoke this function again to add another new row. + * + * @param rowIdPrefix a string such as query.vararg or find.vararg + * @param index a integer appended to the prefix to identify the row such as query.vararg.3 + * @param message the label on the new button + * --------------------------------------------------------------------------------------- */ +function addVarArgRow(rowIdPrefix, index, message) { + var rowId = rowIdPrefix + '.' + index; + var row = document.getElementById(rowId); + clearElement(rowId); + + // New input column for parameter name. Element id is rowId + '.key' + var argNameColumn = document.createElement('td'); + var argNameInput = document.createElement('input'); + argNameInput.setAttribute('type', 'text'); + argNameInput.setAttribute('id', rowId + '.key'); + argNameInput.setAttribute('onblur', 'javascript:toURI("' + rowIdPrefix.split('.')[0] + '");'); + argNameColumn.appendChild(argNameInput); + + // New input column for parameter value. Element id is rowId + '.value' + var argValueColumn = document.createElement('td'); + var argValueInput = document.createElement('input'); + argValueInput.setAttribute('type', 'text'); + argValueInput.setAttribute('id', rowId + '.value'); + argValueInput.setAttribute('onblur', 'javascript:toURI("' + rowIdPrefix.split('.')[0] + '");'); + argValueColumn.appendChild(argValueInput); + + // New column for remove button. Will remove this row. + var removeColumn = document.createElement('td'); + var removeColumnButton = document.createElement('button'); + removeColumnButton.innerHTML = 'Remove'; + removeColumnButton.setAttribute('onclick', 'javascript:removeVarArgRow("' + rowId + '");'); + removeColumn.appendChild(removeColumnButton); + + // Empty column as the first column + var emptyColumn = document.createElement('td'); + emptyColumn.appendChild(document.createTextNode("Key-Value pair")); + + // Add the empty column, two input columns and remove button to the current row + row.appendChild(emptyColumn); + row.appendChild(argNameColumn); + row.appendChild(argValueColumn); + row.appendChild(removeColumn); + + // create a new row with a single column to add another parameter. + // This new row looks similar to the original state of the modified column + var newIndex = index + 1; + var newRowId = rowIdPrefix + '.' + newIndex; + var newRow = document.createElement('tr'); + newRow.setAttribute('id', newRowId); + var newColumn = document.createElement('td'); + var addColumnButton = document.createElement('button'); + addColumnButton.innerHTML = message; + addColumnButton.setAttribute('onclick', 'javascript:addVarArgRow("' + rowIdPrefix + '",' + newIndex + ',"' + + message + '");'); + newColumn.appendChild(addColumnButton); + + newRow.appendChild(newColumn); + row.parentNode.appendChild(newRow); +} + +/** ----------------------------------------------------------------------------------------- + * Removes a variable argument row. + * The URI is updated. + * + * @param rowId the identifier of the row to be removed. The identifier follows the + * naming convention of the variable argument row i.e. {commandName}.varargs.{n} + * --------------------------------------------------------------------------------------- */ +function removeVarArgRow(rowId) { + var row = document.getElementById(rowId); + row.parentNode.removeChild(row); + toURI(rowId.split('.')[0]); +} + +/** ----------------------------------------------------------------------------------------- + * Definition of Command as a JavScript object. + * + * @param name name of the command. Used to identify the command, or identify input elements. + * @param qualifiers zero or more Qualifier objects + * @param arguments zero or more Argument objects + * + * + * --------------------------------------------------------------------------------------- */ +function Command(name, qualifiers, arguments) { + this.name = name; + this.qualifiers = qualifiers; + this.arguments = arguments; + this.toURI = Command_toURI; +} + +/** ----------------------------------------------------------------------------------------- + * Harvests the input HTML elements for a commands qualifiers and arguments and builds up + * a URI. + * Uses several naming convention that are documented in jest.html to identify the input + * elements. + * The naming of the function and its capitalization follows JavaScript convention for it + * to behave as a faux object method. + * + * @returns a string form of URI + * --------------------------------------------------------------------------------------- */ +function Command_toURI() { + var uri = this.name; // command name is same as URI name -- need not be + var iformat = 'xml'; // default response format + for (var i = 0; i < this.qualifiers.length; i++) { + var id = this.name + '.' + this.qualifiers[i].name; + var inode = document.getElementById(id); + var path = this.qualifiers[i].toURI(inode); + if (path != null) { + uri = uri.concat('/').concat(path); + if (this.qualifiers[i].key == 'format') { + iformat = getNodeValue(inode); + } + } + } + var args = ""; + var invalid = null; + for (var i = 0; i < this.arguments.length; i++) { + var id = this.name + '.' + this.arguments[i].name; + var inode = document.getElementById(id); + var arg = this.arguments[i].toURI(inode); + if (arg != null) { + args = args.concat(args.length == 0 ? '' : '&').concat(arg); + } else if (this.arguments[i].mandatory) { + invalid = 'Missing mandatory ' + this.arguments[i].name + ' argument'; + } + } + + // Variable argument processing + var children = document.getElementById(this.name + '.command').getElementsByTagName('tr'); + for (var i = 0; i < children.length; i++) { + var child = children[i]; + if (isVarArgRow(child, this.name)) { + var varargRow = child; + var pair = varargRow.getElementsByTagName('input'); + var key = getNodeValue(pair[0]); + var value = getNodeValue(pair[1]); + if (key != null && value != null) { + args = args.concat(args.length == 0 ? '' : '&').concat(key).concat('=').concat(value); + } + } + } + if (args.length > 0) { + uri = uri.concat('?').concat(args); + } + + // update the command URI element + console.log("New URI is " + uri); + var uriNode = document.getElementById(this.name + ".uri"); + var uriCtrl = document.getElementById(this.name + ".execute"); + if (invalid == null) { + uriNode.setAttribute('class', 'url'); + uriNode.innerHTML = uri; + var contentType = getContentTypeForCommand(this.name); + uriCtrl.setAttribute('onclick', + 'javascript:render("' + + uri + + '", "canvas"' + ',"' + + contentType + '","' + + iformat + '");'); + uriCtrl.style.display = 'inline'; + } else { + uriNode.setAttribute('class', 'url-invalid'); + uriNode.innerHTML = uri + ' (' + invalid + ')'; + uriCtrl.style.display = 'none'; + uriCtrl.removeAttribute('onclick'); + } + return uri; +} + +function getContentTypeForCommand(/*string*/ commandName) { + if (commandName == 'find' || commandName == 'query') return 'instances'; + if (commandName == 'domain') return 'domain'; + if (commandName == 'properties') return 'properties'; + +} + +/** ----------------------------------------------------------------------------------------- + * Definition of Qualifier JavaScript object. + * + * A qualifier decorates a Command. For example, a query command can be decorated with + * 'single' qualifier to return a single result. A 'plan' qualifier can decorate a find or + * query command to use a named fetch plan etc. + * A qualifier is encoded in the path segment of JEST URI followed by the + * command name e.g. /query/single or /find/plan=myFetchPlan etc. + * + * + * @param name a name when prefixed by the name of the command identifies the HTML element + * that carries the value of this qualifier. + * @param key the identifier for this qualifier used in the JEST URI + * @param isBoolean is this qualifier carries a boolean value? + * + * @returns {Qualifier} + * -------------------------------------------------------------------------------------- */ +function Qualifier(name, key, isBoolean) { + this.name = name; + this.key = key; + this.isBoolean = isBoolean; + this.toURI = Qualifier_toURI; +} + +/** ----------------------------------------------------------------------------------------- + * Generates a string for this qualifier to appear in the command URI. + * + * A qualifier is translated to a URI fragment as a key=value pair. A boolean + * qualifier is translated to a URI fragment only if the corresponding HTML + * element is checked. And even then, only the key is sufficient. + * + * @returns a string + * --------------------------------------------------------------------------------------- */ +function Qualifier_toURI(inode) { + var value = getNodeValue(inode); + if (isEmpty(value) || (this.isBoolean && !inode.checked)) { + return null; + } + if (this.isBoolean) { + return this.key + (value == 'true' ? '' : '=' + value); + } + return this.key + '=' + value; +} + +/** ----------------------------------------------------------------------------------------- + * Definition of Argument JavaScript object. + * + * An argument for a command. Some argument can be mandatory.
+ * Each argument is encoded as key=value pair in JEST URI in query parameters + * separated by '&' character. + * + * @param name a name when prefixed by the name of the command identifies the HTML element + * that carries the value of this argument. + * @param key the identifier for this argument used in the JEST URI + * @param mandatory is this argument mandatory? + * + * @returns {Argument} + * -------------------------------------------------------------------------------------- */ +function Argument(name, key, mandatory) { + this.name = name; + this.key = key; + this.mandatory = mandatory; + this.toURI = Argument_toURI; +} + +/** ----------------------------------------------------------------------------------------- + * Generates a string for this argument to appear in the command URI. + * + * An argument is translated to a URI fragment as a key=value pair. + * + * @returns a string + * --------------------------------------------------------------------------------------- */ +function Argument_toURI(inode) { + var value = getNodeValue(inode); + if (isEmpty(value)) + return null; + if (this.key == null) { + return value; + } else { + return this.key + '=' + value; + } +} + +/** ---------------------------------------------------------------------------------------- + * Utility functions + * ------------------------------------------------------------------------------------- */ +/** + * Trims a String. + */ +String.prototype.trim = function () { + return this.replace(/^\s*/, "").replace(/\s*$/, ""); +}; + +/** + * Affirms if the given string appears at the start of this string. + */ +String.prototype.startsWith = function(s) { + return this.indexOf(s, 0) == 0; +}; + +/** + * Affirms if the given string is null or zero-length or trimmed to zero-length. + * + * @param str a string to test for 'emptiness' + * @returns {Boolean} + */ +function isEmpty(str) { + return str == null || str.length == 0 || str.trim().length == 0; +} + +/** + * Gets the string value of the given node. + * + * @param inode a HTML element + * @returns null if given node is null or its value is an empty string. + * Otherwise, trimmed string. + */ +function getNodeValue(inode) { + if (inode == null) { + return null; + } + if (isEmpty(inode.value)) { + return null; + } else { + return inode.value.trim(); + } +} + +/** + * Affirms if the given HTML row element represents a variable argument row + * for the given commandName. + * @param row a HTML row element + * @param commandName name of a command + * + * @returns true if row identifer starts with commandName + '.vararg.' + */ +function isVarArgRow(row, commandName) { + return row != null && row.nodeName != '#text' + && row.hasAttribute("id") + && row.getAttribute("id").startsWith(commandName + '.vararg.'); +} + +/** + * Removes all children of the given element. + */ +function clearElement(/* HTML element id */ id) { + var element = dojo.byId(id); + if (element == null) return; + while (element.childNodes.length > 0) { + element.removeChild(element.firstChild); + } +} + +/** + * Prints and alerts with the given message string. + * @param message a warning message + */ +function warn(/*string*/message) { + console.log(message); + alert(message); +} + +/** ----------------------------------------------------------------------------------------- + * Rendering functions + * + * Several rendering functions to display server response in the canvas section. + * The server responds in following formats : + * 1a) XML + * 1b) JSON + * The response can be rendered in following display modes : + * 2a) raw XML text + * 2b) HTML table + * 2c) Dojo Widgets + * 2d) JSON as text + * The content can be one of the following + * 3a) instances from find() or query() command + * 3b) domain model from domain() command + * 3c) configuration properties from properties() command + * 3d) error stack trace + * + * Thus there are 2x4x4 = 32 possible combinations. However, response format for + * certain content type is fixed e.g. server always sends domain/properties/error + * stack trace in XML format. Moreover certain content-response format-display mode + * combinations are not supported. The following matrix describes the supported + * display modes for content-response format combinations. + * [y] : supported + * [x] : not supported + * n/a : not available + * -------------------------------------------------------------------------------- + * Response Content + * -------------------------------------------------------------------------------- + * instances domain properties error + * -------------------------------------------------------------------------------- + * XML [y] XML text [y] XML text [y] XML text [x] XML text + * [y] HTML [y] HTML [y] HTML [y] HTML + * [y] Dojo Widgets [y] Dojo Widgets [x] Dojo Widgets [x] Dojo Widgets + * [x] JSON [x] JSON [x] JSON [x] JSON + * + * JSON [x] XML text n/a n/a n/a + * [x] HTML Table n/a n/a [y] HTML + * [x] Dojo Widgets n/a n/a n/a + * [y] JSON n/a n/a n/a + * --------------------------------------------------------------------------------- + * The above matrix shows that there are 10 supported combinations. + * ------------------------------------------------------------------------------------- */ +var supportedResponseFormats = new Array('xml', 'json'); +var supportedContentTypes = new Array('instances', 'domain', 'properties', 'error'); +var renderingCombo = new Array( + +/*XML*/ new Array(new Array('xml', 'dojo', 'html'), // instances + new Array('xml', 'dojo', 'html'), // domain + new Array('xml','html'), // properties + new Array('html')), // error +/*JSON*/new Array(new Array('json'), // instances + new Array('xml', 'dojo', 'html'), // domain + new Array('xml', 'html'), // properties + new Array('html'))); // error +/** + * Gets the ordinal index of the given key in the given array + * @param array an array of enumerated strings. + * @param key a key to search for. + * + * @returns {Number} 0-based index of the key in the array. + */ +function getOrdinal(/*Array*/array, /*string*/key) { + for (var i = 0; i < array.length; i++) { + if (key == array[i]) return i; + } + console.log(key + " is not a valid enum in " + array); + return 0; +} +/** + * Gets ordinal number for enumerated response format. + * + * @param iformat response format. one of 'xml', 'json' + * + * @returns {Number} ordinal number 0 for 'xml', + */ +function getOrdinalResponseFormat(/*enum*/iformat) { + return getOrdinal(supportedResponseFormats, iformat); +} + +/** + * Gets ordinal number of the enumerated content types. + * @param contentType type of content. One of 'instances', 'domain', 'properties', 'error' + * @returns + */ +function getOrdinalContentType(/*enum*/contentType) { + return getOrdinal(supportedContentTypes, contentType); +} + +/** + * Gets the array of enumerated strings of display format for the given response format and content type. + * @param iformat + * @param contentType + * @returns + */ +function getSupportedDisplayModes(/*enum*/iformat,/*enum*/contentType) { + var displayModes = renderingCombo[getOrdinalResponseFormat(iformat)][getOrdinalContentType(contentType)]; + if (displayModes == null) { + warn("No display format for response format [" + iformat + "] and content type [" + contentType + "]"); + } + return displayModes; +} + +/** + * Render the response from the given URI on to the given HTML element identified by targetId. + * + * The URI is requested from server in an asynchronous call. Then the server response is rendered + * in all supported display format but only the given display format is made visible. + * + * @param uri the request URI + * @param targetId identifier of the HTML element that will display the data + * @param contentType type of the content, one of 'instances', 'domain', 'properties', 'error' + * @param iformat format of the server response, one of 'xml' or 'json' + * @param oformat format for display, one of 'xml', 'json', 'dojo', 'html' + * + * The combination of iformat-contentType-oformat must be compatiable as described in above matrix. + * + * @returns {Boolean} to prevent default event propagation + */ +function render(/* string */ uri, /* id */ targetId, /* enum */contentType, /* enum */iformat) { + var targetNode = dojo.byId(targetId); + clearElement(targetId); + + //The parameters to pass to xhrGet, the url, how to handle it, and the callbacks. + var xhrArgs = { + url: uri, + handleAs: (iformat == 'json' && contentType == 'instances') ? 'json' : 'xml', + preventCache: contentType == 'instances', + timeout : 1000, + load: function(data, ioargs) { + if (ioargs.xhr.status == 200) { // HTTP OK + var newDivs = null; + if (iformat == 'json') { + newDivs = renderJSONResponse(data, contentType); + } else { + newDivs = renderXMLResponse(data, contentType); + } + var displayModes = getSupportedDisplayModes(iformat, contentType); + targetNode.appendChild(createDisplayModeControl(displayModes)); + for (var i = 0; i < newDivs.length; i++) { + targetNode.appendChild(newDivs[i]); + } + } else { + var errorDiv = renderErrorFromXMLAsHTML(data, ioargs); + targetNode.appendChild(errorDiv); + } + }, + error: function(error, ioargs) { + var errorDiv = renderErrorFromXMLAsHTML(ioargs.xhr.responseXML, ioargs); + targetNode.appendChild(errorDiv); + } + }; + + //Call the asynchronous xhrGet + var deferred = dojo.xhrGet(xhrArgs); + return false; +} + +/** + * Creates a table with radio buttons for supported display modes. + * + * @param displayModes name of supported display modes. + * + * @returns an unattached HTML table + */ +function createDisplayModeControl(displayModes) { + var displayMode = document.createElement("table"); + displayMode.style.width = "100%"; + + var tr = document.createElement("tr"); + displayMode.appendChild(tr); + // append columns. 0-th an dfirst columns are descriptive texts. + var caption = document.createElement("th"); + caption.style.width = (100 - displayModes.length*12) + '%'; + caption.appendChild(document.createTextNode("JEST Response")); + tr.appendChild(caption); + + for (var i = 0; i < displayModes.length; i++) { + var mode = displayModes[i]; + var modeColumn = document.createElement("th"); + modeColumn.style.width = "12%"; + tr.appendChild(modeColumn); + + var radio = document.createElement("input"); + radio.setAttribute("type", "radio"); + radio.setAttribute("value", mode); + radio.setAttribute("name", "display.mode"); + if (i == 0) radio.setAttribute("checked", "checked"); + radio.setAttribute('onchange', createModeSwitchFunction(mode, displayModes)); + + modeColumn.appendChild(radio); + modeColumn.appendChild(document.createTextNode(mode.toUpperCase())); + } + + return displayMode; +} + +/** + * Creates a string for javascript function call to switch between display modes + * @param visible the visible display mode + * @param all available display modes + * @returns {String} a event handler function string + */ +function createModeSwitchFunction(/* string */ visible, /* string[] */ all) { + var array = ''; + for (var i = 0; i < all.length; i++) { + if (all[i] != visible) { + array = array + (array.length == 0 ? '' : ', ') + '"display.mode.' + all[i] + '"'; + } + } + return 'javascript:switchId("display.mode.' + visible+ '", [' + array + '])'; +} + +/** + * The big switch for rendering all content types received as XML DOM. + * Finds out the supported display format for given content type and renders each display format + * in separate divs. The div corresponding to the given display format is made visible, and others + * are hidden. None of the divs are attached to the main document. + * + * @param dom server response as a XML DOM document + * @param contentType enumerated content type. One of 'instances', 'domain', 'properties' or 'error' + * + * @returns an array of unattached divs only one of which is visible. + */ +function renderXMLResponse(/*XML DOM*/dom, /*enum*/contentType) { + var displayModes = getSupportedDisplayModes('xml', contentType); + var newDivs = new Array(displayModes.length); + for (var i = 0; i < displayModes.length; i++) { + var displayMode = displayModes[i]; + if (displayMode == 'xml') { + newDivs[i] = renderXMLasXML(dom); + } else if (contentType == 'instances') { + if (displayMode == 'html') { + newDivs[i] = renderInstancesFromXMLAsHTML(dom); + } else if (displayMode == 'dojo') { + newDivs[i] = renderInstancesFromXMLAsDojo(dom); + } + } else if (contentType == 'domain') { + if (displayMode == 'html') { + newDivs[i] = renderDomainFromXMLAsHTML(dom); + } else if (displayMode == 'dojo') { + newDivs[i] = renderDomainFromXMLAsDojo(dom); + } + } else if (contentType == 'properties') { + newDivs[i] = renderPropertiesFromXMLAsHTML(dom); + } + newDivs[i].style.display = (i == 0 ? 'block' : 'none'); + newDivs[i].setAttribute("id", "display.mode." + displayMode); + } + return newDivs; +} + +/** + * Renders the given instance data in the format of a XML DOM document into a set of Dojo widgets + * inside a div element. + * + * @param data the root node of a XML document containing instances data + * + * @returns an unattached div containing a set of dojo widgets + */ +function renderInstancesFromXMLAsDojo(/* XML DOM*/data) { + var target = document.createElement('div'); + var panels = new Array(); + dojo.query("instance", data).forEach(function(item, index) { + var panel = createInstanceDojoWidget(item); + panels[index] = panel; + }); + + // assign random location to each panel and add them to canvas + dojo.forEach(panels, function(item, index) { + var domNode = item.domNode; + domNode.style.width = "200px"; + domNode.style.position = "absolute"; + domNode.style.left = 100 + (index % 5)*300 + "px"; + domNode.style.top = 100 + Math.floor(index / 5)*200 +"px"; + target.appendChild(domNode); + }); + return target; +} + +/** + * Renders given DOM for metamodel as dojo widgets. + * + * @param data XML DOM for domain model. + * @returns a HTML div + */ +function renderDomainFromXMLAsDojo(/*XML DOM*/data) { + var target = document.createElement('div'); + var panels = new Array(); + dojo.query("entity, embeddable, mapped-superclass", data) + .forEach(function(item, index) { + var panel = createEntityTypeDojoWidget(item); + panels[index] = panel; + }); + + // assign random location to each panel and add them to canvas + dojo.forEach(panels, function(item, index) { + var domNode = item.domNode; + domNode.style.width = "200px"; + domNode.style.position = "absolute"; + domNode.style.left = 100 + (index % 5)*300 + "px"; + domNode.style.top = 100 + Math.floor(index / 5)*200 +"px"; + target.appendChild(domNode); + }); + return target; +} + +/** + * Renders given XML DOM for instances to HTML tables. + * *** NOT IMPLEMENTED + * + * @param data XML DOM for list of instances. All instanes may not belong to same entity. + * @returns a div with zero or more tables. + */ +function renderInstancesFromXMLAsHTML(/* XML DOM */data) { + return unimplemented("Rendering Instances as HTML is not implemented"); +} + +/** + * Renders given XML DOM for domain model to HTML tables. + * *** NOT IMPLEMENTED + * + * @param data XML DOM for list of domain model. + * @returns a div with zero or more tables. + */ +function renderDomainFromXMLAsHTML(/* XML DOM */data) { + return unimplemented("Rendering Domain as HTML is not implemented"); +} + +function unimplemented(/*string*/message) { + var empty = document.createElement('img'); + empty.setAttribute("src", "images/underconstruction.jpg"); + empty.setAttribute("alt", message); + return empty; +} + +/** + * Renders configration (name-value pairs) in a HTML table. + * + * @param data XML DOM for name-value pair properties. + * @returns a HTML table + */ +function renderPropertiesFromXMLAsHTML(/* XML DOM */data) { + var table = document.createElement("table"); + var caption = document.createElement("caption"); + caption.innerHTML = "Configuration Properties"; + table.appendChild(caption); + dojo.query("property", data) + .forEach(function(item, index) { + var row = document.createElement("tr"); + row.className = index%2 == 0 ? 'even' : 'odd'; + var key = document.createElement("td"); + var val = document.createElement("td"); + key.innerHTML = item.getAttribute("name"); + val.innerHTML = item.getAttribute("value"); + row.appendChild(key); + row.appendChild(val); + table.appendChild(row); + } + ); + return table; +} + +/** + * Renders error message in HTML + * + * @param data XML DOM containing error description + * + * @returns a div element with error details + */ +function renderErrorFromXMLAsHTML(/*response as XML DOM*/responseXML, ioargs) { + var div = document.createElement("div"); + var ecode = document.createElement("h3"); + var header = document.createElement("p"); + var msg = document.createElement("p"); + var trace = document.createElement("pre"); + ecode.setAttribute("class", "error-header"); + header.setAttribute("class", "error-message"); + msg.setAttribute("class", "error-message"); + + var serverError = responseXML.documentElement; + ecode.innerHTML = "HTTP Error " + ioargs.xhr.status; + header.innerHTML = dojox.xml.parser.textContent(serverError.getElementsByTagName("error-header").item(0)); + msg.innerHTML = dojox.xml.parser.textContent(serverError.getElementsByTagName("error-message").item(0)); + trace.innerHTML = dojox.xml.parser.textContent(serverError.getElementsByTagName("stacktrace").item(0)); + div.appendChild(ecode); + div.appendChild(header); + div.appendChild(msg); + div.appendChild(trace); + + return div; +} + +/** + * Creates a dojo Title Pane from a DOM instance node. The pane has the instance + * id as its title. The content is a table with name and value of each attribute + * in each row. Multi-cardinality values are in separate row without the attribute + * name repeated except the first row. + * + * @param instanceNode an XML instance node + * + * @returns dojo widget for a single instance + */ +function createInstanceDojoWidget(/*XML node*/instanceNode) { + var instanceTable = document.createElement("table"); + dojo.query('id, basic, enum, version', instanceNode) + .forEach(function(item) { + var attrRow = document.createElement("tr"); + var nameColumn = document.createElement("td"); + var valueColumn = document.createElement("td"); + nameColumn.className = item.nodeName.toLowerCase(); /* May be cross-browser trouble */ + nameColumn.innerHTML = item.getAttribute("name"); + valueColumn.innerHTML = dojox.xml.parser.textContent(item); + attrRow.appendChild(nameColumn); + attrRow.appendChild(valueColumn); + instanceTable.appendChild(attrRow); + } + ); + dojo.query('one-to-one, many-to-one', instanceNode) + .forEach(function(item) { + var attrRow = document.createElement("tr"); + var nameColumn = document.createElement("td"); + var valueColumn = document.createElement("td"); + nameColumn.className = item.nodeName.toLowerCase(); /* May be cross-browser trouble */ + nameColumn.innerHTML = item.getAttribute("name"); + dojo.query('ref, null', instanceNode) + .forEach(function(ref) { + valueColumn.innerHTML = ref.nodeName == 'null' ? 'null' : ref.getAttribute("id"); + valueColumn.className = ref.nodeName.toLowerCase(); + attrRow.appendChild(nameColumn); + attrRow.appendChild(valueColumn); + instanceTable.appendChild(attrRow); + }); + }); + dojo.query('one-to-many, many-to-many', instanceNode).forEach(function(item) { + var attrRow = document.createElement("tr"); + var nameColumn = document.createElement("td"); + var valueColumn = document.createElement("td"); + nameColumn.className = item.nodeName.toLowerCase(); /* May be cross-browser trouble */ + nameColumn.innerHTML = item.getAttribute("name"); + var refs = item.getElementsByTagName("ref"); + for (var i = 0; i < refs.length; i++) { + if (i == 0) { + valueColumn.innerHTML = refs[i].getAttribute("id"); + valueColumn.className = refs[i].nodeName.toLowerCase(); + attrRow.appendChild(nameColumn); + attrRow.appendChild(valueColumn); + instanceTable.appendChild(attrRow); + } else { + var attrRow = document.createElement("tr"); + var nameColumn = document.createElement("td"); // empty column + var valueColumn = document.createElement("td"); + valueColumn.className = refs[i].nodeName.toLowerCase(); + valueColumn.innerHTML = refs[i].getAttribute("id"); + attrRow.appendChild(nameColumn); + attrRow.appendChild(valueColumn); + instanceTable.appendChild(attrRow); + } + } + } + ); + + var pane = new dijit.TitlePane({title:instanceNode.getAttribute("id"),content:instanceTable}); + return pane; +} + + +/** + * Creates a dojo Title Pane from a DOM instance node. The pane has the instance + * id as its title. The content is name and value of each attribute in separate + * line. + * + * @param node + * an instance node + * @returns + */ +function createEntityTypeDojoWidget(node) { + var entityTable = document.createElement("table"); + dojo.query('id, basic, enum, version, one-to-one, many-to-one, one-to-many, many-to-many', node) + .forEach(function(item) { + var attr = document.createElement("tr"); + var name = document.createElement("td"); + name.className = item.nodeName.toLowerCase(); /* May be cross-browser trouble */ + var type = item.getAttribute("type"); + name.innerHTML = type; + if (name.className == 'one-to-many') { + name.innerHTML = type + '<' + item.getAttribute("member-type") + '>'; + } + var value = document.createElement("td"); + value.innerHTML = dojox.xml.parser.textContent(item); + attr.appendChild(name); + attr.appendChild(value); + entityTable.appendChild(attr); + } + ); + + var pane = new dijit.TitlePane({title:node.getAttribute("name"), content: entityTable}); + return pane; +} + +/** + * Generic routine to render the given XML Document as a raw but indented text in to an unattached div section. + * + * @param dom a XML DOM + * + * @returns an unattached div section + */ +function renderXMLasXML(/*XML DOM*/dom) { + var newDiv = document.createElement('div'); + print(dom.documentElement, newDiv, 0); + return newDiv; +} + +/** + * Renders a XML DOM node as a new child of the given HTML node. + * + * CSS styles used: + * node-value : The value of a text node + * attr-name : The name of an attribute + * attr-value : The value of an attribute + * delimiter : symbols such = " < \ > used in visual XML + * the XML element name : e.g. A tag will be decorated with .metamodel CSS style + * + */ +function print(/* XML node */xnode, /* HTML node*/ hnode, /*int*/counter) { + if (xnode.nodeName == '#text') { + addTextNode(hnode, xnode.nodeValue, "node-value"); + return; + } + var root = document.createElement('div'); + root.style.position = 'relative'; + root.style.left = '2em'; + addRoot(xnode, hnode, root, ++counter); + + var attrs = xnode.attributes; + if (attrs) { + for (var i = 0; i < attrs.length; i++) { + var attr = attrs[i]; + addTextNode(root, ' ' + attr.nodeName, "attr-name"); + addDelimiter(root, '='); + addTextNode(root, '"' + attr.nodeValue + '"', "attr-value"); + } + addDelimiter(root, '>'); + } + var children = xnode.childNodes; + if (children) { + for (var i = 0; i < children.length; i++) { + print(children[i], root, ++counter); + } + } + addDelimiter(root, ''); + return; +} + +/** + * Adds the given delimiter text with CSS style 'delimiter' to the given parent node + * @param parentNode + * @param text + */ +function addDelimiter(/* HTML node*/ parentNode, /* Delimiter String*/ delim) { + addTextNode(parentNode, delim, 'delimiter'); +} +/** + * Adds a node of given className to the given parentNode with the given text. + * + * @param parentNode the parent node to which new text is added as a element. + * @param text text to be added to the new element + * @param className class of the new element + * @returns the new node + */ +function addTextNode(/* HTML node*/parentNode, /* String */text, /* String*/className) { + if (isEmpty(text)) return null; + newNode = document.createElement('span'); + if (className) { + newNode.className = className; + } + if (text) { + newNode.appendChild(document.createTextNode(text)); + } + if (parentNode) { + parentNode.appendChild(newNode); + } + return newNode; +} +function isTextNode(/* XML node */ xnode) { + return xnode == null || xnode.nodeName == '#text'; +} + +function isTogglable(/* XML node */ xnode) { + if (xnode == null) return false; + if (isTextNode(xnode)) return false; + var children = xnode.childNodes; + if (children == null || children.length == 0) return false; + if (children.length == 1 && isTextNode(children[0])) return false; + return true; +} + +function addRoot(xnode, hnode, root, counter) { + if (isTogglable(xnode)) { + hnode.appendChild(document.createElement('br')); + var ctrl = addTextNode(hnode, '-'); + root.setAttribute("id", counter); + var moniker = '<' + xnode.nodeName + '>...'; + ctrl.setAttribute("onclick", 'javascript:toggle(this, "' + moniker + '", "' + counter + '");'); + } + addDelimiter(root, '<'); + addTextNode(root, xnode.nodeName, xnode.nodeName); + hnode.appendChild(root); + +} + +function toggle(/* HTML node */ctrl, /* id */ moniker, /* id */ targetId) { + var visible = ctrl.innerHTML == '-'; + ctrl.innerHTML = visible ? '+' + moniker : '-'; + var target = document.getElementById(targetId); + if (visible) { + target.style.display = "none"; + } else { + target.style.display = "inline"; + } +} + + +/** + * Renders server response of JSON objects. + * Server sends always an array of JSON objects. + * @param json an array of hopefully non-empty array of JSON objects + * @param contentType type of content. Currently only instances are JSONized. + * @returns an array of div with a single member + */ +function renderJSONResponse(/*JSON[]*/json, /*enum*/contentType) { + var text = dojo.toJson(json, true); + var div = document.createElement("div"); + var pre = document.createElement("pre"); + pre.innerHTML = text; + div.appendChild(pre); + return [div]; // an array of a single div +} + +/** + * Help related utilities. + */ + +var helpDialog; + +function createDialog() { + if (helpDialog == null) { + helpDialog = new dijit.Dialog({style: "width: 400px; height:300px;overflow:auto"}); + } + return helpDialog; +} + +function showHelp(title, href) { + var dialog = createDialog(); + dialog.set("title", title); + dialog.set("href", href); + dialog.show(); +} + + diff --git a/openjpa-jest/src/main/resources/org/apache/openjpa/persistence/jest/localizer.properties b/openjpa-jest/src/main/resources/org/apache/openjpa/persistence/jest/localizer.properties new file mode 100644 index 000000000..e8cee1470 --- /dev/null +++ b/openjpa-jest/src/main/resources/org/apache/openjpa/persistence/jest/localizer.properties @@ -0,0 +1,60 @@ +# 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. + +no-persistence-unit-param: Missing persistence.unit parameter. JEST Servlet must be \ + configured with a parameter named persistence.unit in <init-param> clause \ + of <servlet> declaration in WEB-INF/web.xml descriptor. +servlet-init: JEST Servlet is initialized for "{0}" persistence unit. +servlet-not-init: JEST Servlet can not find "{0}" persistence unit during servlet initialization. \ + JEST Servlet will try to locate the unit when a request is to be served. + +no-persistence-unit: JEST can not locate the component using persistence unit {0}. This can happen \ + for several reasons: \ +
    the component is not initialized.
\ +
    the component and JEST servlet do not belong to the same deployment module
\ +
    the component did not configure the persistence unit for pooling. To enable pooling, \ + create the persistence unit with configuration property openjpa.EntityManagerFactoryPool=true.
    \ + The property must be passed to Persistence.createEntityManagerFactory(String unit, Map props) \ + with the second Map argument and not via META-INF/persistence.xml
. + + +resource-not-found: Can not locate resource {0}.
This can happen for wrong URI syntax. See \ +JEST URI Help page for correct syntax. + +query-execution-error: Error executing query "{0}". See stacktrace for details. +parse-invalid-qualifier: {0} command does not recognize "{1}" as a qualifier. Valid qualifiers are {2}. +parse-missing-mandatory-argument: {0} command must have "{1}" argument. Available arguments are {2}. +parse-less-argument: {0} command must have at least {2} argument. Available arguments are {1}. +# ---------------------------------------------------------------------- +# Format related error +# ---------------------------------------------------------------------- +format-xml-null-parent: A null XML parent element encountered during serialization +format-xml-null-doc: Given parent element is not part of XML document +format-xml-null-closure: Set of visited instances can not be null for serialization +format-not-supported: format {0} in command {1} is not registered. Available formats are {2}. + +properties-caption: Configuration of {0} Persistence Unit +entity-not-found: Resource of type {0} with identifier {1} is not found. +bad-uri: Can not reconstruct URI from original URL {0} + + +find-title: JEST find +find-desc: JEST find command may return more than one result. Why? +query-title: JEST query +query-desc: JEST query command may return more than the directly selected result. Why? +domain-command: JEST domain +domain-desc: JEST domain command prints the persistent domain model