PageRenderTime 30ms CodeModel.GetById 16ms RepoModel.GetById 0ms app.codeStats 0ms

/src/scala/org/orbeon/oxf/fr/mongdb/MongoDBPersistence.scala

https://github.com/andreaskc/orbeon-forms
Scala | 305 lines | 204 code | 37 blank | 64 comment | 5 complexity | 763b1a74155ca8459fde8b6c1e0ca35d MD5 | raw file
  1. /**
  2. * Copyright (C) 2011 Orbeon, Inc.
  3. *
  4. * This program is free software; you can redistribute it and/or modify it under the terms of the
  5. * GNU Lesser General Public License as published by the Free Software Foundation; either version
  6. * 2.1 of the License, or (at your option) any later version.
  7. *
  8. * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
  9. * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
  10. * See the GNU Lesser General Public License for more details.
  11. *
  12. * The full text of the license is available at http://www.gnu.org/copyleft/lesser.html
  13. */
  14. package org.orbeon.oxf.fr.mongdb
  15. import com.mongodb.casbah.Imports._
  16. import javax.servlet.http.{HttpServletResponse, HttpServletRequest, HttpServlet}
  17. import java.io.{OutputStreamWriter, InputStream}
  18. import javax.servlet.ServletException
  19. import org.orbeon.oxf.util.ScalaUtils._
  20. import org.orbeon.saxon.value.DateTimeValue
  21. import java.util.Date
  22. import com.mongodb.casbah.gridfs.GridFS
  23. import xml.NodeSeq._
  24. import xml.{NodeSeq, Node, XML}
  25. /*!# Experimental: Form Runner MongoDB persistence layer implementation.
  26. Supports:
  27. * storing and retrieving XML data
  28. * storing and retrieving data attachments
  29. * searching: all, keyword, and structured
  30. Known issues:
  31. * does not support storing and retrieving forms
  32. * does not support storing and retrieving form attachments
  33. * reusing connection to MongoDB (opens/closes at each request)
  34. */
  35. class MongoDBPersistence extends HttpServlet {
  36. /*! MongoDB keys used for custom Orbeon fields */
  37. private val DOCUMENT_ID_KEY = "_orbeon_document_id"
  38. private val LAST_UPDATE_KEY = "_orbeon_last_update"
  39. private val XML_KEY = "_orbeon_xml"
  40. private val KEYWORDS_KEY = "_orbeon_keywords"
  41. private val FORM_KEY = "_orbeon_form"
  42. private val XHTML_KEY = "_orbeon_xhtml"
  43. /*! Regexp matching a form data path */
  44. private val DataPath = """.*/crud/([^/]+)/([^/]+)/data/([^/]+)/([^/]+)""".r
  45. /*! Regexp matching a form definition path */
  46. private val FormPath = """.*/crud/([^/]+)/([^/]+)/form/([^/]+)""".r
  47. /*! Regexp matching a search path */
  48. private val SearchPath = """.*/search/([^/]+)/([^/]+)/?""".r
  49. /*!## Servlet PUT entry point
  50. Store form data, form definition, or attachment.
  51. */
  52. override def doPut(req: HttpServletRequest, resp: HttpServletResponse) {
  53. req.getPathInfo match {
  54. case DataPath(app, form, documentId, "data.xml") =>
  55. storeDocument(app, form, documentId, req.getInputStream)
  56. case DataPath(app, form, documentId, attachmentName) =>
  57. storeAttachment(app, form, documentId, attachmentName, req)
  58. case FormPath(app, form, "form.xhtml") =>
  59. storeForm(app, form, req.getInputStream)
  60. case FormPath(app, form, attachmentName) =>
  61. storeFormAttachment(app, form, attachmentName, req)
  62. case _ => throw new ServletException
  63. }
  64. }
  65. /*!## Servlet GET entry point
  66. Retrieve form data, form definition, or attachment.
  67. */
  68. override def doGet(req: HttpServletRequest, resp: HttpServletResponse) {
  69. req.getPathInfo match {
  70. case DataPath(app, form, documentId, "data.xml") =>
  71. retrieveDocument(app, form, documentId, resp)
  72. case DataPath(app, form, documentId, attachmentName) =>
  73. retrieveAttachment(app, form, documentId, attachmentName, resp)
  74. case FormPath(app, form, "form.xhtml") =>
  75. retrieveForm(app, form, resp)
  76. case FormPath(app, form, attachmentName) =>
  77. retrieveFormAttachment(app, form, attachmentName, resp)
  78. case _ => throw new ServletException
  79. }
  80. }
  81. /*!## Servlet POST entry point
  82. Perform a search based on an incoming search specification in XML, and return search results in XML.
  83. */
  84. override def doPost(req: HttpServletRequest, resp: HttpServletResponse) {
  85. req.getPathInfo match {
  86. case SearchPath(app, form) =>
  87. search(app, form, req, resp)
  88. case _ => throw new ServletException
  89. }
  90. }
  91. /*!## Store an XML document */
  92. def storeDocument(app: String, form: String, documentId: String, inputStream: InputStream) {
  93. // Use MongoDB ObjectID as that can serve as timestamp for creation
  94. val builder = MongoDBObject.newBuilder
  95. builder += (DOCUMENT_ID_KEY -> documentId)
  96. builder += (LAST_UPDATE_KEY -> DateTimeValue.getCurrentDateTime(null).getCanonicalLexicalRepresentation.toString)
  97. // Create one entry per leaf, XML doc and keywords
  98. val root = XML.load(inputStream)
  99. val keywords = collection.mutable.Set[String]()
  100. root \\ "_" filter (_ \ "_" isEmpty) map { e =>
  101. val text = e.text
  102. builder += (e.label -> text)
  103. if (text.trim.nonEmpty)
  104. keywords ++= text.split("""\s+""")
  105. }
  106. builder += (XML_KEY -> root.toString)
  107. builder += (KEYWORDS_KEY -> keywords.toArray)
  108. // Create or update
  109. withCollection(app, form) {
  110. _.update(MongoDBObject(DOCUMENT_ID_KEY -> documentId), builder.result, upsert = true, multi = false)
  111. }
  112. }
  113. /*!## Retrieve an XML document */
  114. def retrieveDocument(app: String, form: String, documentId: String, resp: HttpServletResponse) {
  115. withCollection(app, form) { coll =>
  116. coll.findOne(MongoDBObject(DOCUMENT_ID_KEY -> documentId)) match {
  117. case Some(result: DBObject) =>
  118. result(XML_KEY) match {
  119. case xml: String =>
  120. resp.setContentType("application/xml")
  121. useAndClose(new OutputStreamWriter(resp.getOutputStream)) {
  122. osw => osw.write(xml)
  123. }
  124. case _ => resp.setStatus(404)
  125. }
  126. case _ => resp.setStatus(404)
  127. }
  128. }
  129. }
  130. /*!## Store an attachment */
  131. def storeAttachment(app: String, form: String, documentId: String, name: String, req: HttpServletRequest) {
  132. withFS {
  133. _(req.getInputStream) { fh =>
  134. fh.filename = Seq(app, form, documentId, name) mkString "/"
  135. fh.contentType = Option(req.getContentType) getOrElse "application/octet-stream"
  136. }
  137. }
  138. }
  139. /*!## Retrieve an attachment */
  140. def retrieveAttachment(app: String, form: String, documentId: String, name: String, resp: HttpServletResponse) {
  141. withFS {
  142. _.findOne(Seq(app, form, documentId, name) mkString "/") match {
  143. case Some(dbFile) =>
  144. resp.setContentType(dbFile.contentType)
  145. copyStream(dbFile.inputStream, resp.getOutputStream)
  146. case _ => resp.setStatus(404)
  147. }
  148. }
  149. }
  150. /*!## Store an XHTML document */
  151. def storeForm(app: String, form: String, inputStream: InputStream) {
  152. // Use MongoDB ObjectID as that can serve as timestamp for creation
  153. val builder = MongoDBObject.newBuilder
  154. builder += (FORM_KEY -> form)
  155. builder += (LAST_UPDATE_KEY -> DateTimeValue.getCurrentDateTime(null).getCanonicalLexicalRepresentation.toString)
  156. //Load XML Doc, add to builder for storage
  157. val root = XML.load(inputStream)
  158. builder += (XHTML_KEY -> root.toString)
  159. // Create or update
  160. withCollection(app, form) {
  161. _.update(MongoDBObject(FORM_KEY -> form), builder.result, upsert = true, multi = false)
  162. }
  163. }
  164. /*!## Retrieve an XHTML document */
  165. def retrieveForm(app: String, form: String, resp: HttpServletResponse) {
  166. withCollection(app, form) { coll =>
  167. coll.findOne(MongoDBObject(FORM_KEY -> form)) match {
  168. case Some(result: DBObject) =>
  169. result(XHTML_KEY) match {
  170. case xml: String =>
  171. resp.setContentType("application/xhtml+xml")
  172. useAndClose(new OutputStreamWriter(resp.getOutputStream)) {
  173. osw => osw.write(xml)
  174. }
  175. case _ => resp.setStatus(404)
  176. }
  177. case _ => resp.setStatus(404)
  178. }
  179. }
  180. }
  181. def storeFormAttachment(app: String, form: String, name: String, req: HttpServletRequest) {
  182. withFS {
  183. _(req.getInputStream) { fh =>
  184. fh.filename = Seq(app, form, "form", name) mkString "/"
  185. fh.contentType = Option(req.getContentType) getOrElse "application/octet-stream"
  186. }
  187. }
  188. }
  189. /*!## Retrieve an attachment */
  190. def retrieveFormAttachment(app: String, form: String, name: String, resp: HttpServletResponse) {
  191. withFS {
  192. _.findOne(Seq(app, form, "form", name) mkString "/") match {
  193. case Some(dbFile) =>
  194. resp.setContentType(dbFile.contentType)
  195. copyStream(dbFile.inputStream, resp.getOutputStream)
  196. case _ => resp.setStatus(404)
  197. }
  198. }
  199. }
  200. /*!## Perform a search */
  201. def search(app: String, form: String, req: HttpServletRequest, resp: HttpServletResponse) {
  202. def elemValue(n: Node) = n.text.trim
  203. def attValue(n: Node, name: String) = n.attribute(name).get.text
  204. def intValue(n: NodeSeq) = n.head.text.toInt
  205. // Extract search parameters
  206. val root = XML.load(req.getInputStream)
  207. val pageSize = intValue(root \ "page-size")
  208. val pageNumber = intValue(root \ "page-number")
  209. val searchElem = root \ "query"
  210. val fullQuery = elemValue(searchElem.head)
  211. withCollection(app, form) { coll =>
  212. // Create search iterator depending on type of search
  213. val find =
  214. if (searchElem forall (elemValue(_) isEmpty)) {
  215. // Return all
  216. coll.find
  217. } else if (fullQuery.nonEmpty) {
  218. // Keyword search
  219. coll.find(MongoDBObject(KEYWORDS_KEY -> fullQuery))
  220. } else {
  221. // Structured search: gather all non-empty <query name="$NAME">$VALUE</query>
  222. coll.find(MongoDBObject(searchElem.tail filter (elemValue(_) nonEmpty) map (e => (attValue(e, "name") -> elemValue(e))) toList))
  223. }
  224. // Run search with sorting/paging
  225. val resultsToSkip = (pageNumber - 1) * pageSize
  226. val rows = find sort MongoDBObject(LAST_UPDATE_KEY -> -1) skip resultsToSkip limit pageSize
  227. // Create and output result
  228. val result =
  229. <documents total={rows.size.toString} page-size={pageSize.toString} page-number={pageNumber.toString}>{
  230. rows map { o =>
  231. val created = DateTimeValue.fromJavaDate(new Date(o.get("_id").asInstanceOf[ObjectId].getTime)).getCanonicalLexicalRepresentation.toString
  232. <document created={created} last-modified={o.get(LAST_UPDATE_KEY).toString} name={o.get(DOCUMENT_ID_KEY).toString}>
  233. <details>{
  234. searchElem.tail map { e =>
  235. <detail>{o.get(attValue(e, "name"))}</detail>
  236. }
  237. }</details>
  238. </document>
  239. }
  240. }</documents>
  241. resp.setContentType("application/xml")
  242. useAndClose(new OutputStreamWriter(resp.getOutputStream)) {
  243. osw => osw.write(result.toString)
  244. }
  245. }
  246. }
  247. def withDB[T](t: (MongoDB) => T) {
  248. val mongoConnection = MongoConnection()
  249. try {
  250. t(mongoConnection("orbeon"))
  251. } finally {
  252. mongoConnection.close()
  253. }
  254. }
  255. def withCollection[T](app: String, form: String)(t: (MongoCollection) => T) {
  256. withDB { db => t(db(app + '.' + form))}
  257. }
  258. def withFS[T](t: (GridFS) => T) {
  259. withDB { db => t(GridFS(db))}
  260. }
  261. }