From cdbdd25d490e48e4e24f2f2e9b9c04d2933fdbfe Mon Sep 17 00:00:00 2001 From: devjiel Date: Thu, 18 Aug 2016 16:09:57 +0200 Subject: [PATCH 01/10] Ajout du forwarding WSDL Ajout de la prise en compte des requetes MTOM MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Montee/Ajustement version des librairies pour démarrage (certains dépots n'étaient plus présent) --- app/assets/javascripts/soapower.js | 1 - app/controllers/Soap.scala | 99 +++++++++++++++++++++++++----- app/models/Client.scala | 36 ++++++++++- build.sbt | 6 +- conf/routes | 1 + project/build.properties | 2 +- 6 files changed, 122 insertions(+), 23 deletions(-) diff --git a/app/assets/javascripts/soapower.js b/app/assets/javascripts/soapower.js index 70f1e8a..c9e9a1e 100644 --- a/app/assets/javascripts/soapower.js +++ b/app/assets/javascripts/soapower.js @@ -65,4 +65,3 @@ spApp.run(['$location', '$rootScope', function ($location, $rootScope) { $rootScope.$broadcast("showGroupsFilter", false, "Soapower"); }); }]); - diff --git a/app/controllers/Soap.scala b/app/controllers/Soap.scala index b6bca69..a72cc2d 100644 --- a/app/controllers/Soap.scala +++ b/app/controllers/Soap.scala @@ -1,29 +1,79 @@ package controllers +import java.net.{InetAddress, NetworkInterface} + +import models.RequestData._ +import models._ +import org.jboss.netty.handler.codec.http.HttpMethod import play.Logger import play.api.http.HeaderNames -import play.api.mvc._ -import play.api.libs.iteratee._ -import models._ import play.api.libs.concurrent.Execution.Implicits.defaultContext +import play.api.mvc._ +import reactivemongo.bson.{BSONDocument, BSONObjectID} + import scala.concurrent.duration._ import scala.concurrent.{Await, Future} -import reactivemongo.bson.{BSONDocument, BSONObjectID} -import models.RequestData._ -import org.jboss.netty.handler.codec.http.HttpMethod + object Soap extends Controller { - def index(environment: String, localTarget: String) = Action.async(parse.xml) { + def getWSDL(environment: String, localTarget: String) = Action.async { implicit request => - Logger.debug("Request on environment:" + environment + " localTarget:" + localTarget) - val requestContentType = request.contentType.get - val sender = request.remoteAddress - val content = request.body.toString() - val headers = request.headers.toSimpleMap - forwardRequest(environment, localTarget, sender, content, headers, requestContentType) + + Logger.debug("Request WSDL on environment:" + environment + " localTarget:" + localTarget) + + + val service = Service.findByLocalTargetAndEnvironmentName(Service.SOAP, localTarget, environment, HttpMethod.POST) + service.map(svc => + if (svc.isDefined && svc.get != null) { + // Appel du serivce distant pour recuperer le wsdl + val client = new Client(svc.get, environment, null, null, null, null, null) + client.sendGetWSDLRequest + var wsdl : String = client.wsdlResponse.body + + // Modification de la location pour ne pas recontacter directement la cible mais passer par Soapower + wsdl = rewriteTargetLocation(wsdl, environment, localTarget) + + Ok(wsdl) + } else { + val err = "environment " + environment + " with localTarget " + localTarget + " unknown" + Logger.error(err) + BadRequest(err) + } + ) } + + def index(environment: String, localTarget: String) = Action.async(parse.anyContent) { + implicit request => + + Logger.debug("Request on environment:" + environment + " localTarget:" + localTarget) + + // On determine si le flux est du XML ou du MTOM + var content : String = null + if (request.body.isInstanceOf[AnyContentAsXml]) { + + content = request.body.asInstanceOf[AnyContentAsXml].xml.toString + + } else { + + // Cas MTOM + val buffer: RawBuffer = request.body.asRaw.get + content = buffer.asBytes() match { + case Some(x) => new String(x, "UTF-8") + case None => scala.io.Source.fromFile(buffer.asFile).mkString + } + + } + + val requestContentType = request.contentType.get + val sender = request.remoteAddress + val headers = request.headers.toSimpleMap + + forwardRequest(environment, localTarget, sender, content, headers, requestContentType) + + } + /** * Automatically detect new services. If the given parameters interpolates an existing service, then nothing is created otherwise a new service is created. * The new service takes the given parameters and theses defaults parameters : @@ -108,7 +158,8 @@ object Soap extends Controller { /** * Replay a given request. - * @param requestId request to replay, content is post by user + * + * @param requestId request to replay, content is post by user */ def replay(requestId: String) = Action.async(parse.xml) { implicit request => @@ -136,7 +187,7 @@ object Soap extends Controller { private def forwardRequest(environmentName: String, localTarget: String, sender: String, content: String, headers: Map[String, String], requestContentType: String): Future[Result] = { val service = Service.findByLocalTargetAndEnvironmentName(Service.SOAP, localTarget, environmentName, HttpMethod.POST) - service.map(svc => + service.map(svc => if (svc.isDefined && svc.get != null) { val client = new Client(svc.get, environmentName, sender, content, headers, Service.SOAP, requestContentType) if (svc.get.useMockGroup && svc.get.mockGroupId.isDefined) { @@ -168,4 +219,22 @@ object Soap extends Controller { } ) } + + /** + * Permet de redefinir la l'hote a appeler lorsque l'on passe par Soapower, sinon le client appelle directement la cible. + * + * @param wsdl La wsdl à modifier + * @param environment + * @param localTarget + * + * @return la wsdl avec la location pointant vers le service soapower correspondant + */ + private def rewriteTargetLocation(wsdl: String, environment: String, localTarget: String) = { + + val hostname = InetAddress.getLocalHost.getHostName + val port = System.getProperty("http.port", "9000") + + wsdl.replaceAll("location=\".+\"", "location=\"http://" + hostname + ":" + port + "/soap/" + environment + "/" + localTarget +"\"") + + } } \ No newline at end of file diff --git a/app/models/Client.scala b/app/models/Client.scala index 86c38f9..99b98eb 100755 --- a/app/models/Client.scala +++ b/app/models/Client.scala @@ -1,13 +1,12 @@ package models -import com.ning.http.client.{AsyncHttpClient, FluentCaseInsensitiveStringsMap} +import com.ning.http.client.{FluentCaseInsensitiveStringsMap} import com.ning.http.client.providers.netty.NettyResponse import scala.concurrent.Future import scala.concurrent.Await import scala.concurrent.duration._ import play.api.libs.ws._ import play.api.libs.concurrent.Execution.Implicits.defaultContext -import play.api.libs.concurrent._ import play.core.utils.CaseInsensitiveOrdered import play.Logger import collection.immutable.TreeMap @@ -16,6 +15,7 @@ import java.io.StringWriter import java.io.PrintWriter import play.api.Play.current import org.jboss.netty.handler.codec.http.HttpMethod +import java.net.{HttpURLConnection, URL} object Client { private val DEFAULT_NO_SOAPACTION = "Soapower_NoSoapAction" @@ -73,6 +73,8 @@ class Client(pService: Service, environmentName: String, sender: String, content var response: ClientResponse = null + var wsdlResponse: WSDLClientResponse = null + private var futureResponse: Future[WSResponse] = null private var requestTimeInMillis: Long = -1 @@ -100,6 +102,7 @@ class Client(pService: Service, environmentName: String, sender: String, content /** * Send a request to a REST service + * * @param correctUrl * @param query */ @@ -159,11 +162,12 @@ class Client(pService: Service, environmentName: String, sender: String, content * Send a request to a SOAP service */ def sendSoapRequestAndWaitForResponse() { + if (Logger.isDebugEnabled) { Logger.debug("RemoteTarget (soap)" + service.remoteTarget) } - requestTimeInMillis = System.currentTimeMillis + requestTimeInMillis = System.currentTimeMillis // prepare request var wsRequestHolder = WS.url(service.remoteTarget).withRequestTimeout(service.timeoutms.toInt) @@ -188,6 +192,25 @@ class Client(pService: Service, environmentName: String, sender: String, content saveData(content) } + /** + * Permet d'envoyer une demande de WSDL. + * Pour le moment ce type de trame n'est pas persiste + */ + def sendGetWSDLRequest() { + + requestTimeInMillis = System.currentTimeMillis + + val connection = new URL(service.remoteTarget + "?wsdl").openConnection.asInstanceOf[HttpURLConnection] + connection.setConnectTimeout(5000) + connection.setRequestMethod("GET") + val inputStream = connection.getInputStream + val content: String = scala.io.Source.fromInputStream(inputStream).mkString + if (inputStream != null) inputStream.close + + wsdlResponse = new WSDLClientResponse(content, (System.currentTimeMillis - requestTimeInMillis)) + + } + private def waitForResponse(headers: Map[String, String]) = { try { val wsResponse: WSResponse = Await.result(futureResponse, service.timeoutms.millis * 1000000) @@ -230,6 +253,7 @@ class Client(pService: Service, environmentName: String, sender: String, content /** * If content is null or empty, return "[null or empty]" + * * @param content a string * @return [null or empty] or the content if not null (or empty!) */ @@ -313,4 +337,10 @@ class ClientResponse(wsResponse: WSResponse = null, val responseTimeInMillis: Lo } } +class WSDLClientResponse(wsdl : String = null, val responseTimeInMillis: Long) { + + var body: String = if (wsdl != null) wsdl else "" + +} + diff --git a/build.sbt b/build.sbt index 206e121..b69a8d3 100644 --- a/build.sbt +++ b/build.sbt @@ -6,15 +6,15 @@ version := "2.1.5" lazy val root = (project in file(".")).enablePlugins(PlayScala,SbtWeb) -scalaVersion := "2.11.1" +scalaVersion := "2.11.2" resolvers += "Sonatype Snapshots" at "https://oss.sonatype.org/content/repositories/snapshots/" libraryDependencies ++= Seq( cache, ws, - "org.reactivemongo" %% "reactivemongo" % "0.11.0-SNAPSHOT", - "org.reactivemongo" %% "play2-reactivemongo" % "0.10.5.akka23-SNAPSHOT" + "org.reactivemongo" %% "reactivemongo" % "0.10.5.0.akka23", + "org.reactivemongo" %% "play2-reactivemongo" % "0.10.5.0.akka23" ) mappings in Universal <++= baseDirectory map { dir => (dir / "soapowerctl.sh").*** --- dir x relativeTo(dir) } diff --git a/conf/routes b/conf/routes index 3510e30..bf45766 100644 --- a/conf/routes +++ b/conf/routes @@ -28,6 +28,7 @@ PUT /autorest/:group/:environment/*remoteTarget POST /autorest/:group/:environment/*remoteTarget controllers.Rest.autoIndex(group, environment, remoteTarget) # Soap +GET /soap/:environment/*localTarget controllers.Soap.getWSDL(environment, localTarget) POST /soap/:environment/*localTarget controllers.Soap.index(environment, localTarget) POST /autosoap/:group/:environment/*remoteTarget controllers.Soap.autoIndex(group, environment, remoteTarget) POST /replay/soap/:id controllers.Soap.replay(id:String) diff --git a/project/build.properties b/project/build.properties index be6c454..304ec92 100644 --- a/project/build.properties +++ b/project/build.properties @@ -1 +1 @@ -sbt.version=0.13.5 +sbt.version=0.13.7 \ No newline at end of file From deaaf71b406444d88c96e70ac2b112c21444d30e Mon Sep 17 00:00:00 2001 From: devjiel Date: Fri, 19 Aug 2016 11:03:01 +0200 Subject: [PATCH 02/10] Les mocks peuvent desormais retourner un WSDL --- .../app/controllers/admin/mocksController.js | 1 + app/controllers/Soap.scala | 36 ++++++++++++------- app/models/Mock.scala | 7 ++-- public/partials/admin/mocks/detail.html | 9 +++++ 4 files changed, 39 insertions(+), 14 deletions(-) diff --git a/app/assets/javascripts/app/controllers/admin/mocksController.js b/app/assets/javascripts/app/controllers/admin/mocksController.js index bd5fff6..1e9f624 100644 --- a/app/assets/javascripts/app/controllers/admin/mocksController.js +++ b/app/assets/javascripts/app/controllers/admin/mocksController.js @@ -91,6 +91,7 @@ function MockNewCtrl($scope, $location, $routeParams, Mock) { $scope.mock.timeoutms = 0; $scope.mock.httpStatus = 200; $scope.mock.response = ""; + $scope.mock.wsdl = ""; $scope.mock.criteria = "*"; $scope.mock.mockGroupName = $routeParams.mockGroupName; diff --git a/app/controllers/Soap.scala b/app/controllers/Soap.scala index a72cc2d..8fb5b78 100644 --- a/app/controllers/Soap.scala +++ b/app/controllers/Soap.scala @@ -26,15 +26,24 @@ object Soap extends Controller { val service = Service.findByLocalTargetAndEnvironmentName(Service.SOAP, localTarget, environment, HttpMethod.POST) service.map(svc => if (svc.isDefined && svc.get != null) { - // Appel du serivce distant pour recuperer le wsdl - val client = new Client(svc.get, environment, null, null, null, null, null) - client.sendGetWSDLRequest - var wsdl : String = client.wsdlResponse.body + if (svc.get.useMockGroup && svc.get.mockGroupId.isDefined) { - // Modification de la location pour ne pas recontacter directement la cible mais passer par Soapower - wsdl = rewriteTargetLocation(wsdl, environment, localTarget) + val fmock = Mock.findByMockGroupAndContent(BSONObjectID(svc.get.mockGroupId.get), "") + val mock = Await.result(fmock, 1.second) - Ok(wsdl) + + Ok(mock.wsdl) + } else { + // Call remote service to get WSDL back + val client = new Client(svc.get, environment, null, null, null, null, null) + client.sendGetWSDLRequest + var wsdl: String = client.wsdlResponse.body + + // Modify the location to call Soapower instead of remote target in the next call + wsdl = rewriteTargetLocation(wsdl, environment, localTarget) + + Ok(wsdl) + } } else { val err = "environment " + environment + " with localTarget " + localTarget + " unknown" Logger.error(err) @@ -197,7 +206,11 @@ object Soap extends Controller { val sr = new Results.Status(mock.httpStatus).apply(mock.response.getBytes()) .withHeaders("ProxyVia" -> "soapower") .withHeaders(UtilConvert.headersFromString(mock.httpHeaders).toArray: _*) - .as(XML) + + // Define Content-Type only if not defined in the Mock + if (!mock.httpHeaders.contains("Content-Type")) { + sr.as(XML) + } val timeoutFuture = play.api.libs.concurrent.Promise.timeout(sr, mock.timeoutms.milliseconds) Await.result(timeoutFuture, 10.second) // 10 seconds (10000 ms) is the maximum allowed. @@ -221,13 +234,12 @@ object Soap extends Controller { } /** - * Permet de redefinir la l'hote a appeler lorsque l'on passe par Soapower, sinon le client appelle directement la cible. + * Redefine host to call when get WSDL back, to avoid calling the remote target directly * - * @param wsdl La wsdl à modifier + * @param wsdl wsdl to modify * @param environment * @param localTarget - * - * @return la wsdl avec la location pointant vers le service soapower correspondant + * @return wsdl with location on Soapower */ private def rewriteTargetLocation(wsdl: String, environment: String, localTarget: String) = { diff --git a/app/models/Mock.scala b/app/models/Mock.scala index b1fd0af..33566b7 100644 --- a/app/models/Mock.scala +++ b/app/models/Mock.scala @@ -20,6 +20,7 @@ case class Mock(_id: Option[BSONObjectID], httpHeaders: String, criteria: String, response: String, + wsdl: String, mockGroupName: Option[String]) { def this(mockDoc: BSONDocument, mockGroupName: Option[String]) = this( @@ -31,6 +32,7 @@ case class Mock(_id: Option[BSONObjectID], mockDoc.getAs[String]("httpHeaders").get, mockDoc.getAs[String]("criteria").get, mockDoc.getAs[String]("response").get, + mockDoc.getAs[String]("wsdl").get, mockGroupName) } @@ -73,7 +75,8 @@ object Mock { "httpStatus" -> BSONInteger(mock.httpStatus), "httpHeaders" -> BSONString(mock.httpHeaders), "criteria" -> BSONString(mock.criteria), - "response" -> BSONString(mock.response)) + "response" -> BSONString(mock.response), + "wsdl" -> BSONString(mock.wsdl)) } /** @@ -151,7 +154,7 @@ object Mock { mocksGroup.map( omocks => { val noMockFound: Mock = new Mock(Some(BSONObjectID.generate), "mockNotFoundName", "mockNotFoundDescription", -1, - 0, HttpStatus.SC_INTERNAL_SERVER_ERROR.toString, "noCriteria", "no mock found in soapower", + 0, HttpStatus.SC_INTERNAL_SERVER_ERROR.toString, "noCriteria", "no mock found in soapower", "no mock found", Some("Error getting Mock with mockGroupId " + mockGroupId) ) diff --git a/public/partials/admin/mocks/detail.html b/public/partials/admin/mocks/detail.html index e0e0600..2ada603 100644 --- a/public/partials/admin/mocks/detail.html +++ b/public/partials/admin/mocks/detail.html @@ -83,6 +83,15 @@

{{ title }}

+
+ +
+ +
+
Some client need to get WSDL before calling the service. +
+
+
Cancel From a637d89c3937c04b6c38048172e95ae679676756 Mon Sep 17 00:00:00 2001 From: Cornic Mathieu Date: Fri, 4 Nov 2016 14:15:36 +0100 Subject: [PATCH 03/10] Rolling log files 100mB --- conf/prod-logger.xml | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/conf/prod-logger.xml b/conf/prod-logger.xml index 42ea475..da9840f 100644 --- a/conf/prod-logger.xml +++ b/conf/prod-logger.xml @@ -2,8 +2,19 @@ - + ${application.home}/logs/application.log + + + application.%i.log.zip + 1 + 5 + + + + 100MB + + %date - [%level] - from %logger in %thread %n%message%n%xException%n @@ -16,7 +27,7 @@ - + From e595fd5e1aecbd4a02e21f973d5dc6ca5ab02d3a Mon Sep 17 00:00:00 2001 From: Cornic Mathieu Date: Fri, 4 Nov 2016 17:18:49 +0100 Subject: [PATCH 04/10] fix #4 : response body is displayed even when application/json or xml is used with charset/url --- app/controllers/Search.scala | 156 +++++++++--------- public/index.html | 2 +- .../highlightjs/angular-highlightjs.min.js | 8 +- .../javascripts/highlightjs/highlight.pack.js | 3 +- public/partials/visualize/view.html | 2 +- 5 files changed, 90 insertions(+), 81 deletions(-) diff --git a/app/controllers/Search.scala b/app/controllers/Search.scala index 8aad44d..0e95590 100644 --- a/app/controllers/Search.scala +++ b/app/controllers/Search.scala @@ -5,13 +5,17 @@ import play.api.libs.json._ import models._ import models.UtilDate._ import play.api.libs.iteratee.Enumerator -import play.api.http.{HeaderNames} +import play.api.http.HeaderNames + import scala.xml.PrettyPrinter import org.xml.sax.SAXParseException import java.net.URLDecoder -import scala.concurrent.{Await, Future, ExecutionContext} +import java.util.regex.Pattern + +import scala.concurrent.{Await, ExecutionContext, Future} import ExecutionContext.Implicits.global -import reactivemongo.bson.{BSONObjectID, BSONString, BSONDocumentWriter, BSONDocument} +import reactivemongo.bson.{BSONDocument, BSONDocumentWriter, BSONObjectID, BSONString} + import scala.util.parsing.json.JSONObject import scala.concurrent.duration._ import com.fasterxml.jackson.core.JsonParseException @@ -151,36 +155,39 @@ object Search extends Controller { */ def getResponse(id: String) = Action.async { RequestData.loadResponse(id).map { - tuple => tuple match { - case Some(doc: BSONDocument) => { - doc.getAs[String]("contentType").get match { - case "application/json" => - var content = doc.getAs[String]("response").get - try { - val content = Json.parse(doc.getAs[String]("response").get) - Ok(Json.toJson(content)); - } - catch { - case e: JsonParseException => - Ok(content) - } - case "application/xml" | "text/xml" => { - var content = doc.getAs[String]("response").get - try { - content = new PrettyPrinter(250, 4).format(scala.xml.XML.loadString(content)) - } catch { - case e: SAXParseException => + case Some(doc: BSONDocument) => { - } - Ok(content) + val patternJson = ".*(application/json).*".r + val patternXml = ".*(application/xml).*".r + val patternTextXml = ".*(text/xml).*".r + + doc.getAs[String]("contentType").get match { + case patternJson(_) => + val content = doc.getAs[String]("response").get + try { + val content = Json.parse(doc.getAs[String]("response").get) + Ok(Json.toJson(content)); } - case _ => - Ok(doc.getAs[String]("response").get) + catch { + case e: JsonParseException => + Ok(content) + } + case patternXml(_) | patternTextXml(_) => { + var content = doc.getAs[String]("response").get + try { + content = new PrettyPrinter(250, 4).format(scala.xml.XML.loadString(content)) + } catch { + case e: SAXParseException => + + } + Ok(content) } + case _ => + Ok(doc.getAs[String]("response").get) } - case None => - NotFound("The response does not exist") } + case None => + NotFound("The response does not exist") } } @@ -191,7 +198,7 @@ object Search extends Controller { */ def downloadResponse(id: String) = Action.async { val future = RequestData.loadResponse(id) - downloadInCorrectFormat(future, id, false) + downloadInCorrectFormat(future, id, isRequest = false) } @@ -216,61 +223,62 @@ object Search extends Controller { var contentInCorrectFormat = "" future.map { - tuple => tuple match { - case Some(doc: BSONDocument) => { - val contentType = doc.getAs[String]("contentType").get - // doc.getAs[String]("response") - val content = doc.getAs[String](keyContent).get + case Some(doc: BSONDocument) => { + val contentType = doc.getAs[String]("contentType").get + // doc.getAs[String]("response") + val content = doc.getAs[String](keyContent).get + val patternJson = ".*(application/json).*".r + val patternXml = ".*(application/xml).*".r + val patternTextXml = ".*(text/xml).*".r - contentType match { - case "application/xml" | "text/xml" => { - try { - contentInCorrectFormat = new PrettyPrinter(250, 4).format(scala.xml.XML.loadString(content)) - filename += ".xml" - } catch { - case e: SAXParseException => contentInCorrectFormat = content - filename += ".txt" - } + contentType match { + case patternXml(_) | patternTextXml(_) => { + try { + contentInCorrectFormat = new PrettyPrinter(250, 4).format(scala.xml.XML.loadString(content)) + filename += ".xml" + } catch { + case e: SAXParseException => contentInCorrectFormat = content + filename += ".txt" + } - var result = Result( - header = ResponseHeader(play.api.http.Status.OK), - body = Enumerator(contentInCorrectFormat.getBytes)) + var result = Result( + header = ResponseHeader(play.api.http.Status.OK), + body = Enumerator(contentInCorrectFormat.getBytes)) - result = result.withHeaders((HeaderNames.CONTENT_DISPOSITION, "attachment; filename=" + filename)) - result.as(XML) + result = result.withHeaders((HeaderNames.CONTENT_DISPOSITION, "attachment; filename=" + filename)) + result.as(XML) - } + } - case "application/json" => { - try { - contentInCorrectFormat = Json.parse(content).toString - filename += ".json" - } - catch { - case e: Exception => - contentInCorrectFormat = content - filename += ".txt" - } - var result = Result( - header = ResponseHeader(play.api.http.Status.OK), - body = Enumerator(contentInCorrectFormat.getBytes)) - result = result.withHeaders((HeaderNames.CONTENT_DISPOSITION, "attachment; filename=" + filename)) - result.as(JSON) + case patternJson(_) => { + try { + contentInCorrectFormat = Json.parse(content).toString + filename += ".json" } - case _ => { - filename += ".txt" - var result = Result( - header = ResponseHeader(play.api.http.Status.OK), - body = Enumerator(content.getBytes)) - result = result.withHeaders((HeaderNames.CONTENT_DISPOSITION, "attachment; filename=" + filename)) - result.as(TEXT) + catch { + case e: Exception => + contentInCorrectFormat = content + filename += ".txt" } - + var result = Result( + header = ResponseHeader(play.api.http.Status.OK), + body = Enumerator(contentInCorrectFormat.getBytes)) + result = result.withHeaders((HeaderNames.CONTENT_DISPOSITION, "attachment; filename=" + filename)) + result.as(JSON) + } + case _ => { + filename += ".txt" + var result = Result( + header = ResponseHeader(play.api.http.Status.OK), + body = Enumerator(content.getBytes)) + result = result.withHeaders((HeaderNames.CONTENT_DISPOSITION, "attachment; filename=" + filename)) + result.as(TEXT) } + } - case _ => - NotFound("The request does not exist") } + case _ => + NotFound("The request does not exist") } } diff --git a/public/index.html b/public/index.html index cdac0c6..e192e1b 100644 --- a/public/index.html +++ b/public/index.html @@ -16,6 +16,7 @@ + @@ -126,6 +127,5 @@ - diff --git a/public/javascripts/highlightjs/angular-highlightjs.min.js b/public/javascripts/highlightjs/angular-highlightjs.min.js index 8e29812..76168f5 100644 --- a/public/javascripts/highlightjs/angular-highlightjs.min.js +++ b/public/javascripts/highlightjs/angular-highlightjs.min.js @@ -1,6 +1,6 @@ /*! angular-highlightjs -version: 0.3.0 -build date: 2014-05-24 -author: Robin Fan +version: 0.6.2 +build date: 2016-08-19 +author: Chih-Hsuan Fan https://github.com/pc035860/angular-highlightjs.git */ -angular.module("hljs",[]).provider("hljsService",function(){var a={};return{setOptions:function(b){angular.extend(a,b)},getOptions:function(){return angular.copy(a)},$get:["$window",function(b){return(b.hljs.configure||angular.noop)(a),b.hljs}]}}).factory("hljsCache",["$cacheFactory",function(a){return a("hljsCache")}]).controller("HljsCtrl",["hljsCache","hljsService",function(a,b){var c=this,d=null,e=null,f=null,g=null;c.init=function(a){d=a},c.setLanguage=function(a){e=a,f&&c.highlight(f)},c.highlightCallback=function(a){g=a},c.highlight=function(h){if(d){var i,j;f=h,e?(j=c._cacheKey(e,f),i=a.get(j),i||(i=b.highlight(e,b.fixMarkup(f),!0),a.put(j,i))):(j=c._cacheKey(f),i=a.get(j),i||(i=b.highlightAuto(b.fixMarkup(f)),a.put(j,i))),d.html(i.value),d.addClass(i.language),null!==g&&angular.isFunction(g)&&g()}},c.clear=function(){d&&(f=null,d.text(""))},c.release=function(){d=null},c._cacheKey=function(){var a=Array.prototype.slice.call(arguments),b="!angular-highlightjs!";return a.join(b)}}]).directive("hljs",["$compile","$parse",function(a,b){return{restrict:"EA",controller:"HljsCtrl",compile:function(c){var d=c[0].innerHTML.replace(/^(\r\n|\r|\n)/m,"");return c.html('
'),function(c,e,f,g){var h;angular.isDefined(f.compile)&&(h=b(f.compile)),g.init(e.find("code")),f.onhighlight&&g.highlightCallback(function(){c.$eval(f.onhighlight)}),d&&(g.highlight(d),h&&h(c)&&a(e.find("code").contents())(c)),c.$on("$destroy",function(){g.release()})}}}}]).directive("language",[function(){return{require:"hljs",restrict:"A",link:function(a,b,c,d){c.$observe("language",function(a){angular.isDefined(a)&&d.setLanguage(a)})}}}]).directive("source",["$compile","$parse",function(a,b){return{require:"hljs",restrict:"A",link:function(c,d,e,f){var g;angular.isDefined(e.compile)&&(g=b(e.compile)),c.$watch(e.source,function(b){b?(f.highlight(b),g&&g(c)&&a(d.find("code").contents())(c)):f.clear()})}}}]).directive("include",["$http","$templateCache","$q","$compile","$parse",function(a,b,c,d,e){return{require:"hljs",restrict:"A",compile:function(f,g){var h=g.include;return function(f,g,i,j){var k,l=0;angular.isDefined(i.compile)&&(k=e(i.compile)),f.$watch(h,function(e){var h=++l;if(e&&angular.isString(e)){var i,m;i=b.get(e),i||(m=c.defer(),a.get(e,{cache:b,transformResponse:function(a){return a}}).success(function(a){h===l&&m.resolve(a)}).error(function(){h===l&&j.clear(),m.resolve()}),i=m.promise),c.when(i).then(function(a){a&&(angular.isArray(a)?a=a[1]:angular.isObject(a)&&(a=a.data),a=a.replace(/^(\r\n|\r|\n)/m,""),j.highlight(a),k&&k(f)&&d(g.find("code").contents())(f))})}else j.clear()})}}}}]); \ No newline at end of file +!function(a,b){"object"==typeof exports||"object"==typeof module&&module.exports?module.exports=b(require("angular"),require("highlight.js")):"function"==typeof define&&define.amd?define(["angular","hljs"],b):a.returnExports=b(a.angular,a.hljs)}(this,function(a,b){function c(b){return function(c){switch(c){case"escape":return a.isDefined(b.hljsEscape)?b.hljsEscape:b.escape;case"no-escape":return a.isDefined(b.hljsNoEscape)?b.hljsNoEscape:b.noEscape;case"onhighlight":return a.isDefined(b.hljsOnhighlight)?b.hljsOnhighlight:b.onhighlight}}}function d(b){var c=!0;return a.forEach(["source","include"],function(a){b[a]&&(c=!1)}),c}var e=a.module("hljs",[]);e.provider("hljsService",function(){var c={};return{setOptions:function(b){a.extend(c,b)},getOptions:function(){return a.copy(c)},$get:function(){return(b.configure||a.noop)(c),b}}}),e.factory("hljsCache",["$cacheFactory",function(a){return a("hljsCache")}]),e.controller("HljsCtrl",["hljsCache","hljsService","$interpolate","$window",function(b,c,d,e){function f(a,b,c){var d;return function(){var f=this,g=arguments,h=function(){d=null,c||a.apply(f,g)},i=c&&!d;e.clearTimeout(d),d=e.setTimeout(h,b),i&&a.apply(f,g)}}function g(a,b){var c=b?"\\\\$&":"\\$&";return a.replace(/[-[\]{}()*+?.,\\^$|#\s]/g,c)}function h(a){for(var b,c=[],d=new RegExp(q,"g"),e="",f=0;null!==(b=d.exec(a));)e+=a.substring(f,b.index)+r,f=b.index+b[0].length,c.push(b[0]);return e+=a.substr(f),{code:e,tokens:c}}function i(a,b){for(var c,d=new RegExp(r,"g"),e="",f=0;null!==(c=d.exec(a));)e+=a.substring(f,c.index)+b.shift(),f=c.index+c[0].length;return e+=a.substr(f)}var j=this,k=null,l=null,m=null,n=!1,o=null,p=null,q=g(d.startSymbol())+"((.|\\s)+?)"+g(d.endSymbol()),r="∫";j.init=function(a){k=a},j.setInterpolateScope=function(a){n=a,m&&j.highlight(m)},j.setLanguage=function(a){l=a,m&&j.highlight(m)},j.highlightCallback=function(a){p=a},j._highlight=function(e){if(k){var f,g,q;if(m=e,n&&(q=h(e),e=q.code),l?(g=j._cacheKey(l,!!n,e),f=b.get(g),f||(f=c.highlight(l,c.fixMarkup(e),!0),b.put(g,f))):(g=j._cacheKey(!!n,e),f=b.get(g),f||(f=c.highlightAuto(c.fixMarkup(e)),b.put(g,f))),e=f.value,n){(o||a.noop)(),q&&(e=i(e,q.tokens));var r=d(e);o=n.$watch(r,function(a,b){a!==b&&k.html(a)}),n.$apply(),k.html(r(n))}else k.html(e);k.addClass(f.language),null!==p&&a.isFunction(p)&&p()}},j.highlight=f(j._highlight,17),j.clear=function(){k&&(m=null,k.text(""))},j.release=function(){k=null,n=null,(o||a.noop)(),o=null},j._cacheKey=function(){var a=Array.prototype.slice.call(arguments),b="!angular-highlightjs!";return a.join(b)}}]);var f,g,h,i,j;return f=["$parse",function(b){return{restrict:"EA",controller:"HljsCtrl",compile:function(e){var f=e[0].innerHTML.replace(/^(\r\n|\r|\n)/m,""),g=e[0].textContent.replace(/^(\r\n|\r|\n)/m,"");return e.html('
'),function(e,h,i,j){var k,l=c(i);if(a.isDefined(l("escape"))?k=b(l("escape")):a.isDefined(l("no-escape"))&&(k=b("false")),j.init(h.find("code")),l("onhighlight")&&j.highlightCallback(function(){e.$eval(l("onhighlight"))}),(f||g)&&d(i)){var m;m=k&&!k(e)?g:f,j.highlight(m)}e.$on("$destroy",function(){j.release()})}}}}],h=function(b){return function(){return{require:"?hljs",restrict:"A",link:function(c,d,e,f){f&&e.$observe(b,function(b){a.isDefined(b)&&f.setLanguage(b)})}}}},g=function(a){return function(){return{require:"?hljs",restrict:"A",link:function(b,c,d,e){e&&b.$watch(d[a],function(a,c){(a||a!==c)&&e.setInterpolateScope(a?b:null)})}}}},i=function(a){return function(){return{require:"?hljs",restrict:"A",link:function(b,c,d,e){e&&b.$watch(d[a],function(a){a?e.highlight(a):e.clear()})}}}},j=function(b){return["$http","$templateCache","$q",function(c,d,e){return{require:"?hljs",restrict:"A",compile:function(f,g){var h=g[b];return function(b,f,g,i){var j=0;i&&b.$watch(h,function(b){var f=++j;if(b&&a.isString(b)){var g,h;g=d.get(b),g||(h=e.defer(),c.get(b,{cache:d,transformResponse:function(a){return a}}).success(function(a){f===j&&h.resolve(a)}).error(function(){f===j&&i.clear(),h.resolve()}),g=h.promise),e.when(g).then(function(b){b&&(a.isArray(b)?b=b[1]:a.isObject(b)&&(b=b.data),b=b.replace(/^(\r\n|\r|\n)/m,""),i.highlight(b))})}else i.clear()})}}}}]},function(b){b.directive("hljs",f),a.forEach(["interpolate","hljsInterpolate","compile","hljsCompile"],function(a){b.directive(a,g(a))}),a.forEach(["language","hljsLanguage"],function(a){b.directive(a,h(a))}),a.forEach(["source","hljsSource"],function(a){b.directive(a,i(a))}),a.forEach(["include","hljsInclude"],function(a){b.directive(a,j(a))})}(e),"hljs"}); \ No newline at end of file diff --git a/public/javascripts/highlightjs/highlight.pack.js b/public/javascripts/highlightjs/highlight.pack.js index 52f8b93..e70a496 100644 --- a/public/javascripts/highlightjs/highlight.pack.js +++ b/public/javascripts/highlightjs/highlight.pack.js @@ -1 +1,2 @@ -var hljs=new function(){function k(v){return v.replace(/&/gm,"&").replace(//gm,">")}function t(v){return v.nodeName.toLowerCase()}function i(w,x){var v=w&&w.exec(x);return v&&v.index==0}function d(v){return Array.prototype.map.call(v.childNodes,function(w){if(w.nodeType==3){return b.useBR?w.nodeValue.replace(/\n/g,""):w.nodeValue}if(t(w)=="br"){return"\n"}return d(w)}).join("")}function r(w){var v=(w.className+" "+(w.parentNode?w.parentNode.className:"")).split(/\s+/);v=v.map(function(x){return x.replace(/^language-/,"")});return v.filter(function(x){return j(x)||x=="no-highlight"})[0]}function o(x,y){var v={};for(var w in x){v[w]=x[w]}if(y){for(var w in y){v[w]=y[w]}}return v}function u(x){var v=[];(function w(y,z){for(var A=y.firstChild;A;A=A.nextSibling){if(A.nodeType==3){z+=A.nodeValue.length}else{if(t(A)=="br"){z+=1}else{if(A.nodeType==1){v.push({event:"start",offset:z,node:A});z=w(A,z);v.push({event:"stop",offset:z,node:A})}}}}return z})(x,0);return v}function q(w,y,C){var x=0;var F="";var z=[];function B(){if(!w.length||!y.length){return w.length?w:y}if(w[0].offset!=y[0].offset){return(w[0].offset"}function E(G){F+=""}function v(G){(G.event=="start"?A:E)(G.node)}while(w.length||y.length){var D=B();F+=k(C.substr(x,D[0].offset-x));x=D[0].offset;if(D==w){z.reverse().forEach(E);do{v(D.splice(0,1)[0]);D=B()}while(D==w&&D.length&&D[0].offset==x);z.reverse().forEach(A)}else{if(D[0].event=="start"){z.push(D[0].node)}else{z.pop()}v(D.splice(0,1)[0])}}return F+k(C.substr(x))}function m(y){function v(z){return(z&&z.source)||z}function w(A,z){return RegExp(v(A),"m"+(y.cI?"i":"")+(z?"g":""))}function x(D,C){if(D.compiled){return}D.compiled=true;D.k=D.k||D.bK;if(D.k){var z={};function E(G,F){if(y.cI){F=F.toLowerCase()}F.split(" ").forEach(function(H){var I=H.split("|");z[I[0]]=[G,I[1]?Number(I[1]):1]})}if(typeof D.k=="string"){E("keyword",D.k)}else{Object.keys(D.k).forEach(function(F){E(F,D.k[F])})}D.k=z}D.lR=w(D.l||/\b[A-Za-z0-9_]+\b/,true);if(C){if(D.bK){D.b=D.bK.split(" ").join("|")}if(!D.b){D.b=/\B|\b/}D.bR=w(D.b);if(!D.e&&!D.eW){D.e=/\B|\b/}if(D.e){D.eR=w(D.e)}D.tE=v(D.e)||"";if(D.eW&&C.tE){D.tE+=(D.e?"|":"")+C.tE}}if(D.i){D.iR=w(D.i)}if(D.r===undefined){D.r=1}if(!D.c){D.c=[]}var B=[];D.c.forEach(function(F){if(F.v){F.v.forEach(function(G){B.push(o(F,G))})}else{B.push(F=="self"?D:F)}});D.c=B;D.c.forEach(function(F){x(F,D)});if(D.starts){x(D.starts,C)}var A=D.c.map(function(F){return F.bK?"\\.?\\b("+F.b+")\\b\\.?":F.b}).concat([D.tE]).concat([D.i]).map(v).filter(Boolean);D.t=A.length?w(A.join("|"),true):{exec:function(F){return null}};D.continuation={}}x(y)}function c(S,L,J,R){function v(U,V){for(var T=0;T";U+=Z+'">';return U+X+Y}function N(){var U=k(C);if(!I.k){return U}var T="";var X=0;I.lR.lastIndex=0;var V=I.lR.exec(U);while(V){T+=U.substr(X,V.index-X);var W=E(I,V);if(W){H+=W[1];T+=w(W[0],V[0])}else{T+=V[0]}X=I.lR.lastIndex;V=I.lR.exec(U)}return T+U.substr(X)}function F(){if(I.sL&&!f[I.sL]){return k(C)}var T=I.sL?c(I.sL,C,true,I.continuation.top):g(C);if(I.r>0){H+=T.r}if(I.subLanguageMode=="continuous"){I.continuation.top=T.top}return w(T.language,T.value,false,true)}function Q(){return I.sL!==undefined?F():N()}function P(V,U){var T=V.cN?w(V.cN,"",true):"";if(V.rB){D+=T;C=""}else{if(V.eB){D+=k(U)+T;C=""}else{D+=T;C=U}}I=Object.create(V,{parent:{value:I}})}function G(T,X){C+=T;if(X===undefined){D+=Q();return 0}var V=v(X,I);if(V){D+=Q();P(V,X);return V.rB?0:X.length}var W=z(I,X);if(W){var U=I;if(!(U.rE||U.eE)){C+=X}D+=Q();do{if(I.cN){D+=""}H+=I.r;I=I.parent}while(I!=W.parent);if(U.eE){D+=k(X)}C="";if(W.starts){P(W.starts,"")}return U.rE?0:X.length}if(A(X,I)){throw new Error('Illegal lexeme "'+X+'" for mode "'+(I.cN||"")+'"')}C+=X;return X.length||1}var M=j(S);if(!M){throw new Error('Unknown language: "'+S+'"')}m(M);var I=R||M;var D="";for(var K=I;K!=M;K=K.parent){if(K.cN){D=w(K.cN,D,true)}}var C="";var H=0;try{var B,y,x=0;while(true){I.t.lastIndex=x;B=I.t.exec(L);if(!B){break}y=G(L.substr(x,B.index-x),B[0]);x=B.index+y}G(L.substr(x));for(var K=I;K.parent;K=K.parent){if(K.cN){D+=""}}return{r:H,value:D,language:S,top:I}}catch(O){if(O.message.indexOf("Illegal")!=-1){return{r:0,value:k(L)}}else{throw O}}}function g(y,x){x=x||b.languages||Object.keys(f);var v={r:0,value:k(y)};var w=v;x.forEach(function(z){if(!j(z)){return}var A=c(z,y,false);A.language=z;if(A.r>w.r){w=A}if(A.r>v.r){w=v;v=A}});if(w.language){v.second_best=w}return v}function h(v){if(b.tabReplace){v=v.replace(/^((<[^>]+>|\t)+)/gm,function(w,z,y,x){return z.replace(/\t/g,b.tabReplace)})}if(b.useBR){v=v.replace(/\n/g,"
")}return v}function p(z){var y=d(z);var A=r(z);if(A=="no-highlight"){return}var v=A?c(A,y,true):g(y);var w=u(z);if(w.length){var x=document.createElementNS("http://www.w3.org/1999/xhtml","pre");x.innerHTML=v.value;v.value=q(w,u(x),y)}v.value=h(v.value);z.innerHTML=v.value;z.className+=" hljs "+(!A&&v.language||"");z.result={language:v.language,re:v.r};if(v.second_best){z.second_best={language:v.second_best.language,re:v.second_best.r}}}var b={classPrefix:"hljs-",tabReplace:null,useBR:false,languages:undefined};function s(v){b=o(b,v)}function l(){if(l.called){return}l.called=true;var v=document.querySelectorAll("pre code");Array.prototype.forEach.call(v,p)}function a(){addEventListener("DOMContentLoaded",l,false);addEventListener("load",l,false)}var f={};var n={};function e(v,x){var w=f[v]=x(this);if(w.aliases){w.aliases.forEach(function(y){n[y]=v})}}function j(v){return f[v]||f[n[v]]}this.highlight=c;this.highlightAuto=g;this.fixMarkup=h;this.highlightBlock=p;this.configure=s;this.initHighlighting=l;this.initHighlightingOnLoad=a;this.registerLanguage=e;this.getLanguage=j;this.inherit=o;this.IR="[a-zA-Z][a-zA-Z0-9_]*";this.UIR="[a-zA-Z_][a-zA-Z0-9_]*";this.NR="\\b\\d+(\\.\\d+)?";this.CNR="(\\b0[xX][a-fA-F0-9]+|(\\b\\d+(\\.\\d*)?|\\.\\d+)([eE][-+]?\\d+)?)";this.BNR="\\b(0b[01]+)";this.RSR="!|!=|!==|%|%=|&|&&|&=|\\*|\\*=|\\+|\\+=|,|-|-=|/=|/|:|;|<<|<<=|<=|<|===|==|=|>>>=|>>=|>=|>>>|>>|>|\\?|\\[|\\{|\\(|\\^|\\^=|\\||\\|=|\\|\\||~";this.BE={b:"\\\\[\\s\\S]",r:0};this.ASM={cN:"string",b:"'",e:"'",i:"\\n",c:[this.BE]};this.QSM={cN:"string",b:'"',e:'"',i:"\\n",c:[this.BE]};this.CLCM={cN:"comment",b:"//",e:"$"};this.CBLCLM={cN:"comment",b:"/\\*",e:"\\*/"};this.HCM={cN:"comment",b:"#",e:"$"};this.NM={cN:"number",b:this.NR,r:0};this.CNM={cN:"number",b:this.CNR,r:0};this.BNM={cN:"number",b:this.BNR,r:0};this.REGEXP_MODE={cN:"regexp",b:/\//,e:/\/[gim]*/,i:/\n/,c:[this.BE,{b:/\[/,e:/\]/,r:0,c:[this.BE]}]};this.TM={cN:"title",b:this.IR,r:0};this.UTM={cN:"title",b:this.UIR,r:0}}();hljs.registerLanguage("xml",function(a){var c="[A-Za-z0-9\\._:-]+";var d={b:/<\?(php)?(?!\w)/,e:/\?>/,sL:"php",subLanguageMode:"continuous"};var b={eW:true,i:/]+/}]}]}]};return{aliases:["html"],cI:true,c:[{cN:"doctype",b:"",r:10,c:[{b:"\\[",e:"\\]"}]},{cN:"comment",b:"",r:10},{cN:"cdata",b:"<\\!\\[CDATA\\[",e:"\\]\\]>",r:10},{cN:"tag",b:"|$)",e:">",k:{title:"style"},c:[b],starts:{e:"",rE:true,sL:"css"}},{cN:"tag",b:"|$)",e:">",k:{title:"script"},c:[b],starts:{e:"<\/script>",rE:true,sL:"javascript"}},{b:"<%",e:"%>",sL:"vbscript"},d,{cN:"pi",b:/<\?\w+/,e:/\?>/,r:10},{cN:"tag",b:"",c:[{cN:"title",b:"[^ /><]+",r:0},b]}]}});hljs.registerLanguage("json",function(a){var e={literal:"true false null"};var d=[a.QSM,a.CNM];var c={cN:"value",e:",",eW:true,eE:true,c:d,k:e};var b={b:"{",e:"}",c:[{cN:"attribute",b:'\\s*"',e:'"\\s*:\\s*',eB:true,eE:true,c:[a.BE],i:"\\n",starts:c}],i:"\\S"};var f={b:"\\[",e:"\\]",c:[a.inherit(c,{cN:null})],i:"\\S"};d.splice(d.length,0,b,f);return{c:d,k:e,i:"\\S"}}); \ No newline at end of file +/*! highlight.js v9.8.0 | BSD3 License | git.io/hljslicense */ +!function(e){var n="object"==typeof window&&window||"object"==typeof self&&self;"undefined"!=typeof exports?e(exports):n&&(n.hljs=e({}),"function"==typeof define&&define.amd&&define([],function(){return n.hljs}))}(function(e){function n(e){return e.replace(/[&<>]/gm,function(e){return I[e]})}function t(e){return e.nodeName.toLowerCase()}function r(e,n){var t=e&&e.exec(n);return t&&0===t.index}function a(e){return k.test(e)}function i(e){var n,t,r,i,o=e.className+" ";if(o+=e.parentNode?e.parentNode.className:"",t=B.exec(o))return R(t[1])?t[1]:"no-highlight";for(o=o.split(/\s+/),n=0,r=o.length;r>n;n++)if(i=o[n],a(i)||R(i))return i}function o(e,n){var t,r={};for(t in e)r[t]=e[t];if(n)for(t in n)r[t]=n[t];return r}function u(e){var n=[];return function r(e,a){for(var i=e.firstChild;i;i=i.nextSibling)3===i.nodeType?a+=i.nodeValue.length:1===i.nodeType&&(n.push({event:"start",offset:a,node:i}),a=r(i,a),t(i).match(/br|hr|img|input/)||n.push({event:"stop",offset:a,node:i}));return a}(e,0),n}function c(e,r,a){function i(){return e.length&&r.length?e[0].offset!==r[0].offset?e[0].offset"}function u(e){l+=""}function c(e){("start"===e.event?o:u)(e.node)}for(var s=0,l="",f=[];e.length||r.length;){var g=i();if(l+=n(a.substr(s,g[0].offset-s)),s=g[0].offset,g===e){f.reverse().forEach(u);do c(g.splice(0,1)[0]),g=i();while(g===e&&g.length&&g[0].offset===s);f.reverse().forEach(o)}else"start"===g[0].event?f.push(g[0].node):f.pop(),c(g.splice(0,1)[0])}return l+n(a.substr(s))}function s(e){function n(e){return e&&e.source||e}function t(t,r){return new RegExp(n(t),"m"+(e.cI?"i":"")+(r?"g":""))}function r(a,i){if(!a.compiled){if(a.compiled=!0,a.k=a.k||a.bK,a.k){var u={},c=function(n,t){e.cI&&(t=t.toLowerCase()),t.split(" ").forEach(function(e){var t=e.split("|");u[t[0]]=[n,t[1]?Number(t[1]):1]})};"string"==typeof a.k?c("keyword",a.k):E(a.k).forEach(function(e){c(e,a.k[e])}),a.k=u}a.lR=t(a.l||/\w+/,!0),i&&(a.bK&&(a.b="\\b("+a.bK.split(" ").join("|")+")\\b"),a.b||(a.b=/\B|\b/),a.bR=t(a.b),a.e||a.eW||(a.e=/\B|\b/),a.e&&(a.eR=t(a.e)),a.tE=n(a.e)||"",a.eW&&i.tE&&(a.tE+=(a.e?"|":"")+i.tE)),a.i&&(a.iR=t(a.i)),null==a.r&&(a.r=1),a.c||(a.c=[]);var s=[];a.c.forEach(function(e){e.v?e.v.forEach(function(n){s.push(o(e,n))}):s.push("self"===e?a:e)}),a.c=s,a.c.forEach(function(e){r(e,a)}),a.starts&&r(a.starts,i);var l=a.c.map(function(e){return e.bK?"\\.?("+e.b+")\\.?":e.b}).concat([a.tE,a.i]).map(n).filter(Boolean);a.t=l.length?t(l.join("|"),!0):{exec:function(){return null}}}}r(e)}function l(e,t,a,i){function o(e,n){var t,a;for(t=0,a=n.c.length;a>t;t++)if(r(n.c[t].bR,e))return n.c[t]}function u(e,n){if(r(e.eR,n)){for(;e.endsParent&&e.parent;)e=e.parent;return e}return e.eW?u(e.parent,n):void 0}function c(e,n){return!a&&r(n.iR,e)}function g(e,n){var t=N.cI?n[0].toLowerCase():n[0];return e.k.hasOwnProperty(t)&&e.k[t]}function h(e,n,t,r){var a=r?"":y.classPrefix,i='',i+n+o}function p(){var e,t,r,a;if(!E.k)return n(B);for(a="",t=0,E.lR.lastIndex=0,r=E.lR.exec(B);r;)a+=n(B.substr(t,r.index-t)),e=g(E,r),e?(M+=e[1],a+=h(e[0],n(r[0]))):a+=n(r[0]),t=E.lR.lastIndex,r=E.lR.exec(B);return a+n(B.substr(t))}function d(){var e="string"==typeof E.sL;if(e&&!x[E.sL])return n(B);var t=e?l(E.sL,B,!0,L[E.sL]):f(B,E.sL.length?E.sL:void 0);return E.r>0&&(M+=t.r),e&&(L[E.sL]=t.top),h(t.language,t.value,!1,!0)}function b(){k+=null!=E.sL?d():p(),B=""}function v(e){k+=e.cN?h(e.cN,"",!0):"",E=Object.create(e,{parent:{value:E}})}function m(e,n){if(B+=e,null==n)return b(),0;var t=o(n,E);if(t)return t.skip?B+=n:(t.eB&&(B+=n),b(),t.rB||t.eB||(B=n)),v(t,n),t.rB?0:n.length;var r=u(E,n);if(r){var a=E;a.skip?B+=n:(a.rE||a.eE||(B+=n),b(),a.eE&&(B=n));do E.cN&&(k+=C),E.skip||(M+=E.r),E=E.parent;while(E!==r.parent);return r.starts&&v(r.starts,""),a.rE?0:n.length}if(c(n,E))throw new Error('Illegal lexeme "'+n+'" for mode "'+(E.cN||"")+'"');return B+=n,n.length||1}var N=R(e);if(!N)throw new Error('Unknown language: "'+e+'"');s(N);var w,E=i||N,L={},k="";for(w=E;w!==N;w=w.parent)w.cN&&(k=h(w.cN,"",!0)+k);var B="",M=0;try{for(var I,j,O=0;;){if(E.t.lastIndex=O,I=E.t.exec(t),!I)break;j=m(t.substr(O,I.index-O),I[0]),O=I.index+j}for(m(t.substr(O)),w=E;w.parent;w=w.parent)w.cN&&(k+=C);return{r:M,value:k,language:e,top:E}}catch(T){if(T.message&&-1!==T.message.indexOf("Illegal"))return{r:0,value:n(t)};throw T}}function f(e,t){t=t||y.languages||E(x);var r={r:0,value:n(e)},a=r;return t.filter(R).forEach(function(n){var t=l(n,e,!1);t.language=n,t.r>a.r&&(a=t),t.r>r.r&&(a=r,r=t)}),a.language&&(r.second_best=a),r}function g(e){return y.tabReplace||y.useBR?e.replace(M,function(e,n){return y.useBR&&"\n"===e?"
":y.tabReplace?n.replace(/\t/g,y.tabReplace):void 0}):e}function h(e,n,t){var r=n?L[n]:t,a=[e.trim()];return e.match(/\bhljs\b/)||a.push("hljs"),-1===e.indexOf(r)&&a.push(r),a.join(" ").trim()}function p(e){var n,t,r,o,s,p=i(e);a(p)||(y.useBR?(n=document.createElementNS("http://www.w3.org/1999/xhtml","div"),n.innerHTML=e.innerHTML.replace(/\n/g,"").replace(//g,"\n")):n=e,s=n.textContent,r=p?l(p,s,!0):f(s),t=u(n),t.length&&(o=document.createElementNS("http://www.w3.org/1999/xhtml","div"),o.innerHTML=r.value,r.value=c(t,u(o),s)),r.value=g(r.value),e.innerHTML=r.value,e.className=h(e.className,p,r.language),e.result={language:r.language,re:r.r},r.second_best&&(e.second_best={language:r.second_best.language,re:r.second_best.r}))}function d(e){y=o(y,e)}function b(){if(!b.called){b.called=!0;var e=document.querySelectorAll("pre code");w.forEach.call(e,p)}}function v(){addEventListener("DOMContentLoaded",b,!1),addEventListener("load",b,!1)}function m(n,t){var r=x[n]=t(e);r.aliases&&r.aliases.forEach(function(e){L[e]=n})}function N(){return E(x)}function R(e){return e=(e||"").toLowerCase(),x[e]||x[L[e]]}var w=[],E=Object.keys,x={},L={},k=/^(no-?highlight|plain|text)$/i,B=/\blang(?:uage)?-([\w-]+)\b/i,M=/((^(<[^>]+>|\t|)+|(?:\n)))/gm,C="
",y={classPrefix:"hljs-",tabReplace:null,useBR:!1,languages:void 0},I={"&":"&","<":"<",">":">"};return e.highlight=l,e.highlightAuto=f,e.fixMarkup=g,e.highlightBlock=p,e.configure=d,e.initHighlighting=b,e.initHighlightingOnLoad=v,e.registerLanguage=m,e.listLanguages=N,e.getLanguage=R,e.inherit=o,e.IR="[a-zA-Z]\\w*",e.UIR="[a-zA-Z_]\\w*",e.NR="\\b\\d+(\\.\\d+)?",e.CNR="(-?)(\\b0[xX][a-fA-F0-9]+|(\\b\\d+(\\.\\d*)?|\\.\\d+)([eE][-+]?\\d+)?)",e.BNR="\\b(0b[01]+)",e.RSR="!|!=|!==|%|%=|&|&&|&=|\\*|\\*=|\\+|\\+=|,|-|-=|/=|/|:|;|<<|<<=|<=|<|===|==|=|>>>=|>>=|>=|>>>|>>|>|\\?|\\[|\\{|\\(|\\^|\\^=|\\||\\|=|\\|\\||~",e.BE={b:"\\\\[\\s\\S]",r:0},e.ASM={cN:"string",b:"'",e:"'",i:"\\n",c:[e.BE]},e.QSM={cN:"string",b:'"',e:'"',i:"\\n",c:[e.BE]},e.PWM={b:/\b(a|an|the|are|I'm|isn't|don't|doesn't|won't|but|just|should|pretty|simply|enough|gonna|going|wtf|so|such|will|you|your|like)\b/},e.C=function(n,t,r){var a=e.inherit({cN:"comment",b:n,e:t,c:[]},r||{});return a.c.push(e.PWM),a.c.push({cN:"doctag",b:"(?:TODO|FIXME|NOTE|BUG|XXX):",r:0}),a},e.CLCM=e.C("//","$"),e.CBCM=e.C("/\\*","\\*/"),e.HCM=e.C("#","$"),e.NM={cN:"number",b:e.NR,r:0},e.CNM={cN:"number",b:e.CNR,r:0},e.BNM={cN:"number",b:e.BNR,r:0},e.CSSNM={cN:"number",b:e.NR+"(%|em|ex|ch|rem|vw|vh|vmin|vmax|cm|mm|in|pt|pc|px|deg|grad|rad|turn|s|ms|Hz|kHz|dpi|dpcm|dppx)?",r:0},e.RM={cN:"regexp",b:/\//,e:/\/[gimuy]*/,i:/\n/,c:[e.BE,{b:/\[/,e:/\]/,r:0,c:[e.BE]}]},e.TM={cN:"title",b:e.IR,r:0},e.UTM={cN:"title",b:e.UIR,r:0},e.METHOD_GUARD={b:"\\.\\s*"+e.UIR,r:0},e});hljs.registerLanguage("sql",function(e){var t=e.C("--","$");return{cI:!0,i:/[<>{}*#]/,c:[{bK:"begin end start commit rollback savepoint lock alter create drop rename call delete do handler insert load replace select truncate update set show pragma grant merge describe use explain help declare prepare execute deallocate release unlock purge reset change stop analyze cache flush optimize repair kill install uninstall checksum restore check backup revoke comment",e:/;/,eW:!0,l:/[\w\.]+/,k:{keyword:"abort abs absolute acc acce accep accept access accessed accessible account acos action activate add addtime admin administer advanced advise aes_decrypt aes_encrypt after agent aggregate ali alia alias allocate allow alter always analyze ancillary and any anydata anydataset anyschema anytype apply archive archived archivelog are as asc ascii asin assembly assertion associate asynchronous at atan atn2 attr attri attrib attribu attribut attribute attributes audit authenticated authentication authid authors auto autoallocate autodblink autoextend automatic availability avg backup badfile basicfile before begin beginning benchmark between bfile bfile_base big bigfile bin binary_double binary_float binlog bit_and bit_count bit_length bit_or bit_xor bitmap blob_base block blocksize body both bound buffer_cache buffer_pool build bulk by byte byteordermark bytes cache caching call calling cancel capacity cascade cascaded case cast catalog category ceil ceiling chain change changed char_base char_length character_length characters characterset charindex charset charsetform charsetid check checksum checksum_agg child choose chr chunk class cleanup clear client clob clob_base clone close cluster_id cluster_probability cluster_set clustering coalesce coercibility col collate collation collect colu colum column column_value columns columns_updated comment commit compact compatibility compiled complete composite_limit compound compress compute concat concat_ws concurrent confirm conn connec connect connect_by_iscycle connect_by_isleaf connect_by_root connect_time connection consider consistent constant constraint constraints constructor container content contents context contributors controlfile conv convert convert_tz corr corr_k corr_s corresponding corruption cos cost count count_big counted covar_pop covar_samp cpu_per_call cpu_per_session crc32 create creation critical cross cube cume_dist curdate current current_date current_time current_timestamp current_user cursor curtime customdatum cycle data database databases datafile datafiles datalength date_add date_cache date_format date_sub dateadd datediff datefromparts datename datepart datetime2fromparts day day_to_second dayname dayofmonth dayofweek dayofyear days db_role_change dbtimezone ddl deallocate declare decode decompose decrement decrypt deduplicate def defa defau defaul default defaults deferred defi defin define degrees delayed delegate delete delete_all delimited demand dense_rank depth dequeue des_decrypt des_encrypt des_key_file desc descr descri describ describe descriptor deterministic diagnostics difference dimension direct_load directory disable disable_all disallow disassociate discardfile disconnect diskgroup distinct distinctrow distribute distributed div do document domain dotnet double downgrade drop dumpfile duplicate duration each edition editionable editions element ellipsis else elsif elt empty enable enable_all enclosed encode encoding encrypt end end-exec endian enforced engine engines enqueue enterprise entityescaping eomonth error errors escaped evalname evaluate event eventdata events except exception exceptions exchange exclude excluding execu execut execute exempt exists exit exp expire explain export export_set extended extent external external_1 external_2 externally extract failed failed_login_attempts failover failure far fast feature_set feature_value fetch field fields file file_name_convert filesystem_like_logging final finish first first_value fixed flash_cache flashback floor flush following follows for forall force form forma format found found_rows freelist freelists freepools fresh from from_base64 from_days ftp full function general generated get get_format get_lock getdate getutcdate global global_name globally go goto grant grants greatest group group_concat group_id grouping grouping_id groups gtid_subtract guarantee guard handler hash hashkeys having hea head headi headin heading heap help hex hierarchy high high_priority hosts hour http id ident_current ident_incr ident_seed identified identity idle_time if ifnull ignore iif ilike ilm immediate import in include including increment index indexes indexing indextype indicator indices inet6_aton inet6_ntoa inet_aton inet_ntoa infile initial initialized initially initrans inmemory inner innodb input insert install instance instantiable instr interface interleaved intersect into invalidate invisible is is_free_lock is_ipv4 is_ipv4_compat is_not is_not_null is_used_lock isdate isnull isolation iterate java join json json_exists keep keep_duplicates key keys kill language large last last_day last_insert_id last_value lax lcase lead leading least leaves left len lenght length less level levels library like like2 like4 likec limit lines link list listagg little ln load load_file lob lobs local localtime localtimestamp locate locator lock locked log log10 log2 logfile logfiles logging logical logical_reads_per_call logoff logon logs long loop low low_priority lower lpad lrtrim ltrim main make_set makedate maketime managed management manual map mapping mask master master_pos_wait match matched materialized max maxextents maximize maxinstances maxlen maxlogfiles maxloghistory maxlogmembers maxsize maxtrans md5 measures median medium member memcompress memory merge microsecond mid migration min minextents minimum mining minus minute minvalue missing mod mode model modification modify module monitoring month months mount move movement multiset mutex name name_const names nan national native natural nav nchar nclob nested never new newline next nextval no no_write_to_binlog noarchivelog noaudit nobadfile nocheck nocompress nocopy nocycle nodelay nodiscardfile noentityescaping noguarantee nokeep nologfile nomapping nomaxvalue nominimize nominvalue nomonitoring none noneditionable nonschema noorder nopr nopro noprom nopromp noprompt norely noresetlogs noreverse normal norowdependencies noschemacheck noswitch not nothing notice notrim novalidate now nowait nth_value nullif nulls num numb numbe nvarchar nvarchar2 object ocicoll ocidate ocidatetime ociduration ociinterval ociloblocator ocinumber ociref ocirefcursor ocirowid ocistring ocitype oct octet_length of off offline offset oid oidindex old on online only opaque open operations operator optimal optimize option optionally or oracle oracle_date oradata ord ordaudio orddicom orddoc order ordimage ordinality ordvideo organization orlany orlvary out outer outfile outline output over overflow overriding package pad parallel parallel_enable parameters parent parse partial partition partitions pascal passing password password_grace_time password_lock_time password_reuse_max password_reuse_time password_verify_function patch path patindex pctincrease pctthreshold pctused pctversion percent percent_rank percentile_cont percentile_disc performance period period_add period_diff permanent physical pi pipe pipelined pivot pluggable plugin policy position post_transaction pow power pragma prebuilt precedes preceding precision prediction prediction_cost prediction_details prediction_probability prediction_set prepare present preserve prior priority private private_sga privileges procedural procedure procedure_analyze processlist profiles project prompt protection public publishingservername purge quarter query quick quiesce quota quotename radians raise rand range rank raw read reads readsize rebuild record records recover recovery recursive recycle redo reduced ref reference referenced references referencing refresh regexp_like register regr_avgx regr_avgy regr_count regr_intercept regr_r2 regr_slope regr_sxx regr_sxy reject rekey relational relative relaylog release release_lock relies_on relocate rely rem remainder rename repair repeat replace replicate replication required reset resetlogs resize resource respect restore restricted result result_cache resumable resume retention return returning returns reuse reverse revoke right rlike role roles rollback rolling rollup round row row_count rowdependencies rowid rownum rows rtrim rules safe salt sample save savepoint sb1 sb2 sb4 scan schema schemacheck scn scope scroll sdo_georaster sdo_topo_geometry search sec_to_time second section securefile security seed segment select self sequence sequential serializable server servererror session session_user sessions_per_user set sets settings sha sha1 sha2 share shared shared_pool short show shrink shutdown si_averagecolor si_colorhistogram si_featurelist si_positionalcolor si_stillimage si_texture siblings sid sign sin size size_t sizes skip slave sleep smalldatetimefromparts smallfile snapshot some soname sort soundex source space sparse spfile split sql sql_big_result sql_buffer_result sql_cache sql_calc_found_rows sql_small_result sql_variant_property sqlcode sqldata sqlerror sqlname sqlstate sqrt square standalone standby start starting startup statement static statistics stats_binomial_test stats_crosstab stats_ks_test stats_mode stats_mw_test stats_one_way_anova stats_t_test_ stats_t_test_indep stats_t_test_one stats_t_test_paired stats_wsr_test status std stddev stddev_pop stddev_samp stdev stop storage store stored str str_to_date straight_join strcmp strict string struct stuff style subdate subpartition subpartitions substitutable substr substring subtime subtring_index subtype success sum suspend switch switchoffset switchover sync synchronous synonym sys sys_xmlagg sysasm sysaux sysdate sysdatetimeoffset sysdba sysoper system system_user sysutcdatetime table tables tablespace tan tdo template temporary terminated tertiary_weights test than then thread through tier ties time time_format time_zone timediff timefromparts timeout timestamp timestampadd timestampdiff timezone_abbr timezone_minute timezone_region to to_base64 to_date to_days to_seconds todatetimeoffset trace tracking transaction transactional translate translation treat trigger trigger_nestlevel triggers trim truncate try_cast try_convert try_parse type ub1 ub2 ub4 ucase unarchived unbounded uncompress under undo unhex unicode uniform uninstall union unique unix_timestamp unknown unlimited unlock unpivot unrecoverable unsafe unsigned until untrusted unusable unused update updated upgrade upped upper upsert url urowid usable usage use use_stored_outlines user user_data user_resources users using utc_date utc_timestamp uuid uuid_short validate validate_password_strength validation valist value values var var_samp varcharc vari varia variab variabl variable variables variance varp varraw varrawc varray verify version versions view virtual visible void wait wallet warning warnings week weekday weekofyear wellformed when whene whenev wheneve whenever where while whitespace with within without work wrapped xdb xml xmlagg xmlattributes xmlcast xmlcolattval xmlelement xmlexists xmlforest xmlindex xmlnamespaces xmlpi xmlquery xmlroot xmlschema xmlserialize xmltable xmltype xor year year_to_month years yearweek",literal:"true false null",built_in:"array bigint binary bit blob boolean char character date dec decimal float int int8 integer interval number numeric real record serial serial8 smallint text varchar varying void"},c:[{cN:"string",b:"'",e:"'",c:[e.BE,{b:"''"}]},{cN:"string",b:'"',e:'"',c:[e.BE,{b:'""'}]},{cN:"string",b:"`",e:"`",c:[e.BE]},e.CNM,e.CBCM,t]},e.CBCM,t]}});hljs.registerLanguage("xml",function(s){var e="[A-Za-z0-9\\._:-]+",t={eW:!0,i:/`]+/}]}]}]};return{aliases:["html","xhtml","rss","atom","xjb","xsd","xsl","plist"],cI:!0,c:[{cN:"meta",b:"",r:10,c:[{b:"\\[",e:"\\]"}]},s.C("",{r:10}),{b:"<\\!\\[CDATA\\[",e:"\\]\\]>",r:10},{b:/<\?(php)?/,e:/\?>/,sL:"php",c:[{b:"/\\*",e:"\\*/",skip:!0}]},{cN:"tag",b:"|$)",e:">",k:{name:"style"},c:[t],starts:{e:"",rE:!0,sL:["css","xml"]}},{cN:"tag",b:"|$)",e:">",k:{name:"script"},c:[t],starts:{e:"",rE:!0,sL:["actionscript","javascript","handlebars","xml"]}},{cN:"meta",v:[{b:/<\?xml/,e:/\?>/,r:10},{b:/<\?\w+/,e:/\?>/}]},{cN:"tag",b:"",c:[{cN:"name",b:/[^\/><\s]+/,r:0},t]}]}});hljs.registerLanguage("python",function(e){var r={cN:"meta",b:/^(>>>|\.\.\.) /},b={cN:"string",c:[e.BE],v:[{b:/(u|b)?r?'''/,e:/'''/,c:[r],r:10},{b:/(u|b)?r?"""/,e:/"""/,c:[r],r:10},{b:/(u|r|ur)'/,e:/'/,r:10},{b:/(u|r|ur)"/,e:/"/,r:10},{b:/(b|br)'/,e:/'/},{b:/(b|br)"/,e:/"/},e.ASM,e.QSM]},a={cN:"number",r:0,v:[{b:e.BNR+"[lLjJ]?"},{b:"\\b(0o[0-7]+)[lLjJ]?"},{b:e.CNR+"[lLjJ]?"}]},l={cN:"params",b:/\(/,e:/\)/,c:["self",r,a,b]};return{aliases:["py","gyp"],k:{keyword:"and elif is global as in if from raise for except finally print import pass return exec else break not with class assert yield try while continue del or def lambda async await nonlocal|10 None True False",built_in:"Ellipsis NotImplemented"},i:/(<\/|->|\?)|=>/,c:[r,a,b,e.HCM,{v:[{cN:"function",bK:"def"},{cN:"class",bK:"class"}],e:/:/,i:/[${=;\n,]/,c:[e.UTM,l,{b:/->/,eW:!0,k:"None"}]},{cN:"meta",b:/^[\t ]*@/,e:/$/},{b:/\b(print|exec)\(/}]}});hljs.registerLanguage("makefile",function(e){var a={cN:"variable",b:/\$\(/,e:/\)/,c:[e.BE]};return{aliases:["mk","mak"],c:[e.HCM,{b:/^\w+\s*\W*=/,rB:!0,r:0,starts:{e:/\s*\W*=/,eE:!0,starts:{e:/$/,r:0,c:[a]}}},{cN:"section",b:/^[\w]+:\s*$/},{cN:"meta",b:/^\.PHONY:/,e:/$/,k:{"meta-keyword":".PHONY"},l:/[\.\w]+/},{b:/^\t+/,e:/$/,r:0,c:[e.QSM,a]}]}});hljs.registerLanguage("cpp",function(t){var e={cN:"keyword",b:"\\b[a-z\\d_]*_t\\b"},r={cN:"string",v:[{b:'(u8?|U)?L?"',e:'"',i:"\\n",c:[t.BE]},{b:'(u8?|U)?R"',e:'"',c:[t.BE]},{b:"'\\\\?.",e:"'",i:"."}]},s={cN:"number",v:[{b:"\\b(0b[01']+)"},{b:"\\b([\\d']+(\\.[\\d']*)?|\\.[\\d']+)(u|U|l|L|ul|UL|f|F|b|B)"},{b:"(-?)(\\b0[xX][a-fA-F0-9']+|(\\b[\\d']+(\\.[\\d']*)?|\\.[\\d']+)([eE][-+]?[\\d']+)?)"}],r:0},i={cN:"meta",b:/#\s*[a-z]+\b/,e:/$/,k:{"meta-keyword":"if else elif endif define undef warning error line pragma ifdef ifndef include"},c:[{b:/\\\n/,r:0},t.inherit(r,{cN:"meta-string"}),{cN:"meta-string",b:"<",e:">",i:"\\n"},t.CLCM,t.CBCM]},a=t.IR+"\\s*\\(",c={keyword:"int float while private char catch import module export virtual operator sizeof dynamic_cast|10 typedef const_cast|10 const struct for static_cast|10 union namespace unsigned long volatile static protected bool template mutable if public friend do goto auto void enum else break extern using class asm case typeid short reinterpret_cast|10 default double register explicit signed typename try this switch continue inline delete alignof constexpr decltype noexcept static_assert thread_local restrict _Bool complex _Complex _Imaginary atomic_bool atomic_char atomic_schar atomic_uchar atomic_short atomic_ushort atomic_int atomic_uint atomic_long atomic_ulong atomic_llong atomic_ullong new throw return",built_in:"std string cin cout cerr clog stdin stdout stderr stringstream istringstream ostringstream auto_ptr deque list queue stack vector map set bitset multiset multimap unordered_set unordered_map unordered_multiset unordered_multimap array shared_ptr abort abs acos asin atan2 atan calloc ceil cosh cos exit exp fabs floor fmod fprintf fputs free frexp fscanf isalnum isalpha iscntrl isdigit isgraph islower isprint ispunct isspace isupper isxdigit tolower toupper labs ldexp log10 log malloc realloc memchr memcmp memcpy memset modf pow printf putchar puts scanf sinh sin snprintf sprintf sqrt sscanf strcat strchr strcmp strcpy strcspn strlen strncat strncmp strncpy strpbrk strrchr strspn strstr tanh tan vfprintf vprintf vsprintf endl initializer_list unique_ptr",literal:"true false nullptr NULL"},n=[e,t.CLCM,t.CBCM,s,r];return{aliases:["c","cc","h","c++","h++","hpp"],k:c,i:"",k:c,c:["self",e]},{b:t.IR+"::",k:c},{v:[{b:/=/,e:/;/},{b:/\(/,e:/\)/},{bK:"new throw return else",e:/;/}],k:c,c:n.concat([{b:/\(/,e:/\)/,k:c,c:n.concat(["self"]),r:0}]),r:0},{cN:"function",b:"("+t.IR+"[\\*&\\s]+)+"+a,rB:!0,e:/[{;=]/,eE:!0,k:c,i:/[^\w\s\*&]/,c:[{b:a,rB:!0,c:[t.TM],r:0},{cN:"params",b:/\(/,e:/\)/,k:c,r:0,c:[t.CLCM,t.CBCM,r,s,e]},t.CLCM,t.CBCM,i]}]),exports:{preprocessor:i,strings:r,k:c}}});hljs.registerLanguage("nginx",function(e){var r={cN:"variable",v:[{b:/\$\d+/},{b:/\$\{/,e:/}/},{b:"[\\$\\@]"+e.UIR}]},b={eW:!0,l:"[a-z/_]+",k:{literal:"on off yes no true false none blocked debug info notice warn error crit select break last permanent redirect kqueue rtsig epoll poll /dev/poll"},r:0,i:"=>",c:[e.HCM,{cN:"string",c:[e.BE,r],v:[{b:/"/,e:/"/},{b:/'/,e:/'/}]},{b:"([a-z]+):/",e:"\\s",eW:!0,eE:!0,c:[r]},{cN:"regexp",c:[e.BE,r],v:[{b:"\\s\\^",e:"\\s|{|;",rE:!0},{b:"~\\*?\\s+",e:"\\s|{|;",rE:!0},{b:"\\*(\\.[a-z\\-]+)+"},{b:"([a-z\\-]+\\.)+\\*"}]},{cN:"number",b:"\\b\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}(:\\d{1,5})?\\b"},{cN:"number",b:"\\b\\d+[kKmMgGdshdwy]*\\b",r:0},r]};return{aliases:["nginxconf"],c:[e.HCM,{b:e.UIR+"\\s+{",rB:!0,e:"{",c:[{cN:"section",b:e.UIR}],r:0},{b:e.UIR+"\\s",e:";|{",rB:!0,c:[{cN:"attribute",b:e.UIR,starts:b}],r:0}],i:"[^\\s\\}]"}});hljs.registerLanguage("ruby",function(e){var b="[a-zA-Z_]\\w*[!?=]?|[-+~]\\@|<<|>>|=~|===?|<=>|[<>]=?|\\*\\*|[-/+%^&*~`|]|\\[\\]=?",r={keyword:"and then defined module in return redo if BEGIN retry end for self when next until do begin unless END rescue else break undef not super class case require yield alias while ensure elsif or include attr_reader attr_writer attr_accessor",literal:"true false nil"},c={cN:"doctag",b:"@[A-Za-z]+"},a={b:"#<",e:">"},s=[e.C("#","$",{c:[c]}),e.C("^\\=begin","^\\=end",{c:[c],r:10}),e.C("^__END__","\\n$")],n={cN:"subst",b:"#\\{",e:"}",k:r},t={cN:"string",c:[e.BE,n],v:[{b:/'/,e:/'/},{b:/"/,e:/"/},{b:/`/,e:/`/},{b:"%[qQwWx]?\\(",e:"\\)"},{b:"%[qQwWx]?\\[",e:"\\]"},{b:"%[qQwWx]?{",e:"}"},{b:"%[qQwWx]?<",e:">"},{b:"%[qQwWx]?/",e:"/"},{b:"%[qQwWx]?%",e:"%"},{b:"%[qQwWx]?-",e:"-"},{b:"%[qQwWx]?\\|",e:"\\|"},{b:/\B\?(\\\d{1,3}|\\x[A-Fa-f0-9]{1,2}|\\u[A-Fa-f0-9]{4}|\\?\S)\b/},{b:/<<(-?)\w+$/,e:/^\s*\w+$/}]},i={cN:"params",b:"\\(",e:"\\)",endsParent:!0,k:r},d=[t,a,{cN:"class",bK:"class module",e:"$|;",i:/=/,c:[e.inherit(e.TM,{b:"[A-Za-z_]\\w*(::\\w+)*(\\?|\\!)?"}),{b:"<\\s*",c:[{b:"("+e.IR+"::)?"+e.IR}]}].concat(s)},{cN:"function",bK:"def",e:"$|;",c:[e.inherit(e.TM,{b:b}),i].concat(s)},{b:e.IR+"::"},{cN:"symbol",b:e.UIR+"(\\!|\\?)?:",r:0},{cN:"symbol",b:":(?!\\s)",c:[t,{b:b}],r:0},{cN:"number",b:"(\\b0[0-7_]+)|(\\b0x[0-9a-fA-F_]+)|(\\b[1-9][0-9_]*(\\.[0-9_]+)?)|[0_]\\b",r:0},{b:"(\\$\\W)|((\\$|\\@\\@?)(\\w+))"},{cN:"params",b:/\|/,e:/\|/,k:r},{b:"("+e.RSR+")\\s*",c:[a,{cN:"regexp",c:[e.BE,n],i:/\n/,v:[{b:"/",e:"/[a-z]*"},{b:"%r{",e:"}[a-z]*"},{b:"%r\\(",e:"\\)[a-z]*"},{b:"%r!",e:"![a-z]*"},{b:"%r\\[",e:"\\][a-z]*"}]}].concat(s),r:0}].concat(s);n.c=d,i.c=d;var l="[>?]>",o="[\\w#]+\\(\\w+\\):\\d+:\\d+>",w="(\\w+-)?\\d+\\.\\d+\\.\\d(p\\d+)?[^>]+>",u=[{b:/^\s*=>/,starts:{e:"$",c:d}},{cN:"meta",b:"^("+l+"|"+o+"|"+w+")",starts:{e:"$",c:d}}];return{aliases:["rb","gemspec","podspec","thor","irb"],k:r,i:/\/\*/,c:s.concat(u).concat(d)}});hljs.registerLanguage("perl",function(e){var t="getpwent getservent quotemeta msgrcv scalar kill dbmclose undef lc ma syswrite tr send umask sysopen shmwrite vec qx utime local oct semctl localtime readpipe do return format read sprintf dbmopen pop getpgrp not getpwnam rewinddir qqfileno qw endprotoent wait sethostent bless s|0 opendir continue each sleep endgrent shutdown dump chomp connect getsockname die socketpair close flock exists index shmgetsub for endpwent redo lstat msgctl setpgrp abs exit select print ref gethostbyaddr unshift fcntl syscall goto getnetbyaddr join gmtime symlink semget splice x|0 getpeername recv log setsockopt cos last reverse gethostbyname getgrnam study formline endhostent times chop length gethostent getnetent pack getprotoent getservbyname rand mkdir pos chmod y|0 substr endnetent printf next open msgsnd readdir use unlink getsockopt getpriority rindex wantarray hex system getservbyport endservent int chr untie rmdir prototype tell listen fork shmread ucfirst setprotoent else sysseek link getgrgid shmctl waitpid unpack getnetbyname reset chdir grep split require caller lcfirst until warn while values shift telldir getpwuid my getprotobynumber delete and sort uc defined srand accept package seekdir getprotobyname semop our rename seek if q|0 chroot sysread setpwent no crypt getc chown sqrt write setnetent setpriority foreach tie sin msgget map stat getlogin unless elsif truncate exec keys glob tied closedirioctl socket readlink eval xor readline binmode setservent eof ord bind alarm pipe atan2 getgrent exp time push setgrent gt lt or ne m|0 break given say state when",r={cN:"subst",b:"[$@]\\{",e:"\\}",k:t},s={b:"->{",e:"}"},n={v:[{b:/\$\d/},{b:/[\$%@](\^\w\b|#\w+(::\w+)*|{\w+}|\w+(::\w*)*)/},{b:/[\$%@][^\s\w{]/,r:0}]},i=[e.BE,r,n],o=[n,e.HCM,e.C("^\\=\\w","\\=cut",{eW:!0}),s,{cN:"string",c:i,v:[{b:"q[qwxr]?\\s*\\(",e:"\\)",r:5},{b:"q[qwxr]?\\s*\\[",e:"\\]",r:5},{b:"q[qwxr]?\\s*\\{",e:"\\}",r:5},{b:"q[qwxr]?\\s*\\|",e:"\\|",r:5},{b:"q[qwxr]?\\s*\\<",e:"\\>",r:5},{b:"qw\\s+q",e:"q",r:5},{b:"'",e:"'",c:[e.BE]},{b:'"',e:'"'},{b:"`",e:"`",c:[e.BE]},{b:"{\\w+}",c:[],r:0},{b:"-?\\w+\\s*\\=\\>",c:[],r:0}]},{cN:"number",b:"(\\b0[0-7_]+)|(\\b0x[0-9a-fA-F_]+)|(\\b[1-9][0-9_]*(\\.[0-9_]+)?)|[0_]\\b",r:0},{b:"(\\/\\/|"+e.RSR+"|\\b(split|return|print|reverse|grep)\\b)\\s*",k:"split return print reverse grep",r:0,c:[e.HCM,{cN:"regexp",b:"(s|tr|y)/(\\\\.|[^/])*/(\\\\.|[^/])*/[a-z]*",r:10},{cN:"regexp",b:"(m|qr)?/",e:"/[a-z]*",c:[e.BE],r:0}]},{cN:"function",bK:"sub",e:"(\\s*\\(.*?\\))?[;{]",eE:!0,r:5,c:[e.TM]},{b:"-\\w\\b",r:0},{b:"^__DATA__$",e:"^__END__$",sL:"mojolicious",c:[{b:"^@@.*",e:"$",cN:"comment"}]}];return r.c=o,s.c=o,{aliases:["pl","pm"],l:/[\w\.]+/,k:t,c:o}});hljs.registerLanguage("cs",function(e){var i={keyword:"abstract as base bool break byte case catch char checked const continue decimal default delegate do double else enum event explicit extern finally fixed float for foreach goto if implicit in int interface internal is lock long object operator out override params private protected public readonly ref sbyte sealed short sizeof stackalloc static string struct switch this try typeof uint ulong unchecked unsafe ushort using virtual void volatile while nameof add alias ascending async await by descending dynamic equals from get global group into join let on orderby partial remove select set value var where yield",literal:"null false true"},r={cN:"string",b:'@"',e:'"',c:[{b:'""'}]},t=e.inherit(r,{i:/\n/}),a={cN:"subst",b:"{",e:"}",k:i},n=e.inherit(a,{i:/\n/}),c={cN:"string",b:/\$"/,e:'"',i:/\n/,c:[{b:"{{"},{b:"}}"},e.BE,n]},s={cN:"string",b:/\$@"/,e:'"',c:[{b:"{{"},{b:"}}"},{b:'""'},a]},o=e.inherit(s,{i:/\n/,c:[{b:"{{"},{b:"}}"},{b:'""'},n]});a.c=[s,c,r,e.ASM,e.QSM,e.CNM,e.CBCM],n.c=[o,c,t,e.ASM,e.QSM,e.CNM,e.inherit(e.CBCM,{i:/\n/})];var l={v:[s,c,r,e.ASM,e.QSM]},b=e.IR+"(<"+e.IR+"(\\s*,\\s*"+e.IR+")*>)?(\\[\\])?";return{aliases:["csharp"],k:i,i:/::/,c:[e.C("///","$",{rB:!0,c:[{cN:"doctag",v:[{b:"///",r:0},{b:""},{b:""}]}]}),e.CLCM,e.CBCM,{cN:"meta",b:"#",e:"$",k:{"meta-keyword":"if else elif endif define undef warning error line region endregion pragma checksum"}},l,e.CNM,{bK:"class interface",e:/[{;=]/,i:/[^\s:]/,c:[e.TM,e.CLCM,e.CBCM]},{bK:"namespace",e:/[{;=]/,i:/[^\s:]/,c:[e.inherit(e.TM,{b:"[a-zA-Z](\\.?\\w)*"}),e.CLCM,e.CBCM]},{bK:"new return throw await",r:0},{cN:"function",b:"("+b+"\\s+)+"+e.IR+"\\s*\\(",rB:!0,e:/[{;=]/,eE:!0,k:i,c:[{b:e.IR+"\\s*\\(",rB:!0,c:[e.TM],r:0},{cN:"params",b:/\(/,e:/\)/,eB:!0,eE:!0,k:i,r:0,c:[l,e.CNM,e.CBCM]},e.CLCM,e.CBCM]}]}});hljs.registerLanguage("php",function(e){var c={b:"\\$+[a-zA-Z_-ÿ][a-zA-Z0-9_-ÿ]*"},i={cN:"meta",b:/<\?(php)?|\?>/},t={cN:"string",c:[e.BE,i],v:[{b:'b"',e:'"'},{b:"b'",e:"'"},e.inherit(e.ASM,{i:null}),e.inherit(e.QSM,{i:null})]},a={v:[e.BNM,e.CNM]};return{aliases:["php3","php4","php5","php6"],cI:!0,k:"and include_once list abstract global private echo interface as static endswitch array null if endwhile or const for endforeach self var while isset public protected exit foreach throw elseif include __FILE__ empty require_once do xor return parent clone use __CLASS__ __LINE__ else break print eval new catch __METHOD__ case exception default die require __FUNCTION__ enddeclare final try switch continue endfor endif declare unset true false trait goto instanceof insteadof __DIR__ __NAMESPACE__ yield finally",c:[e.HCM,e.C("//","$",{c:[i]}),e.C("/\\*","\\*/",{c:[{cN:"doctag",b:"@[A-Za-z]+"}]}),e.C("__halt_compiler.+?;",!1,{eW:!0,k:"__halt_compiler",l:e.UIR}),{cN:"string",b:/<<<['"]?\w+['"]?$/,e:/^\w+;?$/,c:[e.BE,{cN:"subst",v:[{b:/\$\w+/},{b:/\{\$/,e:/\}/}]}]},i,{cN:"keyword",b:/\$this\b/},c,{b:/(::|->)+[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*/},{cN:"function",bK:"function",e:/[;{]/,eE:!0,i:"\\$|\\[|%",c:[e.UTM,{cN:"params",b:"\\(",e:"\\)",c:["self",c,e.CBCM,t,a]}]},{cN:"class",bK:"class interface",e:"{",eE:!0,i:/[:\(\$"]/,c:[{bK:"extends implements"},e.UTM]},{bK:"namespace",e:";",i:/[\.']/,c:[e.UTM]},{bK:"use",e:";",c:[e.UTM]},{b:"=>"},t,a]}});hljs.registerLanguage("json",function(e){var i={literal:"true false null"},n=[e.QSM,e.CNM],r={e:",",eW:!0,eE:!0,c:n,k:i},t={b:"{",e:"}",c:[{cN:"attr",b:/"/,e:/"/,c:[e.BE],i:"\\n"},e.inherit(r,{b:/:/})],i:"\\S"},c={b:"\\[",e:"\\]",c:[e.inherit(r)],i:"\\S"};return n.splice(n.length,0,t,c),{c:n,k:i,i:"\\S"}});hljs.registerLanguage("javascript",function(e){var r="[A-Za-z$_][0-9A-Za-z$_]*",t={keyword:"in of if for while finally var new function do return void else break catch instanceof with throw case default try this switch continue typeof delete let yield const export super debugger as async await static import from as",literal:"true false null undefined NaN Infinity",built_in:"eval isFinite isNaN parseFloat parseInt decodeURI decodeURIComponent encodeURI encodeURIComponent escape unescape Object Function Boolean Error EvalError InternalError RangeError ReferenceError StopIteration SyntaxError TypeError URIError Number Math Date String RegExp Array Float32Array Float64Array Int16Array Int32Array Int8Array Uint16Array Uint32Array Uint8Array Uint8ClampedArray ArrayBuffer DataView JSON Intl arguments require module console window document Symbol Set Map WeakSet WeakMap Proxy Reflect Promise"},a={cN:"number",v:[{b:"\\b(0[bB][01]+)"},{b:"\\b(0[oO][0-7]+)"},{b:e.CNR}],r:0},n={cN:"subst",b:"\\$\\{",e:"\\}",k:t,c:[]},c={cN:"string",b:"`",e:"`",c:[e.BE,n]};n.c=[e.ASM,e.QSM,c,a,e.RM];var s=n.c.concat([e.CBCM,e.CLCM]);return{aliases:["js","jsx"],k:t,c:[{cN:"meta",r:10,b:/^\s*['"]use (strict|asm)['"]/},{cN:"meta",b:/^#!/,e:/$/},e.ASM,e.QSM,c,e.CLCM,e.CBCM,a,{b:/[{,]\s*/,r:0,c:[{b:r+"\\s*:",rB:!0,r:0,c:[{cN:"attr",b:r,r:0}]}]},{b:"("+e.RSR+"|\\b(case|return|throw)\\b)\\s*",k:"return throw case",c:[e.CLCM,e.CBCM,e.RM,{cN:"function",b:"(\\(.*?\\)|"+r+")\\s*=>",rB:!0,e:"\\s*=>",c:[{cN:"params",v:[{b:r},{b:/\(\s*\)/},{b:/\(/,e:/\)/,eB:!0,eE:!0,k:t,c:s}]}]},{b://,sL:"xml",c:[{b:/<\w+\s*\/>/,skip:!0},{b:/<\w+/,e:/(\/\w+|\w+\/)>/,skip:!0,c:[{b:/<\w+\s*\/>/,skip:!0},"self"]}]}],r:0},{cN:"function",bK:"function",e:/\{/,eE:!0,c:[e.inherit(e.TM,{b:r}),{cN:"params",b:/\(/,e:/\)/,eB:!0,eE:!0,c:s}],i:/\[|%/},{b:/\$[(.]/},e.METHOD_GUARD,{cN:"class",bK:"class",e:/[{;=]/,eE:!0,i:/[:"\[\]]/,c:[{bK:"extends"},e.UTM]},{bK:"constructor",e:/\{/,eE:!0}],i:/#(?!!)/}});hljs.registerLanguage("ini",function(e){var b={cN:"string",c:[e.BE],v:[{b:"'''",e:"'''",r:10},{b:'"""',e:'"""',r:10},{b:'"',e:'"'},{b:"'",e:"'"}]};return{aliases:["toml"],cI:!0,i:/\S/,c:[e.C(";","$"),e.HCM,{cN:"section",b:/^\s*\[+/,e:/\]+/},{b:/^[a-z0-9\[\]_-]+\s*=\s*/,e:"$",rB:!0,c:[{cN:"attr",b:/[a-z0-9\[\]_-]+/},{b:/=/,eW:!0,r:0,c:[{cN:"literal",b:/\bon|off|true|false|yes|no\b/},{cN:"variable",v:[{b:/\$[\w\d"][\w\d_]*/},{b:/\$\{(.*?)}/}]},b,{cN:"number",b:/([\+\-]+)?[\d]+_[\d_]+/},e.NM]}]}]}});hljs.registerLanguage("diff",function(e){return{aliases:["patch"],c:[{cN:"meta",r:10,v:[{b:/^@@ +\-\d+,\d+ +\+\d+,\d+ +@@$/},{b:/^\*\*\* +\d+,\d+ +\*\*\*\*$/},{b:/^\-\-\- +\d+,\d+ +\-\-\-\-$/}]},{cN:"comment",v:[{b:/Index: /,e:/$/},{b:/={3,}/,e:/$/},{b:/^\-{3}/,e:/$/},{b:/^\*{3} /,e:/$/},{b:/^\+{3}/,e:/$/},{b:/\*{5}/,e:/\*{5}$/}]},{cN:"addition",b:"^\\+",e:"$"},{cN:"deletion",b:"^\\-",e:"$"},{cN:"addition",b:"^\\!",e:"$"}]}});hljs.registerLanguage("coffeescript",function(e){var c={keyword:"in if for while finally new do return else break catch instanceof throw try this switch continue typeof delete debugger super then unless until loop of by when and or is isnt not",literal:"true false null undefined yes no on off",built_in:"npm require console print module global window document"},n="[A-Za-z$_][0-9A-Za-z$_]*",r={cN:"subst",b:/#\{/,e:/}/,k:c},s=[e.BNM,e.inherit(e.CNM,{starts:{e:"(\\s*/)?",r:0}}),{cN:"string",v:[{b:/'''/,e:/'''/,c:[e.BE]},{b:/'/,e:/'/,c:[e.BE]},{b:/"""/,e:/"""/,c:[e.BE,r]},{b:/"/,e:/"/,c:[e.BE,r]}]},{cN:"regexp",v:[{b:"///",e:"///",c:[r,e.HCM]},{b:"//[gim]*",r:0},{b:/\/(?![ *])(\\\/|.)*?\/[gim]*(?=\W|$)/}]},{b:"@"+n},{b:"`",e:"`",eB:!0,eE:!0,sL:"javascript"}];r.c=s;var i=e.inherit(e.TM,{b:n}),t="(\\(.*\\))?\\s*\\B[-=]>",o={cN:"params",b:"\\([^\\(]",rB:!0,c:[{b:/\(/,e:/\)/,k:c,c:["self"].concat(s)}]};return{aliases:["coffee","cson","iced"],k:c,i:/\/\*/,c:s.concat([e.C("###","###"),e.HCM,{cN:"function",b:"^\\s*"+n+"\\s*=\\s*"+t,e:"[-=]>",rB:!0,c:[i,o]},{b:/[:\(,=]\s*/,r:0,c:[{cN:"function",b:t,e:"[-=]>",rB:!0,c:[o]}]},{cN:"class",bK:"class",e:"$",i:/[:="\[\]]/,c:[{bK:"extends",eW:!0,i:/[:="\[\]]/,c:[i]},i]},{b:n+":",e:":",rB:!0,rE:!0,r:0}])}});hljs.registerLanguage("objectivec",function(e){var t={cN:"built_in",b:"\\b(AV|CA|CF|CG|CI|CL|CM|CN|CT|MK|MP|MTK|MTL|NS|SCN|SK|UI|WK|XC)\\w+"},_={keyword:"int float while char export sizeof typedef const struct for union unsigned long volatile static bool mutable if do return goto void enum else break extern asm case short default double register explicit signed typename this switch continue wchar_t inline readonly assign readwrite self @synchronized id typeof nonatomic super unichar IBOutlet IBAction strong weak copy in out inout bycopy byref oneway __strong __weak __block __autoreleasing @private @protected @public @try @property @end @throw @catch @finally @autoreleasepool @synthesize @dynamic @selector @optional @required @encode @package @import @defs @compatibility_alias __bridge __bridge_transfer __bridge_retained __bridge_retain __covariant __contravariant __kindof _Nonnull _Nullable _Null_unspecified __FUNCTION__ __PRETTY_FUNCTION__ __attribute__ getter setter retain unsafe_unretained nonnull nullable null_unspecified null_resettable class instancetype NS_DESIGNATED_INITIALIZER NS_UNAVAILABLE NS_REQUIRES_SUPER NS_RETURNS_INNER_POINTER NS_INLINE NS_AVAILABLE NS_DEPRECATED NS_ENUM NS_OPTIONS NS_SWIFT_UNAVAILABLE NS_ASSUME_NONNULL_BEGIN NS_ASSUME_NONNULL_END NS_REFINED_FOR_SWIFT NS_SWIFT_NAME NS_SWIFT_NOTHROW NS_DURING NS_HANDLER NS_ENDHANDLER NS_VALUERETURN NS_VOIDRETURN",literal:"false true FALSE TRUE nil YES NO NULL",built_in:"BOOL dispatch_once_t dispatch_queue_t dispatch_sync dispatch_async dispatch_once"},i=/[a-zA-Z@][a-zA-Z0-9_]*/,n="@interface @class @protocol @implementation";return{aliases:["mm","objc","obj-c"],k:_,l:i,i:""}]}]},{cN:"class",b:"("+n.split(" ").join("|")+")\\b",e:"({|$)",eE:!0,k:n,l:i,c:[e.UTM]},{b:"\\."+e.UIR,r:0}]}});hljs.registerLanguage("http",function(e){var t="HTTP/[0-9\\.]+";return{aliases:["https"],i:"\\S",c:[{b:"^"+t,e:"$",c:[{cN:"number",b:"\\b\\d{3}\\b"}]},{b:"^[A-Z]+ (.*?) "+t+"$",rB:!0,e:"$",c:[{cN:"string",b:" ",e:" ",eB:!0,eE:!0},{b:t},{cN:"keyword",b:"[A-Z]+"}]},{cN:"attribute",b:"^\\w",e:": ",eE:!0,i:"\\n|\\s|=",starts:{e:"$",r:0}},{b:"\\n\\n",starts:{sL:[],eW:!0}}]}});hljs.registerLanguage("css",function(e){var c="[a-zA-Z-][a-zA-Z0-9_-]*",t={b:/[A-Z\_\.\-]+\s*:/,rB:!0,e:";",eW:!0,c:[{cN:"attribute",b:/\S/,e:":",eE:!0,starts:{eW:!0,eE:!0,c:[{b:/[\w-]+\(/,rB:!0,c:[{cN:"built_in",b:/[\w-]+/},{b:/\(/,e:/\)/,c:[e.ASM,e.QSM]}]},e.CSSNM,e.QSM,e.ASM,e.CBCM,{cN:"number",b:"#[0-9A-Fa-f]+"},{cN:"meta",b:"!important"}]}}]};return{cI:!0,i:/[=\/|'\$]/,c:[e.CBCM,{cN:"selector-id",b:/#[A-Za-z0-9_-]+/},{cN:"selector-class",b:/\.[A-Za-z0-9_-]+/},{cN:"selector-attr",b:/\[/,e:/\]/,i:"$"},{cN:"selector-pseudo",b:/:(:)?[a-zA-Z0-9\_\-\+\(\)"'.]+/},{b:"@(font-face|page)",l:"[a-z-]+",k:"font-face page"},{b:"@",e:"[{;]",i:/:/,c:[{cN:"keyword",b:/\w+/},{b:/\s/,eW:!0,eE:!0,r:0,c:[e.ASM,e.QSM,e.CSSNM]}]},{cN:"selector-tag",b:c,r:0},{b:"{",e:"}",i:/\S/,c:[e.CBCM,t]}]}});hljs.registerLanguage("java",function(e){var a="[À-ʸa-zA-Z_$][À-ʸa-zA-Z_$0-9]*",t=a+"(<"+a+"(\\s*,\\s*"+a+")*>)?",r="false synchronized int abstract float private char boolean static null if const for true while long strictfp finally protected import native final void enum else break transient catch instanceof byte super volatile case assert short package default double public try this switch continue throws protected public private module requires exports do",s="\\b(0[bB]([01]+[01_]+[01]+|[01]+)|0[xX]([a-fA-F0-9]+[a-fA-F0-9_]+[a-fA-F0-9]+|[a-fA-F0-9]+)|(([\\d]+[\\d_]+[\\d]+|[\\d]+)(\\.([\\d]+[\\d_]+[\\d]+|[\\d]+))?|\\.([\\d]+[\\d_]+[\\d]+|[\\d]+))([eE][-+]?\\d+)?)[lLfF]?",c={cN:"number",b:s,r:0};return{aliases:["jsp"],k:r,i:/<\/|#/,c:[e.C("/\\*\\*","\\*/",{r:0,c:[{b:/\w+@/,r:0},{cN:"doctag",b:"@[A-Za-z]+"}]}),e.CLCM,e.CBCM,e.ASM,e.QSM,{cN:"class",bK:"class interface",e:/[{;=]/,eE:!0,k:"class interface",i:/[:"\[\]]/,c:[{bK:"extends implements"},e.UTM]},{bK:"new throw return else",r:0},{cN:"function",b:"("+t+"\\s+)+"+e.UIR+"\\s*\\(",rB:!0,e:/[{;=]/,eE:!0,k:r,c:[{b:e.UIR+"\\s*\\(",rB:!0,r:0,c:[e.UTM]},{cN:"params",b:/\(/,e:/\)/,k:r,r:0,c:[e.ASM,e.QSM,e.CNM,e.CBCM]},e.CLCM,e.CBCM]},c,{cN:"meta",b:"@[A-Za-z]+"}]}});hljs.registerLanguage("bash",function(e){var t={cN:"variable",v:[{b:/\$[\w\d#@][\w\d_]*/},{b:/\$\{(.*?)}/}]},s={cN:"string",b:/"/,e:/"/,c:[e.BE,t,{cN:"variable",b:/\$\(/,e:/\)/,c:[e.BE]}]},a={cN:"string",b:/'/,e:/'/};return{aliases:["sh","zsh"],l:/-?[a-z\._]+/,k:{keyword:"if then else elif fi for while in do done case esac function",literal:"true false",built_in:"break cd continue eval exec exit export getopts hash pwd readonly return shift test times trap umask unset alias bind builtin caller command declare echo enable help let local logout mapfile printf read readarray source type typeset ulimit unalias set shopt autoload bg bindkey bye cap chdir clone comparguments compcall compctl compdescribe compfiles compgroups compquote comptags comptry compvalues dirs disable disown echotc echoti emulate fc fg float functions getcap getln history integer jobs kill limit log noglob popd print pushd pushln rehash sched setcap setopt stat suspend ttyctl unfunction unhash unlimit unsetopt vared wait whence where which zcompile zformat zftp zle zmodload zparseopts zprof zpty zregexparse zsocket zstyle ztcp",_:"-ne -eq -lt -gt -f -d -e -s -l -a"},c:[{cN:"meta",b:/^#![^\n]+sh\s*$/,r:10},{cN:"function",b:/\w[\w\d_]*\s*\(\s*\)\s*\{/,rB:!0,c:[e.inherit(e.TM,{b:/\w[\w\d_]*/})],r:0},e.HCM,s,a,t]}});hljs.registerLanguage("markdown",function(e){return{aliases:["md","mkdown","mkd"],c:[{cN:"section",v:[{b:"^#{1,6}",e:"$"},{b:"^.+?\\n[=-]{2,}$"}]},{b:"<",e:">",sL:"xml",r:0},{cN:"bullet",b:"^([*+-]|(\\d+\\.))\\s+"},{cN:"strong",b:"[*_]{2}.+?[*_]{2}"},{cN:"emphasis",v:[{b:"\\*.+?\\*"},{b:"_.+?_",r:0}]},{cN:"quote",b:"^>\\s+",e:"$"},{cN:"code",v:[{b:"^```w*s*$",e:"^```s*$"},{b:"`.+?`"},{b:"^( {4}| )",e:"$",r:0}]},{b:"^[-\\*]{3,}",e:"$"},{b:"\\[.+?\\][\\(\\[].*?[\\)\\]]",rB:!0,c:[{cN:"string",b:"\\[",e:"\\]",eB:!0,rE:!0,r:0},{cN:"link",b:"\\]\\(",e:"\\)",eB:!0,eE:!0},{cN:"symbol",b:"\\]\\[",e:"\\]",eB:!0,eE:!0}],r:10},{b:/^\[[^\n]+\]:/,rB:!0,c:[{cN:"symbol",b:/\[/,e:/\]/,eB:!0,eE:!0},{cN:"link",b:/:\s*/,e:/$/,eB:!0}]}]}});hljs.registerLanguage("apache",function(e){var r={cN:"number",b:"[\\$%]\\d+"};return{aliases:["apacheconf"],cI:!0,c:[e.HCM,{cN:"section",b:""},{cN:"attribute",b:/\w+/,r:0,k:{nomarkup:"order deny allow setenv rewriterule rewriteengine rewritecond documentroot sethandler errordocument loadmodule options header listen serverroot servername"},starts:{e:/$/,r:0,k:{literal:"on off all"},c:[{cN:"meta",b:"\\s\\[",e:"\\]$"},{cN:"variable",b:"[\\$%]\\{",e:"\\}",c:["self",r]},r,e.QSM]}}],i:/\S/}}); \ No newline at end of file diff --git a/public/partials/visualize/view.html b/public/partials/visualize/view.html index 90b75b1..d384954 100644 --- a/public/partials/visualize/view.html +++ b/public/partials/visualize/view.html @@ -23,7 +23,7 @@

Details of response

Headers

-
+

Body

From c05529323a71f9789cc64076974d7fed8290503b Mon Sep 17 00:00:00 2001 From: Cornic Mathieu Date: Tue, 8 Nov 2016 18:51:46 +0100 Subject: [PATCH 05/10] feat #6 : download and upload configuration of services and environments --- README.md | 35 +- .../app/controllers/admin/bulkController.js | 19 ++ app/assets/javascripts/app/services.js | 12 + app/assets/javascripts/soapower.js | 4 +- app/controllers/admin/BulkConfiguration.scala | 132 ++++++++ app/models/Environment.scala | 261 ++++++++++++--- app/models/Errors.scala | 7 + app/models/Service.scala | 305 ++++++++++++++++-- app/models/UtilNumbers.scala | 21 ++ conf/routes | 5 + public/index.html | 5 + public/javascripts/ng-upload/ng-upload.min.js | 1 + public/partials/admin/bulk/bulk.html | 45 +++ 13 files changed, 766 insertions(+), 86 deletions(-) create mode 100644 app/assets/javascripts/app/controllers/admin/bulkController.js create mode 100644 app/controllers/admin/BulkConfiguration.scala create mode 100644 app/models/Errors.scala create mode 100644 app/models/UtilNumbers.scala create mode 100644 public/javascripts/ng-upload/ng-upload.min.js create mode 100644 public/partials/admin/bulk/bulk.html diff --git a/README.md b/README.md index 454357c..6833028 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,33 @@ -Soapower - Monitoring Webservices -======== +# Soapower - Monitoring Webservices -What it is and how to install it ? -Go to http://soapower.github.io/soapower/ +Soapower provides a GUI for -Download & Release Notes -============= +- viewing webservices requests (live and search page), +- download the data from the request and response, +- getting response time, +- viewing 90percentiles response times, etc ... +- Soapower allows monitoring several applications across multiple environments. + +It is also possible to set a threshold response time on soapaction, the alerts are rising if the receiver does not send the response back in time. + +Administration interface is available for + +- Configure environments and webservices (local / remote target / timeout) +- Set the thresholds for response time for each serviceaction +- Import / export service configurations & settings +- Monitoring CPU / Heap Memory / logs file + +## Getting started + +- [Install activator](http://www.lightbend.com/community/core-tools/activator-and-sbt) +- Run : `activator -jvm-debug 9999 run` + +I you have to connect to a proxy, don't forget to add proxy options to activator command : `-Dhttp.proxyHost=XXX -Dhttp.proxyPort=YYYY -Dhttp.nonProxyHosts="localhost|127.0.0.01"` + +# Download & Release Notes See https://github.com/soapower/soapower/releases -Licence -======= +# Licence + This software is licensed under the GNU GENERAL PUBLIC LICENSE V3. See LICENSE file. diff --git a/app/assets/javascripts/app/controllers/admin/bulkController.js b/app/assets/javascripts/app/controllers/admin/bulkController.js new file mode 100644 index 0000000..71e3e9d --- /dev/null +++ b/app/assets/javascripts/app/controllers/admin/bulkController.js @@ -0,0 +1,19 @@ +function BulkCtrl($scope, $rootScope, $location, ConfigurationService) { + $scope.downloadConfigurationURL = "/bulk/downloadConfiguration"; + $scope.uploadConfigurationURL = "/bulk/uploadConfiguration"; + $scope.showResponseUpload = false; + $scope.showUploadRunning = false; + $scope.uploadCompleted = function (content) { + $scope.response = content; + $scope.showUploadRunning = false; + $scope.showResponseUpload = true; + }; + + ConfigurationService.configurationExample(). + success(function (data) { + $scope.exampleCode = data; + }) + .error(function (resp) { + console.log("Error with ConfigurationService.configurationExample" + resp); + }); +} \ No newline at end of file diff --git a/app/assets/javascripts/app/services.js b/app/assets/javascripts/app/services.js index 36a32ef..5fad3c8 100644 --- a/app/assets/javascripts/app/services.js +++ b/app/assets/javascripts/app/services.js @@ -22,6 +22,18 @@ spApp.factory("GroupsService", function ($http) { } }); +/*************************************** + * CONFIGURATION + ***************************************/ + +spApp.factory("ConfigurationService", function ($http) { + return { + configurationExample: function () { + return $http.get('/bulk/configurationExample'); + } + } +}); + /*************************************** * ENVIRONMENTS ***************************************/ diff --git a/app/assets/javascripts/soapower.js b/app/assets/javascripts/soapower.js index c9e9a1e..e2a8944 100644 --- a/app/assets/javascripts/soapower.js +++ b/app/assets/javascripts/soapower.js @@ -2,7 +2,7 @@ 'use strict'; -var spApp = angular.module('spApp', [ 'ui.bootstrap', 'ngRoute', 'ngResource', 'ngTable', 'ui.bootstrap.datetimepicker', 'ui.select2', 'hljs']); +var spApp = angular.module('spApp', [ 'ui.bootstrap', 'ngRoute', 'ngResource', 'ngTable', 'ui.bootstrap.datetimepicker', 'ui.select2', 'hljs', 'ngUpload']); spApp.config(function ($routeProvider) { $routeProvider @@ -37,6 +37,8 @@ spApp.config(function ($routeProvider) { .when('/services/edit/:environmentName/:serviceId/:groups', {controller: ServiceEditCtrl, templateUrl: 'partials/admin/services/detail.html'}) .when('/services/list/:environmentName/:groups', { controller: ServicesCtrl, templateUrl: 'partials/admin/services/list.html'}) + .when('/bulk', { controller: BulkCtrl, templateUrl: 'partials/admin/bulk/bulk.html'}) + .when('/environments', { redirectTo: '/environments/list/all'}) .when('/environments/new/:groups', {controller: EnvironmentNewCtrl, templateUrl: 'partials/admin/environments/detail.html'}) .when('/environments/edit/:environmentId/:groups', {controller: EnvironmentEditCtrl, templateUrl: 'partials/admin/environments/detail.html'}) diff --git a/app/controllers/admin/BulkConfiguration.scala b/app/controllers/admin/BulkConfiguration.scala new file mode 100644 index 0000000..bb787e2 --- /dev/null +++ b/app/controllers/admin/BulkConfiguration.scala @@ -0,0 +1,132 @@ +package controllers.admin + +import play.api.mvc._ +import models.{ErrorUploadCsv, _} +import play.api.Logger +import play.api.libs.iteratee._ +import play.api.http._ +import play.api.libs.json.Json + +import scala.collection.mutable.ListBuffer + +/** + *BulkConfiguration controller is the endpoint of routes where admin can configure Soapower application by bulk batches + */ +object BulkConfiguration extends Controller { + /** + * Convert an message object to a Json Object + * @param code the code of the message + * @param text the message to send + * @param data the metadata for the message + * @return A Json representation of the message + */ + def toJson(code: String, text: String, data: List[String] = List()) = Json.obj("code" -> code, "text" -> text, "data" -> data) + + /** + * Upload the configuration from a file (body field fileUploaded) + * @return + */ + def uploadConfiguration = Action(parse.multipartFormData) { + request => + // Clear caches, as we will create new services/environments and update potentially all services/environments + Service.clearCache + Environment.clearCache + // Get the file + request.body.file("fileUploaded").map { + fileUploaded => + import scala.io._ + var errors = new ListBuffer[ErrorUploadCsv]() // Errors that may happen on line parsing are appended + var linesNumber: Int = 1 // Counter of lines in file + var effectiveLines: Int = 0 // Counter of effective lines in file (i.e. comments are not processed) + var linesUploaded: Int = 0 // Counter of lines successfully uploaded + + // Parse the file, line by line + for (line <- Source.fromFile(fileUploaded.ref.file).getLines()) { + try { + val res = if (line.startsWith(Service.csvKey)) { + Logger.info("Uploading service: " + line) + effectiveLines += 1 + Some(Service.uploadCSV(line)) + } else if (line.startsWith(Environment.csvKey)) { + Logger.info("Uploading environment: " + line) + effectiveLines += 1 + Some(Environment.uploadCSV(line)) + } else { + None + } + + res match { + case Some(Left(error)) => { + Logger.warn(s"Failed upload of line ${linesNumber}: ${error.msg}") + errors += ErrorUploadCsv(s"Line ${linesNumber}: ${error.msg}") + } + case Some(Right(result)) => { + linesUploaded += 1 + Logger.info("Uploaded : " + line) + } + case None => { + Logger.debug(s"Ignoring not recognized line ${line}") + } + } + } catch { + case e: Exception => { + Logger.warn(s"Failed upload line ${linesNumber}: ${e.getMessage}") + errors += ErrorUploadCsv(s"Line ${linesNumber}: ${e.getMessage}") + } + } finally { + linesNumber += 1 + } + } + + if (!errors.isEmpty) { + Ok(toJson( + "warning", + s"Warning ! Configuration uploaded partially (${linesUploaded}/${effectiveLines} lines uploaded). Fix the errors and reupload the file.", + errors.map(e => e.msg).toList + )).as(JSON) + } else { + Ok(toJson("success", "Success ! Every line of configuration is uploaded.")).as(JSON) + } + }.getOrElse { + Ok(toJson("danger", "Error ! Uploading configuration is not available right now. See with an administrator.")).as(JSON) + } + } + + /** + * Download the configuration as a file. File exported can be uploaded afterwards + * @return + */ + def downloadConfiguration = Action { + // data + var content = "" + content += Environment.fetchCsvHeader() + "\n" + content += Service.fetchCsvHeader() + "\n" + Environment.fetchCsv().foreach { s => content += s + "\n"} + Service.fetchCsv().foreach { s => content += s + "\n"} + + // result as a file + val fileContent = Enumerator(content.getBytes()) + Result( + header = ResponseHeader(play.api.http.Status.OK), + body = fileContent + ).withHeaders((HeaderNames.CONTENT_DISPOSITION, "attachment; filename=configuration.csv")).as(BINARY) + } + + /** + * Print a sample code of file configuration + * Explains different columns + * @return + */ + def configurationExample = Action { + var content = "#Example for key \"" + Environment.csvKey + "\"\n#" + content += Environment.fetchCsvHeader() + "\n" + content += Environment(None, "env1", List("group1", "newGroup2")).toCSV() + "\n" + content += "#Example for key \"" + Service.csvKey + "\"\n#" + content += Service.fetchCsvHeader() + "\n" + content += Service(None, "A simple desc", "REST", "GET", "localTarget", "http://target:port/remote", 1000, true, true, false, None, Some("env1")).toCSV() + "\n" + Ok(content) + } + + + +} diff --git a/app/models/Environment.scala b/app/models/Environment.scala index 5fd5fac..372919b 100644 --- a/app/models/Environment.scala +++ b/app/models/Environment.scala @@ -2,19 +2,20 @@ package models import play.api.Play.current import play.api.cache._ - import java.util.{Calendar, GregorianCalendar} + import play.modules.reactivemongo.ReactiveMongoPlugin import play.api.libs.json._ -import reactivemongo.api.indexes.{IndexType, Index} +import reactivemongo.api.indexes.{Index, IndexType} import reactivemongo.bson._ + import scala.concurrent.{Await, Future} -import play.modules.reactivemongo.json.BSONFormats._ import scala.concurrent.duration._ import play.api.libs.concurrent.Execution.Implicits.defaultContext import reactivemongo.core.commands.RawCommand import play.api.Logger import reactivemongo.api.collections.default.BSONCollection +import play.modules.reactivemongo.json.BSONFormats._ case class Environment(_id: Option[BSONObjectID], name: String, @@ -24,13 +25,35 @@ case class Environment(_id: Option[BSONObjectID], nbDayKeepContentData: Int = 2, nbDayKeepAllData: Int = 5, recordContentData: Boolean = true, - recordData: Boolean = true) + recordData: Boolean = true) { + /** + * Converts an environment to a CSV line + * + * @return + */ + def toCSV(): String = { + val columns = Environment.csvTitle.map { + case Environment.csvKey => Environment.csvKey + case "id" => if (_id.isEmpty) "" else _id.get.stringify + case "name" => name + case "groupsName" => groups.mkString(";") + case "hourRecordContentDataMin" => hourRecordContentDataMin.toString + case "hourRecordContentDataMax" => hourRecordContentDataMax.toString + case "nbDayKeepContentData" => nbDayKeepContentData.toString + case "nbDayKeepAllData" => nbDayKeepAllData.toString + case "recordData" => recordData.toString + case "recordContentData" => recordContentData.toString + } + columns.mkString(",") + } +} object ModePurge extends Enumeration { type ModePurge = Value val CONTENT, ALL = Value } + object Environment { /* @@ -81,8 +104,145 @@ object Environment { private val ENVIRONMENT_NAME_PATTERN = "[a-zA-Z0-9]{1,200}" /** - * Sort the given env option seq - */ + * Identity key for CSV file + */ + val csvKey = "environment" + + /** + * Header of csvFile. Defines the column name and order. + */ + val csvTitle = List( + csvKey, "id", "groupsName", "name", "hourRecordContentDataMin", + "hourRecordContentDataMax", "nbDayKeepContentData", "nbDayKeepAllData", + "recordContentData", "recordData" + ) + + def fetchCsvHeader(): String = { + "#" + Environment.csvTitle.mkString(",") + } + + /** + * Get All environements, csv format. + * + * @return List of Environements, csv format + */ + def fetchCsv(): List[String] = { + val f = findAll.map(environments => environments.map(env => env.toCSV())) + Await result(f, 5.seconds) + } + + /** + * Upload a csvLine => insert environment. + * + * @param csvLine line in csv file + * @return nothing + */ + def uploadCSV(csvLine: String): Either[ErrorUploadCsv, Boolean] = { + + val dataCsv = csvLine.split(",") + + if (dataCsv.size != csvTitle.size) { + throw new Exception("Please check csvFile, " + csvTitle.size + " fields required") + } + + if (dataCsv(0) == csvKey) { + val uploadFuture = uploadEnvironment(dataCsv) + Right(Await.result(uploadFuture, 10.seconds)) + } else { + Left(ErrorUploadCsv(s"First column ${dataCsv(0)} is not recognized as ${csvKey} ")) + } + } + + /** + * Check if environment already exist (with same name). Insert or do nothing if exist. + * + * @param dataCsv line in csv file + * @return environment (new or not) + */ + private def uploadEnvironment(dataCsv: Array[String]): Future[Boolean] = { + + def insertEnvironment: Future[Boolean] = { + // Insert the service by generating the new id + val env = getEnvironmentFromCSV(dataCsv).copy(_id = Some(BSONObjectID.generate)) + val insert = Environment.insert(env) + insert.map { + case res => Logger.info(s"Created new environment ${env.name}") + true + } + } + def updateEnvironment(envToUpdate: Environment): Future[Boolean] = { + val env = getEnvironmentFromCSV(dataCsv).copy(_id = envToUpdate._id) + val u = Environment.update(env) + u.map { + case res => { + Logger.info(s"Updated existing environment ${env.name} (id=${env._id})") + true + } + } + } + + // Find the environment from id if present + // Search by name if not + val id = dataCsv(csvTitle.indexOf("id")) + val potentialEnvironmentF = if (id.isEmpty) { + findByName(dataCsv(csvTitle.indexOf("name")), cached = false) + } else { + findById(BSONObjectID(id)) + } + + potentialEnvironmentF.flatMap { + case Some(e) => { + if (e == null) insertEnvironment else updateEnvironment(e) + } + case None => { + // Create a new environment + insertEnvironment + } + } + + } + + /** + * Create an Environment object model from a csvLine + * Send exception when semantic and/or syntax are not valid + * + * @param dataCsv + * @return + */ + private def getEnvironmentFromCSV(dataCsv: Array[String]) = { + val idRaw = dataCsv(csvTitle.indexOf("id")) + val id = if (idRaw.trim.isEmpty) None else Some(BSONObjectID(idRaw)) + + val nameRaw = dataCsv(csvTitle.indexOf("name")) + val name = if (nameRaw.trim.isEmpty) throw new Exception("name is required") else nameRaw.trim + + val groupsNameRaw = dataCsv(csvTitle.indexOf("groupsName")) + val groups = groupsNameRaw.split(";").map(group => group.trim).toSet.toList + + val hourRecordContentDataMinRaw = dataCsv(csvTitle.indexOf("hourRecordContentDataMin")) + val hourRecordContentDataMin = UtilNumbers.toInt(hourRecordContentDataMinRaw.trim).getOrElse(8) + + val hourRecordContentDataMaxRaw = dataCsv(csvTitle.indexOf("hourRecordContentDataMax")) + val hourRecordContentDataMax = UtilNumbers.toInt(hourRecordContentDataMaxRaw.trim).getOrElse(22) + + val nbDayKeepContentDataRaw = dataCsv(csvTitle.indexOf("nbDayKeepContentData")) + val nbDayKeepContentData = UtilNumbers.toInt(nbDayKeepContentDataRaw.trim).getOrElse(2) + + val nbDayKeepAllDataRaw = dataCsv(csvTitle.indexOf("nbDayKeepAllData")) + val nbDayKeepAllData = UtilNumbers.toInt(nbDayKeepAllDataRaw.trim).getOrElse(5) + + val recordContentDataRaw = dataCsv(csvTitle.indexOf("recordContentData")) + val recordContentData = recordContentDataRaw.trim == "true" + + val recordDataRaw = dataCsv(csvTitle.indexOf("recordData")) + val recordData = recordDataRaw.trim == "true" + + Environment(id, name, groups, hourRecordContentDataMin, hourRecordContentDataMax, nbDayKeepContentData, nbDayKeepAllData, recordContentData, recordData) + } + + /** + * Sort the given env option seq + */ private def sortEnvs(envs: Seq[(String, String)]): Seq[(String, String)] = { val sortedEnvs = envs.sortWith { (a, b) => @@ -123,28 +283,36 @@ object Environment { } /** - * Retrieve an Environment from id. - */ + * Retrieve an Environment from id. + */ def findById(objectId: BSONObjectID): Future[Option[Environment]] = { val query = BSONDocument("_id" -> objectId) collection.find(query).one[Environment] } /** - * Retrieve an Environment from name. - */ - def findByName(name: String): Future[Option[Environment]] = { - Cache.getOrElse(keyCacheByName + name) { + * Retrieve an Environment from name. + */ + def findByName(name: String, cached: Boolean = true): Future[Option[Environment]] = { + def find: Future[Option[Environment]] = { val query = BSONDocument("name" -> name) collection.find(query).one[Environment] } + if (cached) { + Cache.getOrElse(keyCacheByName + name) { + find + } + } else { + find + } + } /** - * Insert a new environment. - * - * @param environment The environment values. - */ + * Insert a new environment. + * + * @param environment The environment values. + */ def insert(environment: Environment) = { if (!environment.name.trim.matches(ENVIRONMENT_NAME_PATTERN)) { throw new Exception("Environment name invalid:" + environment.name.trim) @@ -159,10 +327,10 @@ object Environment { } /** - * Update a environment. - * - * @param environment The environment values. - */ + * Update a environment. + * + * @param environment The environment values. + */ def update(environment: Environment) = { if (!environment.name.trim.matches(ENVIRONMENT_NAME_PATTERN)) { throw new Exception("Environment name invalid:" + environment.name.trim) @@ -191,10 +359,10 @@ object Environment { } /** - * Delete a environment. - * - * @param id Id of the environment to delete. - */ + * Delete a environment. + * + * @param id Id of the environment to delete. + */ def delete(id: String) = { val objectId = BSONObjectID.apply(id) clearCache() @@ -207,8 +375,8 @@ object Environment { } /** - * Return a list of all environments. - */ + * Return a list of all environments. + */ def findAll: Future[List[Environment]] = { collection. find(BSONDocument()). @@ -218,8 +386,8 @@ object Environment { } /** - * Return a list of all environments in some groups. - */ + * Return a list of all environments in some groups. + */ def findInGroups(groups: String): Future[List[Environment]] = { if ("all".equals(groups)) { return findAll @@ -233,18 +401,19 @@ object Environment { } /** - * Construct the Map[String,String] needed to fill a select options set. - */ + * Construct the Map[String,String] needed to fill a select options set. + */ def options = { Cache.getOrElse(keyCacheAllOptions) { val f = findAll.map(environments => environments.map(e => (e._id.get.stringify, e.name))) sortEnvs(Await result(f, 5.seconds)) } + } /** - * Construct the Map[String,String] needed to fill a select options set for selected groups. - */ + * Construct the Map[String,String] needed to fill a select options set for selected groups. + */ def optionsInGroups(groups: String) = { if ("all".equals(groups)) { options @@ -255,33 +424,35 @@ object Environment { } /** - * Find all distinct groups in environments collections. - * - * @return all distinct groups - */ + * Find all distinct groups in environments collections. + * + * @return all distinct groups + */ def findAllGroups(): Future[BSONDocument] = { val command = RawCommand(BSONDocument("distinct" -> "environments", "key" -> "groups")) collection.db.command(command) // result is Future[BSONDocument] } /** - * Find an environment using his name and retrieve it if the groups in parameters match the environment groups - * @param name name of environment - * @param groups groups, separated by ',', example group1,group2... - * @return - */ + * Find an environment using his name and retrieve it if the groups in parameters match the environment groups + * + * @param name name of environment + * @param groups groups, separated by ',', example group1,group2... + * @return + */ def findByNameAndGroups(name: String, groups: String): Future[Option[Environment]] = { val find = BSONDocument("name" -> name, "groups" -> BSONDocument("$in" -> groups.split(','))) collection.find(find).one[Environment] } /** - * Foreach environment, retrieve his name and his groups - * @return - */ + * Foreach environment, retrieve his name and his groups + * + * @return + */ def findNamesAndGroups(): List[(String, List[String])] = { val query = collection.find(BSONDocument()).cursor[Environment].collect[List]().map { - list => list.map { envir => (envir.name, envir.groups)} + list => list.map { envir => (envir.name, envir.groups) } } Await.result(query, 1.second) } diff --git a/app/models/Errors.scala b/app/models/Errors.scala new file mode 100644 index 0000000..f9bef01 --- /dev/null +++ b/app/models/Errors.scala @@ -0,0 +1,7 @@ +package models + +/** + * Error when uploading CSV + * @param msg the error message + */ +case class ErrorUploadCsv(msg: String) diff --git a/app/models/Service.scala b/app/models/Service.scala index 0fb3c2c..b033b80 100644 --- a/app/models/Service.scala +++ b/app/models/Service.scala @@ -3,14 +3,17 @@ package models import play.api.Play.current import play.api.cache.Cache import reactivemongo.bson._ -import play.modules.reactivemongo.json.BSONFormats._ import play.api.libs.concurrent.Execution.Implicits.defaultContext -import scala.concurrent.Future +import play.modules.reactivemongo.json.BSONFormats._ + +import scala.concurrent.{Await, Future} +import scala.concurrent.duration._ import play.api.libs.json.Json import reactivemongo.bson.BSONBoolean import reactivemongo.bson.BSONString import reactivemongo.bson.BSONInteger import org.jboss.netty.handler.codec.http.HttpMethod +import play.api.Logger case class Service(_id: Option[BSONObjectID], description: String, @@ -39,6 +42,31 @@ case class Service(_id: Option[BSONObjectID], serviceDoc.getAs[Boolean]("useMockGroup").get, serviceDoc.getAs[String]("mockGroupId"), environmentName) + + /** + * Converts an service to a CSV line + * + * @return + */ + def toCSV(): String = { + + val columns = Service.csvTitle.map { + case Service.csvKey => Service.csvKey + case "id" => if (_id.isEmpty) "" else _id.get.stringify + case "description" => description.replaceAll(",", "") + case "typeRequest" => typeRequest + case "httpMethod" => httpMethod + case "localTarget" => localTarget + case "remoteTarget" => remoteTarget + case "timeoutms" => timeoutms.toString + case "recordContentData" => recordContentData.toString + case "recordData" => recordData.toString + case "useMockGroup" => useMockGroup.toString + case "mockGroupId" => mockGroupId.getOrElse("") + case "environmentName" => environmentName.getOrElse("") + } + columns.mkString(",") + } } @@ -51,6 +79,10 @@ object Service { private val keyCacheRequest = "cacheServiceRequest-" + def clearCache() { + Cache.remove(keyCacheRequest) + } + implicit object ServicesBSONReader extends BSONDocumentReader[Services] { def read(doc: BSONDocument): Services = { if (doc.getAs[List[BSONDocument]]("services").isDefined) { @@ -64,9 +96,216 @@ object Service { } } + + /** + * Identity key for CSV file + */ + val csvKey = "service"; + + /** + * Header of csvFile. Defines the column name and order. + */ + val csvTitle = List( + csvKey, "id", "description", "environmentName", "typeRequest", "httpMethod", + "localTarget", "remoteTarget", "useMockGroup", "mockGroupId", + "timeoutms", "recordContentData", "recordData" + ) + + /** + * Get all services, csv format. + * + * @return List of services, csv format + */ + def fetchCsv(): List[String] = { + val f = findAll.map(service => service.map(s => s.toCSV())) + Await result(f, 5.seconds) + } + + /** + * Get the header as a csv string + * + * @return + */ + def fetchCsvHeader(): String = { + "#" + Service.csvTitle.mkString(",") + } + + /** + * Upload a csvLine => insert service & potential environment. + * + * @param csvLine line in csv file + * @return nothing + */ + def uploadCSV(csvLine: String): Either[ErrorUploadCsv, Boolean] = { + + val dataCsv = csvLine.split(",") + + if (dataCsv.size != csvTitle.size) + throw new Exception("Please check csvFile, " + csvTitle.size + " fields required") + + if (dataCsv(0) == csvKey) { + val uploadFuture = uploadEnvironment(dataCsv).flatMap { + uploaded => uploadService(dataCsv) + } + Right(Await.result(uploadFuture, 10.seconds)) + } else { + Left(ErrorUploadCsv(s"First column ${dataCsv(0)} is not recognized as ${csvKey} ")) + } + + } + + /** + * Upload a new Environment from the service + * + * @param dataCsv + */ + private def uploadEnvironment(dataCsv: Array[String]): Future[Boolean] = { + + val envS = dataCsv(csvTitle.indexOf("environmentName")).trim + val localTarget = dataCsv(csvTitle.indexOf("localTarget")) + if (envS.isEmpty) { + throw new Exception(s"environmentName is mandatory when uploading the service ${localTarget}.") + } + + def insertEnvironment(env: Environment, localTarget: String): Future[Boolean] = { + val u = Environment.insert(env) + u.map { + case res => { + Logger.info(s"Created new default environment ${env.name} for service ${localTarget}") + true + } + } + } + + // Search the environment by its name + val potentialEnvironmentF = Environment.findByName(envS, cached = false) + potentialEnvironmentF.flatMap { + case Some(e) => { + // Environment exists, so we do nothing because we don't have any other info to update + Logger.info(s"Environment ${e.name} exists for service ${localTarget}") + Future.successful(true) + } + case None => { + // Create a default environment with name + Logger.info(s"Environment ${envS} does not exist for service ${localTarget}") + val env = new Environment( + None, + envS, + List() + ) + insertEnvironment(env, localTarget) + } + } + } + + + /** + * Check if service already exist (with localTarget and Environment). Insert or update if exist. + * + * @param dataCsv line in csv file + * @return service (new or not) + */ + private def uploadService(dataCsv: Array[String]): Future[Boolean] = { + + val id = dataCsv(csvTitle.indexOf("id")) + val typeRequest = dataCsv(csvTitle.indexOf("typeRequest")) + val localTarget = dataCsv(csvTitle.indexOf("localTarget")) + val environmentName = dataCsv(csvTitle.indexOf("environmentName")) + val httpMethod = dataCsv(csvTitle.indexOf("httpMethod")) + val potentialServiceF = if (id.isEmpty) { + findByLocalTargetAndEnvironmentName(typeRequest, localTarget, environmentName, HttpMethod.valueOf(httpMethod)) + } else { + findById(dataCsv(csvTitle.indexOf("environmentName")), id) + } + + def insertService: Future[Boolean] = { + // Insert the service by generating the new id + val service = getServiceFromCSV(dataCsv).copy(_id = Some(BSONObjectID.generate)) + val insert = Service.insert(service) + insert.map { + case res => Logger.info(s"Created new service ${localTarget} for ${environmentName} and ${typeRequest}/${httpMethod}") + true + } + } + def updateService(serviceToUpdate: Service): Future[Boolean] = { + val service = getServiceFromCSV(dataCsv).copy(_id = serviceToUpdate._id) + val u = Service.update(service) + u.map { + case res => { + Logger.info(s"Updated existing service ${localTarget} for ${environmentName} and ${typeRequest}/${httpMethod}") + true + } + } + } + + potentialServiceF.flatMap { + case Some(e) => { + if (e == null) insertService else updateService(e) + } + case None => { + // Create a new service + insertService + } + } + } + + + /** + * Get a service object from a csv structured line. + * Check syntax and semantic of each column + * + * @param dataCsv + * @return + */ + private def getServiceFromCSV(dataCsv: Array[String]) = { + val idRaw = dataCsv(csvTitle.indexOf("id")) + val id = if (idRaw.trim.isEmpty) None else Some(BSONObjectID(idRaw)) + + val descriptionRaw = dataCsv(csvTitle.indexOf("description")) + val description = descriptionRaw.trim + + val environmentNameRaw = dataCsv(csvTitle.indexOf("environmentName")) + val environmentName = if (environmentNameRaw.trim.isEmpty) throw new Exception("environmentName is required") + else Some(environmentNameRaw.trim) + + val typeRequestRaw = dataCsv(csvTitle.indexOf("typeRequest")) + val typeRequest = if (Set(REST, SOAP).contains(typeRequestRaw.trim)) typeRequestRaw.trim + else throw new Exception(s"typeRequest should be either ${REST} or ${SOAP}") + + val httpMethodRaw = dataCsv(csvTitle.indexOf("httpMethod")) + val httpMethod = if (httpMethodRaw.trim.isEmpty) throw new Exception("httpMethod is required") + else httpMethodRaw.trim + + val localTargetRaw = dataCsv(csvTitle.indexOf("localTarget")) + val localTarget = if (localTargetRaw.trim.isEmpty) throw new Exception("localTarget is required") + else localTargetRaw.trim + + val remoteTargetRaw = dataCsv(csvTitle.indexOf("remoteTarget")) + val remoteTarget = if (remoteTargetRaw.trim.isEmpty) throw new Exception("remoteTarget is required") + else remoteTargetRaw.trim + + val useMockGroupRaw = dataCsv(csvTitle.indexOf("useMockGroup")) + val useMockGroup = useMockGroupRaw.trim == "true" + + val mockGroupIdRaw = dataCsv(csvTitle.indexOf("mockGroupId")) + val mockGroupId = if (mockGroupIdRaw.trim.isEmpty) None else Some(mockGroupIdRaw.trim) + + val timeoutmsRaw = dataCsv(csvTitle.indexOf("timeoutms")) + val timeoutms = UtilNumbers.toInt(timeoutmsRaw.trim).getOrElse(5000) + + val recordContentDataRaw = dataCsv(csvTitle.indexOf("recordContentData")) + val recordContentData = recordContentDataRaw.trim == "true" + + val recordDataRaw = dataCsv(csvTitle.indexOf("recordData")) + val recordData = recordDataRaw.trim == "true" + + Service(id, description, typeRequest, httpMethod, localTarget, remoteTarget, timeoutms, recordContentData, recordData, useMockGroup, mockGroupId, environmentName) + } + + /** - * Services - */ + * Services + */ val REST = "REST" val SOAP = "SOAP" @@ -98,11 +337,12 @@ object Service { } /** - * Retrieve a Service. - * @param environmentName Name of environement - * @param serviceId ObjectID of service - * @return Option of service - */ + * Retrieve a Service. + * + * @param environmentName Name of environement + * @param serviceId ObjectID of service + * @return Option of service + */ def findById(environmentName: String, serviceId: String): Future[Option[Service]] = { val query = BSONDocument("name" -> environmentName) val projection = BSONDocument("name" -> 1, "groups" -> 1, "services" -> BSONDocument( @@ -111,12 +351,12 @@ object Service { } /** - * Retrieve a Soap Service from localTarget / environmentName - * - * @param localTarget localTarget - * @param environmentName Name of environment - * @return service - */ + * Retrieve a Soap Service from localTarget / environmentName + * + * @param localTarget localTarget + * @param environmentName Name of environment + * @return service + */ def findByLocalTargetAndEnvironmentName(typeRequest: String, localTarget: String, environmentName: String, httpMethod: HttpMethod): Future[Option[Service]] = { Cache.getOrElse(keyCacheRequest + typeRequest + localTarget + environmentName + httpMethod.toString, 15) { val query = BSONDocument("name" -> environmentName) @@ -130,10 +370,10 @@ object Service { } /** - * Insert a new service. - * - * @param service The service values - */ + * Insert a new service. + * + * @param service The service values + */ def insert(service: Service) = { val selectorEnv = BSONDocument("name" -> service.environmentName) val insert = BSONDocument("$push" -> BSONDocument("services" -> service)) @@ -141,10 +381,10 @@ object Service { } /** - * Update a service. - * - * @param service The service values. - */ + * Update a service. + * + * @param service The service values. + */ def update(service: Service) = { val selector = BSONDocument( "name" -> service.environmentName, @@ -155,11 +395,12 @@ object Service { } /** - * Delete a service. - * @param environmentName environment name wich contains the service - * @param serviceId id of the service to delete - * @return - */ + * Delete a service. + * + * @param environmentName environment name wich contains the service + * @param serviceId id of the service to delete + * @return + */ def delete(environmentName: String, serviceId: String) = { val selector = BSONDocument("name" -> environmentName) val update = BSONDocument("$pull" -> BSONDocument("services" -> BSONDocument("_id" -> BSONObjectID(serviceId)))) @@ -172,16 +413,16 @@ object Service { } /** - * Return a list of all services. - */ + * Return a list of all services. + */ def findAll: Future[List[Service]] = { val query = BSONDocument() Environment.collection.find(query).cursor[Services].collect[List]().map(l => l.flatMap(s => s.services)) } /** - * Remove first / in localTarget. - */ + * Remove first / in localTarget. + */ private def checkLocalTarget(localTarget: String) = { if (localTarget.startsWith("/")) localTarget.substring(1) else localTarget } diff --git a/app/models/UtilNumbers.scala b/app/models/UtilNumbers.scala new file mode 100644 index 0000000..e8ef002 --- /dev/null +++ b/app/models/UtilNumbers.scala @@ -0,0 +1,21 @@ +package models + +/** + * Contains util functions around numbers. + */ +object UtilNumbers { + + /** + * Convert string to optional int. + * If s cannot be converted to Int, this method returns None + * @param s the string to convert + * @return + */ + def toInt(s: String): Option[Int] = { + try { + Some(s.toInt) + } catch { + case e: Exception => None + } + } +} diff --git a/conf/routes b/conf/routes index bf45766..8c567e3 100644 --- a/conf/routes +++ b/conf/routes @@ -51,6 +51,11 @@ DELETE /environments/:id # Groups GET /groups/findAll controllers.admin.Environments.findAllGroups +# Bulk configuration +GET /bulk/downloadConfiguration controllers.admin.BulkConfiguration.downloadConfiguration +GET /bulk/configurationExample controllers.admin.BulkConfiguration.configurationExample +POST /bulk/uploadConfiguration controllers.admin.BulkConfiguration.uploadConfiguration + # Mock Groups GET /mockgroups/:group/findall controllers.admin.MockGroups.findAll(group:String) POST /mockgroups controllers.admin.MockGroups.create diff --git a/public/index.html b/public/index.html index e192e1b..b51d1a7 100644 --- a/public/index.html +++ b/public/index.html @@ -72,6 +72,9 @@
  • Mockings
  • +
  • + Bulk Operations +
  • Thresholds @@ -103,6 +106,7 @@ + @@ -123,6 +127,7 @@ + diff --git a/public/javascripts/ng-upload/ng-upload.min.js b/public/javascripts/ng-upload/ng-upload.min.js new file mode 100644 index 0000000..92fc2a5 --- /dev/null +++ b/public/javascripts/ng-upload/ng-upload.min.js @@ -0,0 +1 @@ +angular.module("ngUpload",[]).directive("uploadSubmit",["$parse",function(){function n(t,e){t=angular.element(t);var a=t.parent();return e=e.toLowerCase(),a&&a[0].tagName.toLowerCase()===e?a:a?n(a,e):null}return{restrict:"AC",link:function(t,e){e.bind("click",function(t){if(t&&(t.preventDefault(),t.stopPropagation()),!e.attr("disabled")){var a=n(e,"form");a.triggerHandler("submit"),a[0].submit()}})}}}]).directive("ngUpload",["$log","$parse","$document",function(n,t,e){function a(n){var t,a=e.find("head");return angular.forEach(a.find("meta"),function(e){e.getAttribute("name")===n&&(t=e)}),angular.element(t)}var r=1;return{restrict:"AC",link:function(e,o,i){function l(n){e.$isUploading=n}function p(){c.unbind("load"),e.$$phase?l(!1):e.$apply(function(){l(!1)});try{var t,a=(c[0].contentDocument||c[0].contentWindow.document).body;try{t=angular.fromJson(a.innerText||a.textContent),e.$$phase?d(e,{content:t}):e.$apply(function(){d(e,{content:t})})}catch(r){t=a.innerHTML;var o="ng-upload: Response is not valid JSON";n.warn(o),f&&(e.$$phase?f(e,{error:o}):e.$apply(function(){f(e,{error:o})}))}}catch(o){n.warn("ng-upload: Server error"),f&&(e.$$phase?f(e,{error:o}):e.$apply(function(){f(e,{error:o})}))}}r++;var u={},d=i.ngUpload?t(i.ngUpload):null,f=i.errorCatcher?t(i.errorCatcher):null,s=i.ngUploadLoading?t(i.ngUploadLoading):null;i.hasOwnProperty("uploadOptionsConvertHidden")&&(u.convertHidden="false"!=i.uploadOptionsConvertHidden),i.hasOwnProperty("uploadOptionsEnableRailsCsrf")&&(u.enableRailsCsrf="false"!=i.uploadOptionsEnableRailsCsrf),i.hasOwnProperty("uploadOptionsBeforeSubmit")&&(u.beforeSubmit=t(i.uploadOptionsBeforeSubmit)),o.attr({target:"upload-iframe-"+r,method:"post",enctype:"multipart/form-data",encoding:"multipart/form-data"});var c=angular.element('