HomeForumSourceResearchGuide
Sign in to contribute to source. how it works
Component pal.rest by barry
expand copy to clipboardexpand
/*
This is a REST API for the PAL service. You can visit localhost:HOST_POST/meta/ to view a list of available REST API commands for this service.
*/

uses pal.Perception

const int HOST_PORT = 8008

component provides App requires io.Output out, data.IntUtil iu, data.DecUtil du, pal.Assembly, time.Timer timer, composition.RecursiveLoader loader, data.StringUtil stringUtil, net.TCPServerSocket, net.TCPSocket, data.json.JSONParser parser, composition.Search search, data.StringBuilder, io.FileSystem fs, System system {
	
	bool running = true
	char currentConfig[]
	String configs[]
	char systemPath[]

	char HOST_ADDRESS[] = "127.0.0.1"

	TCPServerSocket server
	
	Assembly assembly
	Perception perc
	
	String[] readHeaders(TCPSocket socket)
		{
		String headers[]
		
		char buf[]
		char last4[] = new char[4]
		
		while (last4 != "\r\n\r\n")
			{
			char b[] = socket.recv(1)
			if (b.arrayLength == 0) break
			buf = new char[](buf, b)
			last4[0] = last4[1]
			last4[1] = last4[2]
			last4[2] = last4[3]
			last4[3] = b[0]
			}
		
		headers = stringUtil.explode(buf, "\r\n")
		
		return headers
		}
	
	char[] getHeaderValue(String headers[], char field[])
		{
		for (int i = 0; i < headers.arrayLength; i++)
			{
			String tokens[] = stringUtil.explode(headers[i].string, ":")
			
			if (tokens.arrayLength >= 1 && stringUtil.lowercase(tokens[0].string) == field)
				{
				int ndx = stringUtil.find(headers[i].string, ":") + 1
				return stringUtil.trim(stringUtil.subString(headers[i].string, ndx, headers[i].string.arrayLength - ndx))
				}
			}
		
		return null
		}
	
	void sendOK(TCPSocket socket, char contentType[], char content[])
		{
		int len = content.arrayLength
		
		socket.send("HTTP/1.0 200 OK\r\n")
		socket.send("Server: PAL\r\n")
		socket.send("Content-length: $len\r\n")
		socket.send("Content-type: $contentType\r\n")
		socket.send("Connection: close\r\n")
		socket.send("\r\n")
		
		socket.send(content)
		}
	
	void sendError(TCPSocket socket, char contentType[], char content[])
		{
		int len = content.arrayLength
		
		socket.send("HTTP/1.0 500 Internal server error\r\n")
		socket.send("Server: PAL\r\n")
		socket.send("Content-length: $len\r\n")
		socket.send("Content-type: $contentType\r\n")
		socket.send("Connection: close\r\n")
		socket.send("\r\n")
		
		socket.send(content)
		}
	
	char[] dateToString(DateTime dt)
		{
		char year[] = iu.makeString(dt.year)
		while (year.arrayLength < 4) year = new char[]("0", year)
		char month[] = iu.makeString(dt.month)
		if (month.arrayLength < 2) month = new char[]("0", month)
		char day[] = iu.makeString(dt.day)
		if (day.arrayLength < 2) day = new char[]("0", day)
		
		char hour[] = iu.makeString(dt.hour)
		if (hour.arrayLength < 2) hour = new char[]("0", hour)
		char minute[] = iu.makeString(dt.minute)
		if (minute.arrayLength < 2) minute = new char[]("0", minute)
		char second[] = iu.makeString(dt.second)
		if (second.arrayLength < 2) second = new char[]("0", second)
		
		return "$year-$month-$day $hour:$minute:$second"
		}
	
	char[] perceptionToJSON(PerceptionData pd)
		{
		char content[] = new char[](content, "{")
		
		if (pd != null)
			{
			content = new char[](content, "\"metrics\" : [ ")
			for (int i = 0; i < pd.metrics.arrayLength; i++)
				{
				char preferHigh[] = "false"
				
				if (pd.metrics[i].preferHigh) preferHigh = "true"
				
				content = new char[](content, "{\"name\" : \"$(pd.metrics[i].name)\", \"source\" : \"$(pd.metrics[i].sourceComponent)\", \"value\" : $(pd.metrics[i].totalValue), \"count\" : $(iu.makeString(pd.metrics[i].totalCount)), \"preferHigh\" : $preferHigh, \"startTime\" : \"$(dateToString(pd.metrics[i].timeFirst))\", \"endTime\" : \"$(dateToString(pd.metrics[i].timeLast))\"}")
				if (i + 1 < pd.metrics.arrayLength) content = new char[](content, ",")
				}
			content = new char[](content, "], ")
			
			content = new char[](content, "\"events\" : [")
			for (int i = 0; i < pd.events.arrayLength; i++)
				{
				content = new char[](content, "{\"name\" : \"$(pd.events[i].name)\", \"source\" : \"$(pd.events[i].sourceComponent)\", \"value\" : $(pd.events[i].totalValue), \"count\" : $(iu.makeString(pd.events[i].totalCount)), \"startTime\" : \"$(dateToString(pd.events[i].timeFirst))\", \"endTime\" : \"$(dateToString(pd.events[i].timeLast))\"}")
				if (i + 1 < pd.events.arrayLength) content = new char[](content, ",")
				}
			content = new char[](content, "], ")
			
			content = new char[](content, "\"trace\" : [")
			for (int i = 0; i < pd.trace.arrayLength; i++)
				{
				content = new char[](content, "{\"content\" : \"$(pd.trace[i].content)\"}")
				if (i + 1 < pd.trace.arrayLength) content = new char[](content, ",")
				}
			content = new char[](content, "]")
			}
		
		content = new char[](content, "}")
		
		return content
		}
	
	void handleRequest(TCPSocket socket)
		{
		String headers[] = readHeaders(socket)
		
		String tokens[] = stringUtil.explode(headers[0].string, " ")
		
		char cmd[] = tokens[0].string
		char rsc[] = tokens[1].string
		
		if (cmd == "GET")
			{
			if (rsc == "/meta/get_all_configs")
				{
				//construct JSON object of all available configurations
				StringBuilder sb = new StringBuilder()
				
				char content[]
				
				sb.add("{")
				sb.add("\"configs\" : [")
				
				for (int i = 0; i < configs.arrayLength; i++)
					{
					sb.add("\"$(configs[i].string)\"")
					
					if (i + 1 < configs.arrayLength) sb.add(", ")
					}
				sb.add("]")
				sb.add("}")
				
				//send response
				sendOK(socket, "text/json", sb.get())
				}
				else if (rsc == "/meta/get_config")
				{
				//return currently in-use configuration
				sendOK(socket, "text/json", new char[]("{\"config\" : \"", currentConfig, "\"}"))
				}
				else if (rsc == "/meta/get_perception")
				{
				//construct JSON object of perception data
				PerceptionData pd = perc.getPerception()
				
				char content[] = perceptionToJSON(pd)
				
				//send response
				sendOK(socket, "text/json", content)
				}
				else if (rsc == "/meta/get_system_path")
				{
				//send response
				sendOK(socket, "text", systemPath)
				}
				else if (rsc == "/meta/get_intercepts")
				{
				//construct JSON object of all available configurations
				InterceptInfo iclist[] = assembly.getIntercepts()
				
				StringBuilder sb = new StringBuilder()
				
				char content[]
				
				sb.add("{")
				sb.add("\"intercepts\" : [")
				
				for (int i = 0; i < iclist.arrayLength; i++)
					{
					sb.add("{\"rule\" : \"$(iclist[i].rule)\", \"impl\" : \"$(iclist[i].impl)\"}")
					
					if (i + 1 < iclist.arrayLength) sb.add(", ")
					}
				sb.add("]")
				sb.add("}")
				
				//send response
				sendOK(socket, "text/json", sb.get())
				}
				else if (rsc == "/meta/")
				{
				char divStyle[] = "margin-top:10pt;font-family: monospace;"
				char content[] = new char[]("",
											"",
											"
", "This is a REST API for the PAL service. The following commands are available:", "
", "
", "GET /meta/get_all_configs HTTP/1.0", "
", "
", "GET /meta/get_config HTTP/1.0", "
", "
", "GET /meta/get_perception HTTP/1.0", "
", "
", "POST /meta/set_config HTTP/1.0
", "Content-Type: text
", "
", "
", "POST /meta/add_component HTTP/1.0
", "Content-Type: text
", "
", "
", "POST /meta/rem_component HTTP/1.0
", "Content-Type: text
", "
", "
", "POST /meta/upd_component HTTP/1.0
", "Content-Type: text
", "
", "
", "POST /meta/upd_arch HTTP/1.0
", "Content-Type: text/json
", "
", "", "") //send response sendOK(socket, "text/html", content) } else { sendError(socket, "text", "Operation not known") } } else if (cmd == "POST") { if (rsc == "/meta/set_config") { char contentType[] = getHeaderValue(headers, "content-type") char payloadSize[] = getHeaderValue(headers, "content-length") if (contentType == "text/json" || contentType == "application/json") { char config[] = socket.recv(iu.intFromString(payloadSize)) //parse the JSON data JSONElement doc = parser.parseDocument(config) config = parser.getValue(doc, "config").value if (assembly.setConfig(config)) { currentConfig = config //flush perception data perc.getPerception() //respond with HTTP OK sendOK(socket, "text/html", null) } else { sendError(socket, "text", "Requested configuration '$config' is not known (you must use one of the configurations returned by get_configs)") } } else { sendError(socket, "text", "Content type must be text/json") } } else if (rsc == "/meta/add_component") { char contentType[] = getHeaderValue(headers, "content-type") char payloadSize[] = getHeaderValue(headers, "content-length") if (contentType == "text") { char comp[] = socket.recv(iu.intFromString(payloadSize)) String newConfigs[] = assembly.addComponent(comp) if (newConfigs != null) { //respond with HTTP OK sendOK(socket, "text/html", null) configs = assembly.getConfigs() currentConfig = configs[0].string } else { sendError(socket, "text", "Addition of component failed (check component exists in the system's default search paths)") } } else { sendError(socket, "text", "Content type must be text") } } else if (rsc == "/meta/rem_component") { char contentType[] = getHeaderValue(headers, "content-type") char payloadSize[] = getHeaderValue(headers, "content-length") if (contentType == "text") { char comp[] = socket.recv(iu.intFromString(payloadSize)) String remConfigs[] = assembly.remComponent(comp) if (remConfigs != null) { //respond with HTTP OK sendOK(socket, "text/html", null) configs = assembly.getConfigs() currentConfig = configs[0].string } else { sendError(socket, "text", "Removal of component failed (check component exists in the system, and the system is not currently using the selected component)") } } else { sendError(socket, "text", "Content type must be text") } } else if (rsc == "/meta/upd_component") { char contentType[] = getHeaderValue(headers, "content-type") char payloadSize[] = getHeaderValue(headers, "content-length") if (contentType == "text") { char comp[] = socket.recv(iu.intFromString(payloadSize)) UpdateInfo uinfo = assembly.updComponent(comp) if (uinfo != null) { //respond with HTTP OK sendOK(socket, "text", uinfo.currentConfig) configs = assembly.getConfigs() currentConfig = uinfo.currentConfig } else { sendError(socket, "text", "Update of component failed (check component exists in the system's default search paths)") } } else { sendError(socket, "text", "Content type must be text") } } else if (rsc == "/meta/add_intercept") { char contentType[] = getHeaderValue(headers, "content-type") char payloadSize[] = getHeaderValue(headers, "content-length") if (contentType == "text/json" || contentType == "application/json") { char config[] = socket.recv(iu.intFromString(payloadSize)) //parse the JSON data JSONElement doc = parser.parseDocument(config) char intf[] = parser.getValue(doc, "interface").value char cmp[] = parser.getValue(doc, "interceptComponent").value if (assembly.addIntercept(intf, cmp)) { //respond with HTTP OK sendOK(socket, "text/html", null) } else { sendError(socket, "text", "Add intercept failed") } } else { sendError(socket, "text", "Content type must be text/json") } } else if (rsc == "/meta/rem_intercept") { char contentType[] = getHeaderValue(headers, "content-type") char payloadSize[] = getHeaderValue(headers, "content-length") if (contentType == "text/json" || contentType == "application/json") { char config[] = socket.recv(iu.intFromString(payloadSize)) //parse the JSON data JSONElement doc = parser.parseDocument(config) char intf[] = parser.getValue(doc, "interface").value char cmp[] = parser.getValue(doc, "interceptComponent").value if (assembly.remIntercept(intf, cmp)) { //respond with HTTP OK sendOK(socket, "text/html", null) } else { sendError(socket, "text", "Add intercept failed") } } else { sendError(socket, "text", "Content type must be text/json") } } else { sendError(socket, "text", "Operation not known") } } else { sendError(socket, "text", "Operation type not supported") } socket.disconnect() } void serverThread(int port) { out.println("[REST service started, visit http://localhost:$port/meta/ for API details]") while (running) { TCPSocket s = new TCPSocket() if (s.accept(server)) handleRequest(s) } } char[] searchPaths(char f[]) { if (fs.exists(f)) return f String sp[] = new String[](new String("$(system.getDanaHome())/components"), system.getSearchPaths()) for (int i = 0; i < sp.arrayLength; i++) { if (fs.exists("$(sp[i].string)/$f")) return "$(sp[i].string)/$f" } return null } //this function resolves any valid way of specifying an object file char[] findObjectFile(char f[]) { char result[] = null //try all of our search paths if ((result = searchPaths(f)) != null) return result if ((result = searchPaths("$f.o")) != null) return result //convert dots to slashes, iteratively, starting from the left // - for each iteration, try all of our search paths // - then append .o, and try all of our search paths again char rhs[] = f char lhs[] = "" String parts[] = rhs.lsplit(".") while (parts != null) { rhs = parts[1].string if (lhs.arrayLength == 0) lhs = parts[0].string else lhs = "$lhs/$(parts[0].string)" char form[] = "$lhs/$rhs" if ((result = searchPaths(form)) != null) return result form = "$lhs/$rhs.o" if ((result = searchPaths(form)) != null) return result parts = rhs.lsplit(".") } return null } int App:main(AppParam params[]) { if (params.arrayLength == 0) { out.println("usage: pal.rest main_component.o") out.println("") out.println("Any parameters after main_component.o are passed to the sub-program") out.println("") out.println("options for pal.rest:") out.println(" -exc \"a;b;c\" Exclude given components from initial compostion search") out.println(" -xd \"a;b;c\" Disable exception printing for specific components") out.println(" -p portNumber Set the port number on which the REST service will listen") out.println(" -a serverAddress Set the server address to bind the REST service to") out.println(" Default address is 127.0.0.1 (local connections only)") out.println(" To support remote connections, use ANY, ANY_v4, or ANY_v6)") return 0 } char objectFile[] = null int serverPort = HOST_PORT char serverAddress[] = HOST_ADDRESS String cExclude[] = null String exceptionsDisable[] = null AppParam subParams[] = null for (int i = 0; i < params.arrayLength; i++) { if (objectFile == null) { //parameters here are for pal.rest if (params[i].string == "-p") { if (i + 1 < params.arrayLength) { if (stringUtil.isNumeric(params[i+1].string)) { serverPort = iu.intFromString(params[i+1].string) i ++ } else { out.println("expected port number after -p") return 0 } } else { out.println("expected port number after -p") return 0 } } else if (params[i].string == "-a") { if (i + 1 < params.arrayLength) { serverAddress = params[i+1].string if (serverAddress == "ANY") serverAddress = TCPServerSocket.ANY_ADDRESS else if (serverAddress == "ANY_v4") serverAddress = TCPServerSocket.ANY_ADDRESS_v4 else if (serverAddress == "ANY_v6") serverAddress = TCPServerSocket.ANY_ADDRESS_v6 i ++ } else { out.println("expected IP address, or ANY, ANY_v4, ANY_v6 after -a") return 0 } } else if (params[i].string == "-exc") { if (i + 1 < params.arrayLength) { cExclude = params[i+1].string.explode(";") i ++ } else { out.println("expected one or more components to exclude after -exc") return 0 } } else if (params[i].string == "-xd") { if (i + 1 < params.arrayLength) { exceptionsDisable = new String[](exceptionsDisable, params[i+1].string.explode(";")) i ++ } else { out.println("expected one or more components to exclude after -xd") return 0 } } else { objectFile = params[i].string } } else { //collect as a sub-parameter to pass to the sub-program we're launching subParams = new AppParam[](subParams, new AppParam(params[i].string)) } } if (objectFile == null) { out.println("object file not specified") return 1 } objectFile = objectFile.explode("\\").implode("/") char resolvedFile[] = null if ((resolvedFile = findObjectFile(objectFile)) == null) { out.println("object file $objectFile not found") return 1 } server = new TCPServerSocket() if (!server.bind(serverAddress, serverPort)) { out.println("failed to bind to server address/port of $(serverAddress)/$(serverPort)") return 1 } //we need to find the fully-qualified path to the object file we're launching, so we can pass it in to App.setSourcePath() int splitPoint = 0 systemPath = fs.getFullPath(resolvedFile) if ((splitPoint = systemPath.rfind("/")) != StringUtil.NOT_FOUND) systemPath = systemPath.subString(0, splitPoint+1) else systemPath = "." out.println("[system home path: $systemPath]") char perceptionPath[] = search.getDefaultComponent("pal.Perception") LoadedComponents lc = loader.load(perceptionPath) IDC perception = lc.mainComponent out.println("[scanning system for components, please be patient...]") perc = new Perception() from perception assembly = new Assembly(fs.getFullPath(resolvedFile), subParams, perception, componentExcludeList = cExclude) assembly.disableExceptions(exceptionsDisable) configs = assembly.getConfigs() out.println("[located $(configs.arrayLength) possible configurations for target software system]") assembly.setConfig(configs[0].string) currentConfig = configs[0].string //call assembly.runApp(), and launch the server stuff in a different thread... asynch::serverThread(serverPort) assembly.runApp(systemPath) running = false server.unbind() return 0 } }
Revision history
To propose a new revision to this entity, use dana source put -uc your/new/version.dn -n pal.rest -m "reason for update" -u yourUsername
Version 1 (this version) by barry
Notes for this version: Standard Library Initialisation