You are viewing a plain text version of this content. The canonical link for it is here.
Posted to dev@shindig.apache.org by ch...@apache.org on 2008/07/19 00:47:41 UTC

svn commit: r678068 - in /incubator/shindig/trunk/php/src/social-api: converters/ dataservice/ http/ samplecontainer/

Author: chabotc
Date: Fri Jul 18 15:47:41 2008
New Revision: 678068

URL: http://svn.apache.org/viewvc?rev=678068&view=rev
Log:
Initial support for the batch proxy request type.

The url is /social/rest/batchProxy and it uses the http multipart 
format as described in the RESTful API specification.

It only supports ONE output format for a set of requests (which
is determined by the main url ?format=foo param, and defaults to
json). Input however will support mixing json and atom.

Atom input is still missing (has a debug dump right now) but support
for that will follow quickly.

OAuth and Atom input is still missing, but once their done PHP
Shindig will have full RESTful spec support, we're getting there!


Modified:
    incubator/shindig/trunk/php/src/social-api/converters/OutputAtomConverter.php
    incubator/shindig/trunk/php/src/social-api/converters/OutputConverter.php
    incubator/shindig/trunk/php/src/social-api/converters/OutputJsonConverter.php
    incubator/shindig/trunk/php/src/social-api/dataservice/PeopleHandler.php
    incubator/shindig/trunk/php/src/social-api/dataservice/RestRequestItem.php
    incubator/shindig/trunk/php/src/social-api/http/RestServlet.php
    incubator/shindig/trunk/php/src/social-api/samplecontainer/BasicAppDataService.php
    incubator/shindig/trunk/php/src/social-api/samplecontainer/BasicPeopleService.php

Modified: incubator/shindig/trunk/php/src/social-api/converters/OutputAtomConverter.php
URL: http://svn.apache.org/viewvc/incubator/shindig/trunk/php/src/social-api/converters/OutputAtomConverter.php?rev=678068&r1=678067&r2=678068&view=diff
==============================================================================
--- incubator/shindig/trunk/php/src/social-api/converters/OutputAtomConverter.php (original)
+++ incubator/shindig/trunk/php/src/social-api/converters/OutputAtomConverter.php Fri Jul 18 15:47:41 2008
@@ -1,4 +1,5 @@
 <?php
+
 /*
  * Licensed to the Apache Software Foundation (ASF) under one
  * or more contributor license agreements. See the NOTICE file
@@ -19,7 +20,7 @@
 
 /**
  * Format = atom output converter, for format definition see:
- * http://www.opensocial.org/Technical-Resources/opensocial-specification----implementation-version-08/restful-api-specification
+ * http://sites.google.com/a/opensocial.org/opensocial/Technical-Resources/opensocial-spec-v08/restful-api-specification
  */
 class OutputAtomConverter extends OutputConverter {
 	private static $nameSpace = 'http://www.w3.org/2005/Atom';
@@ -40,7 +41,7 @@
 		$data = $responseItem->getResponse();
 		$userId = $requestItem->getUser()->getUserId($requestItem->getToken());
 		$guid = 'urn:guid:' . $userId;
-		$authorName = $_SERVER['HTTP_HOST'].':'.$userId;
+		$authorName = $_SERVER['HTTP_HOST'] . ':' . $userId;
 		$updatedAtom = date(DATE_ATOM);
 		
 		// Check to see if this is a single entry, or a collection, and construct either an atom 
@@ -52,20 +53,18 @@
 			
 			// The root Feed element
 			$entry = $this->addNode($doc, 'feed', '', false, self::$nameSpace);
-
+			
 			// Required Atom fields
 			$endPos = ($startIndex + $itemsPerPage) > $totalResults ? $totalResults : ($startIndex + $itemsPerPage);
-			$this->addNode($entry, 'title', $requestType.' feed for id '.$authorName.' ('.$startIndex. ' - '. ($endPos - 1).' of '.$totalResults.')');
+			$this->addNode($entry, 'title', $requestType . ' feed for id ' . $authorName . ' (' . $startIndex . ' - ' . ($endPos - 1) . ' of ' . $totalResults . ')');
 			$author = $this->addNode($entry, 'author');
 			$this->addNode($author, 'uri', $guid);
-			$this->addNode($author, 'name', $authorName);			
+			$this->addNode($author, 'name', $authorName);
 			$this->addNode($entry, 'updated', $updatedAtom);
 			$this->addNode($entry, 'id', $guid);
-			$this->addNode($entry, 'link', '', array('rel' => 'self', 'href' => 'http://'.$_SERVER['HTTP_HOST'].$_SERVER['REQUEST_URI']));
-			
+			$this->addNode($entry, 'link', '', array('rel' => 'self', 'href' => 'http://' . $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI']));
 			// Add osearch & next link to the entry
 			$this->addPagingFields($entry, $startIndex, $itemsPerPage, $totalResults);
-			
 			// Add response entries to feed
 			$responses = $responseItem->getResponse()->getEntry();
 			foreach ($responses as $response) {
@@ -79,11 +78,10 @@
 				$this->addNode($author, 'uri', $guid);
 				$this->addNode($author, 'name', $authorName);
 				// Special hoisting rules for activities
-				
 				if ($response instanceof Activity) {
 					$this->addNode($feedEntry, 'category', '', array('term' => 'status'));
 					$this->addNode($feedEntry, 'updated', date(DATE_ATOM, $response->postedTime));
-					$this->addNode($feedEntry, 'id', 'urn:guid:'.$response->id);
+					$this->addNode($feedEntry, 'id', 'urn:guid:' . $response->id);
 					//FIXME should add a link field but don't have URL's available yet:
 					// <link rel="self" type="application/atom+xml" href="http://api.example.org/activity/feeds/.../af3778"/>
 					$this->addNode($feedEntry, 'title', strip_tags($response->title));
@@ -94,43 +92,49 @@
 					unset($response->title);
 					unset($response->body);
 				} else {
-					$this->addNode($feedEntry, 'id', 'urn:guid:'.$idField);
-					$this->addNode($feedEntry, 'title', $requestType.' feed entry for id '.$idField);
+					$this->addNode($feedEntry, 'id', 'urn:guid:' . $idField);
+					$this->addNode($feedEntry, 'title', $requestType . ' feed entry for id ' . $idField);
 					$this->addNode($feedEntry, 'updated', $updatedAtom);
 				}
 				
 				// recursively add responseItem data to the xml structure
 				$this->addData($content, $requestType, $response, self::$osNameSpace);
 			}
-			
 		} else {
-			
 			// Single entry = Atom:Entry	
 			$entry = $doc->appendChild($doc->createElementNS(self::$nameSpace, "entry"));
-			
 			// Atom fields
-			$this->addNode($entry, 'title', $requestType.' entry for '.$authorName);
+			$this->addNode($entry, 'title', $requestType . ' entry for ' . $authorName);
 			$author = $this->addNode($entry, 'author');
 			$this->addNode($author, 'uri', $guid);
 			$this->addNode($author, 'name', $authorName);
 			$this->addNode($entry, 'id', $guid);
 			$this->addNode($entry, 'updated', $updatedAtom);
 			$content = $this->addNode($entry, 'content', '', array('type' => 'application/xml'));
-			
 			// addData loops through the responseItem data recursively creating a matching XML structure
 			$this->addData($content, $requestType, $data, self::$osNameSpace);
 		}
 		$xml = $doc->saveXML();
-		if (self::$includeOsearch && $responseItem->getResponse() instanceof RestFulCollection) {
+		if ($responseItem->getResponse() instanceof RestFulCollection) {
 			//FIXME dirty hack until i find a way to add multiple name spaces using DomXML functions
-			$xml = str_replace('<feed xmlns="http://www.w3.org/2005/Atom">', '<feed xmlns="http://www.w3.org/2005/Atom" xmlos:osearch="http://a9.com/-/spec/opensearch/1.1">' ,$xml);
+			$xml = str_replace('<feed xmlns="http://www.w3.org/2005/Atom">', '<feed xmlns="http://www.w3.org/2005/Atom" xmlns:osearch="http://a9.com/-/spec/opensearch/1.1">', $xml);
 		}
 		echo $xml;
 	}
 
 	function outputBatch(Array $responses, SecurityToken $token)
 	{
-		//TODO once we support spec compliance batching, this needs to be added too
+		$this->boundryHeaders();
+		foreach ($responses as $response) {
+			$request = $response['request'];
+			$response = $response['response'];
+			// output buffering supports multiple levels of it.. it's a nice feature to abuse :)
+			ob_start();
+			$this->outputResponse($response, $request);
+			$part = ob_get_contents();
+			ob_end_clean();
+			$this->outputPart($part, $response->getError());
+		}
 	}
 
 	/**
@@ -174,11 +178,9 @@
 	 */
 	private function addPagingFields($entry, $startIndex, $itemsPerPage, $totalResults)
 	{
-		if (self::$includeOsearch) {
-			$this->addNode($entry, 'osearch:totalResults', $totalResults);
-			$this->addNode($entry, 'osearch:startIndex', $startIndex ? $startIndex : '0');
-			$this->addNode($entry, 'osearch:itemsPerPage', $itemsPerPage);
-		}
+		$this->addNode($entry, 'osearch:totalResults', $totalResults);
+		$this->addNode($entry, 'osearch:startIndex', $startIndex ? $startIndex : '0');
+		$this->addNode($entry, 'osearch:itemsPerPage', $itemsPerPage);
 		// Create a 'next' link based on our current url if this is a pageable collection & there is more to display
 		if (($startIndex + $itemsPerPage) < $totalResults) {
 			$nextStartIndex = ($startIndex + $itemsPerPage) - 1;
@@ -255,6 +257,9 @@
 					}
 					$this->addData($newElement, $key, $val);
 				} else {
+					if (is_numeric($key)) {
+						$key = is_object($val) ? get_class($val) : $key = $name;
+					}					
 					$elm = $newElement->appendChild($this->doc->createElement($key));
 					$elm->appendChild($this->doc->createTextNode($val));
 				}

Modified: incubator/shindig/trunk/php/src/social-api/converters/OutputConverter.php
URL: http://svn.apache.org/viewvc/incubator/shindig/trunk/php/src/social-api/converters/OutputConverter.php?rev=678068&r1=678067&r2=678068&view=diff
==============================================================================
--- incubator/shindig/trunk/php/src/social-api/converters/OutputConverter.php (original)
+++ incubator/shindig/trunk/php/src/social-api/converters/OutputConverter.php Fri Jul 18 15:47:41 2008
@@ -22,6 +22,50 @@
  *
  */
 abstract class OutputConverter {
+	private $boundry;
+	
 	abstract function outputResponse(ResponseItem $responseItem, RestRequestItem $requestItem);
 	abstract function outputBatch(Array $responses, SecurityToken $token);
+	
+	/**
+	 * Output the multipart/mixed headers and returns the boundry token used
+	 *
+	 */
+	public function boundryHeaders()
+	{
+		$this->boundry = '--batch-'.md5(rand(0,32000));
+		header("HTTP/1.1 200 OK", true);
+		header("Content-Type: multipart/mixed; boundary=$this->boundry", true);
+	}
+	
+	public function outputPart($part, $code)
+	{
+		$boundryHeader = "{$this->boundry}\n".
+				"Content-Type: application/http;version=1.1\n".
+				"Content-Transfer-Encoding: binary\n\n";
+		echo $boundryHeader;
+		switch ($code) {
+			case BAD_REQUEST:
+				$code = '400 Bad Request';
+				break;
+			case UNAUTHORIZED:
+				$code = '401 Unauthorized';
+				break;
+			case FORBIDDEN:
+				$code = '403 Forbidden';
+				break;
+			case FORBIDDEN:
+				$code = '404 Not Found';
+				break;
+			case NOT_IMPLEMENTED:
+				$code = '501 Not Implemented';
+				break;
+			case INTERNAL_ERROR:
+			default:
+				$code = '200 OK';
+				break;
+		}
+		echo "$code\n\n";
+		echo $part."\n";
+	}
 }

Modified: incubator/shindig/trunk/php/src/social-api/converters/OutputJsonConverter.php
URL: http://svn.apache.org/viewvc/incubator/shindig/trunk/php/src/social-api/converters/OutputJsonConverter.php?rev=678068&r1=678067&r2=678068&view=diff
==============================================================================
--- incubator/shindig/trunk/php/src/social-api/converters/OutputJsonConverter.php (original)
+++ incubator/shindig/trunk/php/src/social-api/converters/OutputJsonConverter.php Fri Jul 18 15:47:41 2008
@@ -30,6 +30,17 @@
 	
 	function outputBatch(Array $responses, SecurityToken $token)
 	{
+		$this->boundryHeaders();
+		foreach ($responses as $response) {
+			$request = $response['request'];
+			$response = $response['response'];
+			$part = json_encode($response);
+			$this->outputPart($part, $response->getError());
+		}
+	}
+	
+	function outputJsonBatch(Array $responses, SecurityToken $token)
+	{
 		echo json_encode(array("responses" => $responses, "error" => false));
 	}
 }

Modified: incubator/shindig/trunk/php/src/social-api/dataservice/PeopleHandler.php
URL: http://svn.apache.org/viewvc/incubator/shindig/trunk/php/src/social-api/dataservice/PeopleHandler.php?rev=678068&r1=678067&r2=678068&view=diff
==============================================================================
--- incubator/shindig/trunk/php/src/social-api/dataservice/PeopleHandler.php (original)
+++ incubator/shindig/trunk/php/src/social-api/dataservice/PeopleHandler.php Fri Jul 18 15:47:41 2008
@@ -20,7 +20,9 @@
 class PeopleHandler extends DataRequestHandler {
 	private $service;
 	private static $PEOPLE_PATH = "/people/{userId}/{groupId}/{personId}";
-	protected static $DEFAULT_PERSON_FIELDS = array("id", "name", "thumbnailUrl");
+	//FIXME change this back to array("id", "name", "thumbnailUrl") once the dust settles
+	// on the spec discussion related to this
+	protected static $DEFAULT_PERSON_FIELDS = array('all' => 'all');
 
 	public function __construct()
 	{

Modified: incubator/shindig/trunk/php/src/social-api/dataservice/RestRequestItem.php
URL: http://svn.apache.org/viewvc/incubator/shindig/trunk/php/src/social-api/dataservice/RestRequestItem.php?rev=678068&r1=678067&r2=678068&view=diff
==============================================================================
--- incubator/shindig/trunk/php/src/social-api/dataservice/RestRequestItem.php (original)
+++ incubator/shindig/trunk/php/src/social-api/dataservice/RestRequestItem.php Fri Jul 18 15:47:41 2008
@@ -45,12 +45,12 @@
 
 	public function createRequestItemWithRequest($request, $token)
 	{
-		$this->url = $request->url;
-		$this->parameters = $this->createParameterMap($request->url);
+		$this->url = $request['url'];
+		$this->parameters = $this->createParameterMap($request['url']);
 		$this->token = $token;
-		$this->method = $request->method;
-		if (isset($request->postData)) {
-			$this->postData = $request->postData;
+		$this->method = $request['method'];
+		if (isset($request['postData'])) {
+			$this->postData = $request['postData'];
 		}
 	}
 

Modified: incubator/shindig/trunk/php/src/social-api/http/RestServlet.php
URL: http://svn.apache.org/viewvc/incubator/shindig/trunk/php/src/social-api/http/RestServlet.php?rev=678068&r1=678067&r2=678068&view=diff
==============================================================================
--- incubator/shindig/trunk/php/src/social-api/http/RestServlet.php (original)
+++ incubator/shindig/trunk/php/src/social-api/http/RestServlet.php Fri Jul 18 15:47:41 2008
@@ -51,8 +51,6 @@
 require 'src/social-api/converters/OutputAtomConverter.php';
 require 'src/social-api/converters/OutputJsonConverter.php';
 
-//FIXME Delete should respond with a 204 No Content to indicate success
-
 class RestException extends Exception {}
 
 /*
@@ -64,42 +62,80 @@
 define('FORBIDDEN', "forbidden");
 define('BAD_REQUEST', "badRequest");
 define('INTERNAL_ERROR', "internalError");
+//FIXME Delete should respond with a 204 No Content to indicate success
 
 class RestServlet extends HttpServlet {
 	
+	// The json Batch Route is used by the gadgets 
 	private static $JSON_BATCH_ROUTE = "jsonBatch";
+	// The Batch Proxy route is used the one defined in the RESTful API specification
+	private static $BATCH_PROXY_ROUTE = "batchProxy";
+
+	public function doGet()
+	{
+		$this->doPost('GET');
+	}
 
+	public function doPut()
+	{
+		$this->doPost('PUT');
+	}
+
+	public function doDelete()
+	{
+		$this->doPost('DELETE');
+	}
+		
 	public function doPost($method = 'POST')
 	{
-		$this->setNoCache(true);
-		// if oauth, create a token from it's values instead of one based on $_get['st']/$_post['st']
-		// NOTE : if no token is provided an anonymous one is created (owner = viewer = appId = modId = 0)
-		// keep this in mind when creating your data services.. 
-		$token = $this->getSecurityToken();
-		$outputFormat = $this->getOutputFormat();
-		switch ($outputFormat) {
-			case 'json':
-				$this->setContentType('application/json');
-				$outputConverter = new OutputJsonConverter();
-				break;
-			case 'atom':
-				$this->setContentType('application/atom+xml');
-				$outputConverter = new OutputAtomConverter();
-				break;
-			default:
-				$this->outputError(new ResponseItem(NOT_IMPLEMENTED, "Invalid output format"));
-				break;
-		}
-		if ($this->isBatchUrl()) {
-			$responses = $this->handleBatchRequest($token);
-			$outputConverter->outputBatch($responses, $token);
-		} else {
-			$response = $this->handleSingleRequest($token, $method);
-			$outputConverter->outputResponse($response['response'], $response['request']);
+		try {
+			$this->setNoCache(true);
+			// if oauth, create a token from it's values instead of one based on $_get['st']/$_post['st']
+			// NOTE : if no token is provided an anonymous one is created (owner = viewer = appId = modId = 0)
+			// keep this in mind when creating your data services.. 
+			$token = $this->getSecurityToken();
+			$outputFormat = $this->getOutputFormat();
+			switch ($outputFormat) {
+				case 'json':
+					$this->setContentType('application/json');
+					$outputConverter = new OutputJsonConverter();
+					break;
+				case 'atom':
+					$this->setContentType('application/atom+xml');
+					$outputConverter = new OutputAtomConverter();
+					break;
+				default:
+					$this->outputError(new ResponseItem(NOT_IMPLEMENTED, "Invalid output format"));
+					break;
+			}
+			if ($this->isJsonBatchUrl()) {
+				// custom json batch format used by the gadgets
+				$responses = $this->handleJsonBatchRequest($token);
+				$outputConverter->outputJsonBatch($responses, $token);
+			} elseif ($this->isBatchProxyUrl()) {
+				// spec compliant batch proxy
+				$this->noHeaders = true;
+				$responses = $this->handleBatchProxyRequest($token);
+				$outputConverter->outputBatch($responses, $token);
+			} else {
+				// single rest request
+				$response = $this->handleRequest($token, $method);
+				$outputConverter->outputResponse($response['response'], $response['request']);
+			}
+		} catch (Exception $e) {
+			header("HTTP/1.0 500 Internal Server Error");
+			echo "<html><body><h1>500 Internal Server Error</h1>";
+			if (Config::get('debug')) {
+				echo "Message: ".$e->getMessage()."<br />\n";
+				echo "<pre>\n";
+				print_r(debug_backtrace());
+				echo "\n</pre>";
+			}
+			echo "</body></html>";
 		}
 	}
 
-	private function handleSingleRequest($token, $method)
+	private function handleRequest($token, $method)
 	{
 		$params = $this->getListParams();
 		$requestItem = new RestRequestItem();
@@ -109,14 +145,115 @@
 		$responseItem = $this->getResponseItem($requestItem);
 		return array('request' => $requestItem, 'response' => $responseItem);
 	}
-
-	private function getRouteFromParameter($pathInfo)
+	
+	private function handleJsonBatchRequest($token)
 	{
-		$pathInfo = substr($pathInfo, 1);
-		$indexOfNextPathSeparator = strpos($pathInfo, "/");
-		return $indexOfNextPathSeparator != - 1 ? substr($pathInfo, 0, $indexOfNextPathSeparator) : $pathInfo;
+		// we support both a raw http post (without application/x-www-form-urlencoded headers) like java does
+		// and a more php / curl safe version of a form post with 'request' as the post field that holds the request json data
+		if (isset($GLOBALS['HTTP_RAW_POST_DATA']) || isset($_POST['request'])) {
+			$requests = $this->getRequestParams();
+			$responses = array();
+			foreach ($requests as $key => $value) {
+				$requestItem = new RestRequestItem();
+				$requestItem->createRequestItemWithRequest($value, $token);
+				$responses[$key] = $this->getResponseItem($requestItem);
+			}
+			return $responses;
+		} else {
+			throw new Exception("No post data set");
+		}
+	}
+	
+	private function handleBatchProxyRequest($token)
+	{
+		// Is this is a multipath/mixed post? Check content type:
+		if (isset($GLOBALS['HTTP_RAW_POST_DATA']) && strpos($_SERVER['CONTENT_TYPE'], 'multipart/mixed') !== false && strpos($_SERVER['CONTENT_TYPE'],'boundary=') !== false) {
+			// Ok looks swell, see what our boundry is..
+			$boundry = substr($_SERVER['CONTENT_TYPE'], strpos($_SERVER['CONTENT_TYPE'],'boundary=') + strlen('boundary='));
+			// Split up requests per boundry
+			$requests = explode($boundry, $GLOBALS['HTTP_RAW_POST_DATA']);
+			$responses = array();
+			foreach ($requests as $request) {
+				$request = trim($request);
+				if (!empty($request)) {
+					// extractBatchRequest() does the magic parsing of the raw post data to a meaninful request array
+					$request = $this->extractBatchRequest($request);
+					$requestItem = new RestRequestItem();
+					$requestItem->createRequestItemWithRequest($request, $token);
+					$responses[] = array('request' => $requestItem, 'response' => $this->getResponseItem($requestItem));
+				}
+			}
+		} else {
+			$this->outputError(new ResponseItem(BAD_REQUEST, "Invalid multipart/mixed request"));
+		}
+		return $responses;
 	}
 
+	private function extractBatchRequest($request)
+	{
+		/* Multipart request is formatted like:
+		 * -batch-a73hdj3dy3mm347ddjjdf
+		 * Content-Type: application/http;version=1.1
+		 * Content-Transfer-Encoding: binary
+		 * 
+		 * GET /people/@me/@friends?startPage=5&count=10&format=json
+		 * Host: api.example.org
+		 * If-None-Match: "837dyfkdi39df"
+		 * 
+		 * but we only want to have the last bit (the actual request), this filters that down first
+		 */
+		$emptyFound = false;
+		$requestLines = explode("\n", $request);
+		$request = '';
+		foreach ($requestLines as $line) {
+			if ($emptyFound) {
+				$request .= $line."\n";
+			} elseif (empty($line)) {
+				$emptyFound = true;
+			}
+		}
+		// Now that we have the basic request in $request, split that up again & parse it into a meaningful representation
+		$firstFound = $emptyFound = false;
+		$requestLines = explode("\n", $request);
+		$request = array();
+		$request['headers'] = array();
+		$request['postData'] = '';
+		foreach ($requestLines as $line) {
+			if (!$firstFound) {
+				$firstFound = true;
+				$parts = explode(' ', trim($line));
+				if (count($parts) != 2) {
+					throw new Exception("Mallshaped request uri in multipart block");
+				}
+				$request['method'] = strtoupper(trim($parts[0]));
+				// cut it down to an actual meaningful url without the prefix/social/rest part right away 
+				$request['url'] = substr(trim($parts[1]), strlen(Config::get('web_prefix') . '/social/rest'));
+			} elseif (!$emptyFound && !empty($line)) {
+				// convert the key to the PHP 'CONTENT_TYPE' style naming convention.. it's ugly but consitent
+				$key = str_replace('-', '_', strtoupper(trim(substr($line, 0, strpos($line, ':')))));
+				$val = trim(substr($line, strpos($line, ':') + 1));
+				$request['headers'][$key] = $val;
+			} elseif (!$emptyFound && empty($line)) {
+				$emptyFound = true;
+			} else {
+				if (get_magic_quotes_gpc()) {
+					$line = stripslashes($line);
+				}
+				$request['postData'] .= $line."\n";
+			}
+		}
+		if (empty($request['postData'])) {
+			// don't trip the requestItem into thinking there is postData when there's not
+			unset($request['postData']);
+		} else {
+			// if there was a post data blob present, decode it into an array, the format is based on the 
+			// content type header, which is either application/json or 
+			$format = isset($request['headers']['CONTENT_TYPE']) && strtolower($request['headers']['CONTENT_TYPE']) == 'application/atom+xml' ? 'atom' : 'json';
+			$request['postData'] = $this->decodeRequests($request['postData'], $format);
+		}
+		return $request;
+	}
+	
 	private function getResponseItem(RestRequestItem $requestItem)
 	{
 		$path = $this->getRouteFromParameter($requestItem->getUrl());
@@ -140,12 +277,26 @@
 			$class = new $class(null);
 			$response = $class->handleMethod($requestItem);
 		}
-		if ($response->getError() != null && !$this->isBatchUrl()) {
+		if ($response->getError() != null && !$this->isJsonBatchUrl() && !$this->isBatchProxyUrl()) {
 			// Can't use http error codes in batch mode, instead we return the error code in the response item
 			$this->outputError($response);
 		}
 		return $response;
 	}
+	
+	private function decodeRequests($requestParam, $format = 'json')
+	{
+		// temp hack until i know what the intended way to detect format is
+		if ($format == 'json') {
+			return json_decode($requestParam, true);
+		} elseif ($format == 'atom') {
+			$xml = simplexml_load_string($requestParam);
+			print_r($xml);
+			return $xml;
+		} else {
+			throw Exception("Invalid or unsupported input format");
+		}
+	}
 
 	private function getRequestParams()
 	{
@@ -154,44 +305,14 @@
 		if (get_magic_quotes_gpc()) {
 			$requestParam = stripslashes($requestParam);
 		}
-		$requests = json_decode($requestParam);
-		if ($requests == (isset($GLOBALS['HTTP_RAW_POST_DATA']) ? $GLOBALS['HTTP_RAW_POST_DATA'] : $post)) {
-			return new ResponseItem(BAD_REQUEST, "Malformed json string");
-		}
-		return $requests;
-	}
-
-	private function handleBatchRequest($token)
-	{
-		// we support both a raw http post (without application/x-www-form-urlencoded headers) like java does
-		// and a more php / curl safe version of a form post with 'request' as the post field that holds the request json data
-		if (isset($GLOBALS['HTTP_RAW_POST_DATA']) || isset($_POST['request'])) {
-			$requests = $this->getRequestParams();
-			$responses = array();
-			foreach ($requests as $key => $value) {
-				$requestItem = new RestRequestItem();
-				$requestItem->createRequestItemWithRequest($value, $token);
-				$responses[$key] = $this->getResponseItem($requestItem);
-			}
-			return $responses;
-		} else {
-			throw new Exception("No post data set");
-		}
-	}
-
-	public function doGet()
-	{
-		$this->doPost('GET');
-	}
-
-	public function doPut()
-	{
-		$this->doPost('PUT');
+		return $this->decodeRequests($requestParam);
 	}
 
-	public function doDelete()
+	private function getRouteFromParameter($pathInfo)
 	{
-		$this->doPost('DELETE');
+		$pathInfo = substr($pathInfo, 1);
+		$indexOfNextPathSeparator = strpos($pathInfo, "/");
+		return $indexOfNextPathSeparator != - 1 ? substr($pathInfo, 0, $indexOfNextPathSeparator) : $pathInfo;
 	}
 
 	private function outputError(ResponseItem $response)
@@ -217,7 +338,6 @@
 			default:
 				$code = '500 Internal Server Error';
 				break;
-		
 		}
 		header("HTTP/1.0 $code", true);
 		echo "$code - $errorMessage";
@@ -265,8 +385,13 @@
 		return substr($_SERVER["REQUEST_URI"], strlen(Config::get('web_prefix') . '/social/rest'));
 	}
 
-	public function isBatchUrl()
+	public function isJsonBatchUrl()
 	{
 		return strrpos($_SERVER["REQUEST_URI"], RestServlet::$JSON_BATCH_ROUTE) > 0;
 	}
+
+	public function isBatchProxyUrl()
+	{
+		return strrpos($_SERVER["REQUEST_URI"], RestServlet::$BATCH_PROXY_ROUTE) > 0;
+	}
 }

Modified: incubator/shindig/trunk/php/src/social-api/samplecontainer/BasicAppDataService.php
URL: http://svn.apache.org/viewvc/incubator/shindig/trunk/php/src/social-api/samplecontainer/BasicAppDataService.php?rev=678068&r1=678067&r2=678068&view=diff
==============================================================================
--- incubator/shindig/trunk/php/src/social-api/samplecontainer/BasicAppDataService.php (original)
+++ incubator/shindig/trunk/php/src/social-api/samplecontainer/BasicAppDataService.php Fri Jul 18 15:47:41 2008
@@ -85,7 +85,7 @@
 		switch($groupId->getType()) {
 			case 'self':
 				foreach ($fields as $key) {
-					$value = isset($values->$key) ? $values->$key : (@isset($values[$key]) ? @$values[$key] : null);
+					$value = isset($values[$key]) ? @$values[$key] : null;
 					XmlStateFileFetcher::get()->setAppData($userId->getUserId($token), $key, $value);	
 				}
 				break;

Modified: incubator/shindig/trunk/php/src/social-api/samplecontainer/BasicPeopleService.php
URL: http://svn.apache.org/viewvc/incubator/shindig/trunk/php/src/social-api/samplecontainer/BasicPeopleService.php?rev=678068&r1=678067&r2=678068&view=diff
==============================================================================
--- incubator/shindig/trunk/php/src/social-api/samplecontainer/BasicPeopleService.php (original)
+++ incubator/shindig/trunk/php/src/social-api/samplecontainer/BasicPeopleService.php Fri Jul 18 15:47:41 2008
@@ -76,7 +76,7 @@
 				if ($id == $token->getOwnerId()) {
 					$person->setIsOwner(true);
 				}
-				if (is_array($profileDetails) && count($profileDetails)) {
+				if (is_array($profileDetails) && count($profileDetails) && !in_array('all', $profileDetails)) {
 					$newPerson = array();
 					$newPerson['isOwner'] = $person->isOwner;
 					$newPerson['isViewer'] = $person->isViewer;



Re: RESTful API progress

Posted by John Panzer <jp...@google.com>.
Cool!  (Also +spec list)

o Perhaps we could iterate on the Shindig intepretation of adding friend
relationships?
o requestShareApp, last time I asked, required UI approval from the owner of
the gadget (the user); is that still the case?  Not sure how to do that in a
protocol... though it's sort of parallel to sending a message of course.

John Panzer (http://abstractioneer.org)

On Sat, Jul 19, 2008 at 4:43 PM, Chris Chabot <ch...@xs4all.nl> wrote:

> Atom format input is now supported for creating activities and setting app
> data, send a Content-Type: application/atom+xml header to tell shindig to
> expect atom xml :) In other words it now supports all the write actions that
> the gadgets have access too.
>
> 'Extra' actions (like adding friend relationships by doing a put to
> /people/...) isn't supported yet, and to be honest i would first like to see
> an overview of the expected actions that we should add before i start on
> that, because i don't like guessing :)
>
> Also I'm not clear yet on how requestShareApp should work through the
> RESTful API, so i'm waiting for input on that before i can put that in
> place. (cc to John Panzer, is that information available?)
>
> Also PUT /messages/{guid}/outbox style message creation has been added,
> though the sample container implementation just returns a NOT_SUPPORTED
> error, but the structure is in place (and has been tested) so containers
> that choose to support this now can.
>
> With that the RESTful API support is pretty much 'done' on the PHP side..
> (It could use some extra testing, but it works with my test scripts that
> tries all possible actions that i could come up with).
>
> www.partuza.nl / modules.partuza.nl is running the latest code, so if you
> feel like kicking the tires, feel free :)
>
>        -- Chris
>
> On Jul 19, 2008, at 1:04 AM, Chris Chabot wrote:
>
>  This has been a bit of a headache to develop (see spec list) but it's
>> starting to take some shape. It's also quite difficult to test for me, since
>> nothing in PHP really supports multipart http posts... So i had to hand
>> craft both a test client, the input parsing in shindig, and the multi part
>> output construct too.
>>
>> In other words, it works with the input that my test client creates, but i
>> have no way to absolutely verify it is all completely valid http protocol
>> compliant stuff ... ah the joys of developing in the dark with blindfolds on
>> :)
>>
>> The batch proxy is advertised through XRDS simple:
>> http://www.chabotc.com/xrds-test.php?url=http://www.partuza.nl
>>
>> When i shoot a hand crafted multipart payload into it it works & you can
>> specify how you want your (presumably http multipart compliant) output on
>> the main url (to batchProxy) with ?format=atom or ?format=json (json is
>> assumed by default). You could even poke at it at
>> http://modules.partuza.nl/social/rest/batchProxy .. just make sure to
>> grab a valid token from a gadget iframe first to use in your requests and
>> your golden :) The only missing bits are OAuth support, and Atom format
>> input support, they will hopefully follow shortly.
>>
>> However my main question ... does anyone have any useful tools for
>> actually testing and verifying multipart http posts & the multipart response
>> ? :)
>>
>>        -- Chris
>>
>> Begin forwarded message:
>>
>>  From: chabotc@apache.org
>>> Date: July 19, 2008 12:47:41 AM GMT+02:00
>>> To: shindig-commits@incubator.apache.org
>>> Subject: svn commit: r678068 - in
>>> /incubator/shindig/trunk/php/src/social-api: converters/ dataservice/ http/
>>> samplecontainer/
>>> Reply-To: shindig-dev@incubator.apache.org
>>>
>>> Author: chabotc
>>> Date: Fri Jul 18 15:47:41 2008
>>> New Revision: 678068
>>>
>>> URL: http://svn.apache.org/viewvc?rev=678068&view=rev
>>> Log:
>>> Initial support for the batch proxy request type.
>>>
>>> The url is /social/rest/batchProxy and it uses the http multipart
>>> format as described in the RESTful API specification.
>>>
>>> It only supports ONE output format for a set of requests (which
>>> is determined by the main url ?format=foo param, and defaults to
>>> json). Input however will support mixing json and atom.
>>>
>>> Atom input is still missing (has a debug dump right now) but support
>>> for that will follow quickly.
>>>
>>> OAuth and Atom input is still missing, but once their done PHP
>>> Shindig will have full RESTful spec support, we're getting there!
>>>
>>>
>>> Modified:
>>>
>>>  incubator/shindig/trunk/php/src/social-api/converters/OutputAtomConverter.php
>>>
>>>  incubator/shindig/trunk/php/src/social-api/converters/OutputConverter.php
>>>
>>>  incubator/shindig/trunk/php/src/social-api/converters/OutputJsonConverter.php
>>>  incubator/shindig/trunk/php/src/social-api/dataservice/PeopleHandler.php
>>>
>>>  incubator/shindig/trunk/php/src/social-api/dataservice/RestRequestItem.php
>>>  incubator/shindig/trunk/php/src/social-api/http/RestServlet.php
>>>
>>>  incubator/shindig/trunk/php/src/social-api/samplecontainer/BasicAppDataService.php
>>>
>>>  incubator/shindig/trunk/php/src/social-api/samplecontainer/BasicPeopleService.php
>>>
>>> Modified:
>>> incubator/shindig/trunk/php/src/social-api/converters/OutputAtomConverter.php
>>> URL:
>>> http://svn.apache.org/viewvc/incubator/shindig/trunk/php/src/social-api/converters/OutputAtomConverter.php?rev=678068&r1=678067&r2=678068&view=diff
>>>
>>> ==============================================================================
>>> ---
>>> incubator/shindig/trunk/php/src/social-api/converters/OutputAtomConverter.php
>>> (original)
>>> +++
>>> incubator/shindig/trunk/php/src/social-api/converters/OutputAtomConverter.php
>>> Fri Jul 18 15:47:41 2008
>>> @@ -1,4 +1,5 @@
>>> <?php
>>> +
>>> /*
>>> * Licensed to the Apache Software Foundation (ASF) under one
>>> * or more contributor license agreements. See the NOTICE file
>>> @@ -19,7 +20,7 @@
>>>
>>> /**
>>> * Format = atom output converter, for format definition see:
>>> - *
>>> http://www.opensocial.org/Technical-Resources/opensocial-specification----implementation-version-08/restful-api-specification
>>> + *
>>> http://sites.google.com/a/opensocial.org/opensocial/Technical-Resources/opensocial-spec-v08/restful-api-specification
>>> */
>>> class OutputAtomConverter extends OutputConverter {
>>>        private static $nameSpace = 'http://www.w3.org/2005/Atom';
>>> @@ -40,7 +41,7 @@
>>>                $data = $responseItem->getResponse();
>>>                $userId =
>>> $requestItem->getUser()->getUserId($requestItem->getToken());
>>>                $guid = 'urn:guid:' . $userId;
>>> -               $authorName = $_SERVER['HTTP_HOST'].':'.$userId;
>>> +               $authorName = $_SERVER['HTTP_HOST'] . ':' . $userId;
>>>                $updatedAtom = date(DATE_ATOM);
>>>
>>>                // Check to see if this is a single entry, or a
>>> collection, and construct either an atom
>>> @@ -52,20 +53,18 @@
>>>
>>>                        // The root Feed element
>>>                        $entry = $this->addNode($doc, 'feed', '', false,
>>> self::$nameSpace);
>>> -
>>> +
>>>                        // Required Atom fields
>>>                        $endPos = ($startIndex + $itemsPerPage) >
>>> $totalResults ? $totalResults : ($startIndex + $itemsPerPage);
>>> -                       $this->addNode($entry, 'title', $requestType.'
>>> feed for id '.$authorName.' ('.$startIndex. ' - '. ($endPos - 1).' of
>>> '.$totalResults.')');
>>> +                       $this->addNode($entry, 'title', $requestType . '
>>> feed for id ' . $authorName . ' (' . $startIndex . ' - ' . ($endPos - 1) . '
>>> of ' . $totalResults . ')');
>>>                        $author = $this->addNode($entry, 'author');
>>>                        $this->addNode($author, 'uri', $guid);
>>> -                       $this->addNode($author, 'name', $authorName);
>>>
>>> +                       $this->addNode($author, 'name', $authorName);
>>>                        $this->addNode($entry, 'updated', $updatedAtom);
>>>                        $this->addNode($entry, 'id', $guid);
>>> -                       $this->addNode($entry, 'link', '', array('rel' =>
>>> 'self', 'href' => 'http://
>>> '.$_SERVER['HTTP_HOST'].$_SERVER['REQUEST_URI']));
>>> -
>>> +                       $this->addNode($entry, 'link', '', array('rel' =>
>>> 'self', 'href' => 'http://' . $_SERVER['HTTP_HOST'] .
>>> $_SERVER['REQUEST_URI']));
>>>                        // Add osearch & next link to the entry
>>>                        $this->addPagingFields($entry, $startIndex,
>>> $itemsPerPage, $totalResults);
>>> -
>>>                        // Add response entries to feed
>>>                        $responses =
>>> $responseItem->getResponse()->getEntry();
>>>                        foreach ($responses as $response) {
>>> @@ -79,11 +78,10 @@
>>>                                $this->addNode($author, 'uri', $guid);
>>>                                $this->addNode($author, 'name',
>>> $authorName);
>>>                                // Special hoisting rules for activities
>>> -
>>>                                if ($response instanceof Activity) {
>>>                                        $this->addNode($feedEntry,
>>> 'category', '', array('term' => 'status'));
>>>                                        $this->addNode($feedEntry,
>>> 'updated', date(DATE_ATOM, $response->postedTime));
>>> -                                       $this->addNode($feedEntry, 'id',
>>> 'urn:guid:'.$response->id);
>>> +                                       $this->addNode($feedEntry, 'id',
>>> 'urn:guid:' . $response->id);
>>>                                        //FIXME should add a link field
>>> but don't have URL's available yet:
>>>                                        // <link rel="self"
>>> type="application/atom+xml" href="
>>> http://api.example.org/activity/feeds/.../af3778"/>
>>>                                        $this->addNode($feedEntry,
>>> 'title', strip_tags($response->title));
>>> @@ -94,43 +92,49 @@
>>>                                        unset($response->title);
>>>                                        unset($response->body);
>>>                                } else {
>>> -                                       $this->addNode($feedEntry, 'id',
>>> 'urn:guid:'.$idField);
>>> -                                       $this->addNode($feedEntry,
>>> 'title', $requestType.' feed entry for id '.$idField);
>>> +                                       $this->addNode($feedEntry, 'id',
>>> 'urn:guid:' . $idField);
>>> +                                       $this->addNode($feedEntry,
>>> 'title', $requestType . ' feed entry for id ' . $idField);
>>>                                        $this->addNode($feedEntry,
>>> 'updated', $updatedAtom);
>>>                                }
>>>
>>>                                // recursively add responseItem data to
>>> the xml structure
>>>                                $this->addData($content, $requestType,
>>> $response, self::$osNameSpace);
>>>                        }
>>> -
>>>                } else {
>>> -
>>>                        // Single entry = Atom:Entry
>>>                        $entry =
>>> $doc->appendChild($doc->createElementNS(self::$nameSpace, "entry"));
>>> -
>>>                        // Atom fields
>>> -                       $this->addNode($entry, 'title', $requestType.'
>>> entry for '.$authorName);
>>> +                       $this->addNode($entry, 'title', $requestType . '
>>> entry for ' . $authorName);
>>>                        $author = $this->addNode($entry, 'author');
>>>                        $this->addNode($author, 'uri', $guid);
>>>                        $this->addNode($author, 'name', $authorName);
>>>                        $this->addNode($entry, 'id', $guid);
>>>                        $this->addNode($entry, 'updated', $updatedAtom);
>>>                        $content = $this->addNode($entry, 'content', '',
>>> array('type' => 'application/xml'));
>>> -
>>>                        // addData loops through the responseItem data
>>> recursively creating a matching XML structure
>>>                        $this->addData($content, $requestType, $data,
>>> self::$osNameSpace);
>>>                }
>>>                $xml = $doc->saveXML();
>>> -               if (self::$includeOsearch && $responseItem->getResponse()
>>> instanceof RestFulCollection) {
>>> +               if ($responseItem->getResponse() instanceof
>>> RestFulCollection) {
>>>                        //FIXME dirty hack until i find a way to add
>>> multiple name spaces using DomXML functions
>>> -                       $xml = str_replace('<feed xmlns="
>>> http://www.w3.org/2005/Atom">', '<feed xmlns="
>>> http://www.w3.org/2005/Atom" xmlos:osearch="
>>> http://a9.com/-/spec/opensearch/1.1">' ,$xml);
>>> +                       $xml = str_replace('<feed xmlns="
>>> http://www.w3.org/2005/Atom">', '<feed xmlns="
>>> http://www.w3.org/2005/Atom" xmlns:osearch="
>>> http://a9.com/-/spec/opensearch/1.1">', $xml);
>>>                }
>>>                echo $xml;
>>>        }
>>>
>>>        function outputBatch(Array $responses, SecurityToken $token)
>>>        {
>>> -               //TODO once we support spec compliance batching, this
>>> needs to be added too
>>> +               $this->boundryHeaders();
>>> +               foreach ($responses as $response) {
>>> +                       $request = $response['request'];
>>> +                       $response = $response['response'];
>>> +                       // output buffering supports multiple levels of
>>> it.. it's a nice feature to abuse :)
>>> +                       ob_start();
>>> +                       $this->outputResponse($response, $request);
>>> +                       $part = ob_get_contents();
>>> +                       ob_end_clean();
>>> +                       $this->outputPart($part, $response->getError());
>>> +               }
>>>        }
>>>
>>>        /**
>>> @@ -174,11 +178,9 @@
>>>         */
>>>        private function addPagingFields($entry, $startIndex,
>>> $itemsPerPage, $totalResults)
>>>        {
>>> -               if (self::$includeOsearch) {
>>> -                       $this->addNode($entry, 'osearch:totalResults',
>>> $totalResults);
>>> -                       $this->addNode($entry, 'osearch:startIndex',
>>> $startIndex ? $startIndex : '0');
>>> -                       $this->addNode($entry, 'osearch:itemsPerPage',
>>> $itemsPerPage);
>>> -               }
>>> +               $this->addNode($entry, 'osearch:totalResults',
>>> $totalResults);
>>> +               $this->addNode($entry, 'osearch:startIndex', $startIndex
>>> ? $startIndex : '0');
>>> +               $this->addNode($entry, 'osearch:itemsPerPage',
>>> $itemsPerPage);
>>>                // Create a 'next' link based on our current url if this
>>> is a pageable collection & there is more to display
>>>                if (($startIndex + $itemsPerPage) < $totalResults) {
>>>                        $nextStartIndex = ($startIndex + $itemsPerPage) -
>>> 1;
>>> @@ -255,6 +257,9 @@
>>>                                        }
>>>                                        $this->addData($newElement, $key,
>>> $val);
>>>                                } else {
>>> +                                       if (is_numeric($key)) {
>>> +                                               $key = is_object($val) ?
>>> get_class($val) : $key = $name;
>>> +                                       }
>>>
>>>                                        $elm =
>>> $newElement->appendChild($this->doc->createElement($key));
>>>
>>>  $elm->appendChild($this->doc->createTextNode($val));
>>>                                }
>>>
>>> Modified:
>>> incubator/shindig/trunk/php/src/social-api/converters/OutputConverter.php
>>> URL:
>>> http://svn.apache.org/viewvc/incubator/shindig/trunk/php/src/social-api/converters/OutputConverter.php?rev=678068&r1=678067&r2=678068&view=diff
>>>
>>> ==============================================================================
>>> ---
>>> incubator/shindig/trunk/php/src/social-api/converters/OutputConverter.php
>>> (original)
>>> +++
>>> incubator/shindig/trunk/php/src/social-api/converters/OutputConverter.php
>>> Fri Jul 18 15:47:41 2008
>>> @@ -22,6 +22,50 @@
>>> *
>>> */
>>> abstract class OutputConverter {
>>> +       private $boundry;
>>> +
>>>        abstract function outputResponse(ResponseItem $responseItem,
>>> RestRequestItem $requestItem);
>>>        abstract function outputBatch(Array $responses, SecurityToken
>>> $token);
>>> +
>>> +       /**
>>> +        * Output the multipart/mixed headers and returns the boundry
>>> token used
>>> +        *
>>> +        */
>>> +       public function boundryHeaders()
>>> +       {
>>> +               $this->boundry = '--batch-'.md5(rand(0,32000));
>>> +               header("HTTP/1.1 200 OK", true);
>>> +               header("Content-Type: multipart/mixed;
>>> boundary=$this->boundry", true);
>>> +       }
>>> +
>>> +       public function outputPart($part, $code)
>>> +       {
>>> +               $boundryHeader = "{$this->boundry}\n".
>>> +                               "Content-Type:
>>> application/http;version=1.1\n".
>>> +                               "Content-Transfer-Encoding: binary\n\n";
>>> +               echo $boundryHeader;
>>> +               switch ($code) {
>>> +                       case BAD_REQUEST:
>>> +                               $code = '400 Bad Request';
>>> +                               break;
>>> +                       case UNAUTHORIZED:
>>> +                               $code = '401 Unauthorized';
>>> +                               break;
>>> +                       case FORBIDDEN:
>>> +                               $code = '403 Forbidden';
>>> +                               break;
>>> +                       case FORBIDDEN:
>>> +                               $code = '404 Not Found';
>>> +                               break;
>>> +                       case NOT_IMPLEMENTED:
>>> +                               $code = '501 Not Implemented';
>>> +                               break;
>>> +                       case INTERNAL_ERROR:
>>> +                       default:
>>> +                               $code = '200 OK';
>>> +                               break;
>>> +               }
>>> +               echo "$code\n\n";
>>> +               echo $part."\n";
>>> +       }
>>> }
>>>
>>> Modified:
>>> incubator/shindig/trunk/php/src/social-api/converters/OutputJsonConverter.php
>>> URL:
>>> http://svn.apache.org/viewvc/incubator/shindig/trunk/php/src/social-api/converters/OutputJsonConverter.php?rev=678068&r1=678067&r2=678068&view=diff
>>>
>>> ==============================================================================
>>> ---
>>> incubator/shindig/trunk/php/src/social-api/converters/OutputJsonConverter.php
>>> (original)
>>> +++
>>> incubator/shindig/trunk/php/src/social-api/converters/OutputJsonConverter.php
>>> Fri Jul 18 15:47:41 2008
>>> @@ -30,6 +30,17 @@
>>>
>>>        function outputBatch(Array $responses, SecurityToken $token)
>>>        {
>>> +               $this->boundryHeaders();
>>> +               foreach ($responses as $response) {
>>> +                       $request = $response['request'];
>>> +                       $response = $response['response'];
>>> +                       $part = json_encode($response);
>>> +                       $this->outputPart($part, $response->getError());
>>> +               }
>>> +       }
>>> +
>>> +       function outputJsonBatch(Array $responses, SecurityToken $token)
>>> +       {
>>>                echo json_encode(array("responses" => $responses, "error"
>>> => false));
>>>        }
>>> }
>>>
>>> Modified:
>>> incubator/shindig/trunk/php/src/social-api/dataservice/PeopleHandler.php
>>> URL:
>>> http://svn.apache.org/viewvc/incubator/shindig/trunk/php/src/social-api/dataservice/PeopleHandler.php?rev=678068&r1=678067&r2=678068&view=diff
>>>
>>> ==============================================================================
>>> ---
>>> incubator/shindig/trunk/php/src/social-api/dataservice/PeopleHandler.php
>>> (original)
>>> +++
>>> incubator/shindig/trunk/php/src/social-api/dataservice/PeopleHandler.php Fri
>>> Jul 18 15:47:41 2008
>>> @@ -20,7 +20,9 @@
>>> class PeopleHandler extends DataRequestHandler {
>>>        private $service;
>>>        private static $PEOPLE_PATH =
>>> "/people/{userId}/{groupId}/{personId}";
>>> -       protected static $DEFAULT_PERSON_FIELDS = array("id", "name",
>>> "thumbnailUrl");
>>> +       //FIXME change this back to array("id", "name", "thumbnailUrl")
>>> once the dust settles
>>> +       // on the spec discussion related to this
>>> +       protected static $DEFAULT_PERSON_FIELDS = array('all' => 'all');
>>>
>>>        public function __construct()
>>>        {
>>>
>>> Modified:
>>> incubator/shindig/trunk/php/src/social-api/dataservice/RestRequestItem.php
>>> URL:
>>> http://svn.apache.org/viewvc/incubator/shindig/trunk/php/src/social-api/dataservice/RestRequestItem.php?rev=678068&r1=678067&r2=678068&view=diff
>>>
>>> ==============================================================================
>>> ---
>>> incubator/shindig/trunk/php/src/social-api/dataservice/RestRequestItem.php
>>> (original)
>>> +++
>>> incubator/shindig/trunk/php/src/social-api/dataservice/RestRequestItem.php
>>> Fri Jul 18 15:47:41 2008
>>> @@ -45,12 +45,12 @@
>>>
>>>        public function createRequestItemWithRequest($request, $token)
>>>        {
>>> -               $this->url = $request->url;
>>> -               $this->parameters =
>>> $this->createParameterMap($request->url);
>>> +               $this->url = $request['url'];
>>> +               $this->parameters =
>>> $this->createParameterMap($request['url']);
>>>                $this->token = $token;
>>> -               $this->method = $request->method;
>>> -               if (isset($request->postData)) {
>>> -                       $this->postData = $request->postData;
>>> +               $this->method = $request['method'];
>>> +               if (isset($request['postData'])) {
>>> +                       $this->postData = $request['postData'];
>>>                }
>>>        }
>>>
>>>
>>> Modified: incubator/shindig/trunk/php/src/social-api/http/RestServlet.php
>>> URL:
>>> http://svn.apache.org/viewvc/incubator/shindig/trunk/php/src/social-api/http/RestServlet.php?rev=678068&r1=678067&r2=678068&view=diff
>>>
>>> ==============================================================================
>>> --- incubator/shindig/trunk/php/src/social-api/http/RestServlet.php
>>> (original)
>>> +++ incubator/shindig/trunk/php/src/social-api/http/RestServlet.php Fri
>>> Jul 18 15:47:41 2008
>>> @@ -51,8 +51,6 @@
>>> require 'src/social-api/converters/OutputAtomConverter.php';
>>> require 'src/social-api/converters/OutputJsonConverter.php';
>>>
>>> -//FIXME Delete should respond with a 204 No Content to indicate success
>>> -
>>> class RestException extends Exception {}
>>>
>>> /*
>>> @@ -64,42 +62,80 @@
>>> define('FORBIDDEN', "forbidden");
>>> define('BAD_REQUEST', "badRequest");
>>> define('INTERNAL_ERROR', "internalError");
>>> +//FIXME Delete should respond with a 204 No Content to indicate success
>>>
>>> class RestServlet extends HttpServlet {
>>>
>>> +       // The json Batch Route is used by the gadgets
>>>        private static $JSON_BATCH_ROUTE = "jsonBatch";
>>> +       // The Batch Proxy route is used the one defined in the RESTful
>>> API specification
>>> +       private static $BATCH_PROXY_ROUTE = "batchProxy";
>>> +
>>> +       public function doGet()
>>> +       {
>>> +               $this->doPost('GET');
>>> +       }
>>>
>>> +       public function doPut()
>>> +       {
>>> +               $this->doPost('PUT');
>>> +       }
>>> +
>>> +       public function doDelete()
>>> +       {
>>> +               $this->doPost('DELETE');
>>> +       }
>>> +
>>>        public function doPost($method = 'POST')
>>>        {
>>> -               $this->setNoCache(true);
>>> -               // if oauth, create a token from it's values instead of
>>> one based on $_get['st']/$_post['st']
>>> -               // NOTE : if no token is provided an anonymous one is
>>> created (owner = viewer = appId = modId = 0)
>>> -               // keep this in mind when creating your data services..
>>> -               $token = $this->getSecurityToken();
>>> -               $outputFormat = $this->getOutputFormat();
>>> -               switch ($outputFormat) {
>>> -                       case 'json':
>>> -
>>> $this->setContentType('application/json');
>>> -                               $outputConverter = new
>>> OutputJsonConverter();
>>> -                               break;
>>> -                       case 'atom':
>>> -
>>> $this->setContentType('application/atom+xml');
>>> -                               $outputConverter = new
>>> OutputAtomConverter();
>>> -                               break;
>>> -                       default:
>>> -                               $this->outputError(new
>>> ResponseItem(NOT_IMPLEMENTED, "Invalid output format"));
>>> -                               break;
>>> -               }
>>> -               if ($this->isBatchUrl()) {
>>> -                       $responses = $this->handleBatchRequest($token);
>>> -                       $outputConverter->outputBatch($responses,
>>> $token);
>>> -               } else {
>>> -                       $response = $this->handleSingleRequest($token,
>>> $method);
>>> -
>>> $outputConverter->outputResponse($response['response'],
>>> $response['request']);
>>> +               try {
>>> +                       $this->setNoCache(true);
>>> +                       // if oauth, create a token from it's values
>>> instead of one based on $_get['st']/$_post['st']
>>> +                       // NOTE : if no token is provided an anonymous
>>> one is created (owner = viewer = appId = modId = 0)
>>> +                       // keep this in mind when creating your data
>>> services..
>>> +                       $token = $this->getSecurityToken();
>>> +                       $outputFormat = $this->getOutputFormat();
>>> +                       switch ($outputFormat) {
>>> +                               case 'json':
>>> +
>>> $this->setContentType('application/json');
>>> +                                       $outputConverter = new
>>> OutputJsonConverter();
>>> +                                       break;
>>> +                               case 'atom':
>>> +
>>> $this->setContentType('application/atom+xml');
>>> +                                       $outputConverter = new
>>> OutputAtomConverter();
>>> +                                       break;
>>> +                               default:
>>> +                                       $this->outputError(new
>>> ResponseItem(NOT_IMPLEMENTED, "Invalid output format"));
>>> +                                       break;
>>> +                       }
>>> +                       if ($this->isJsonBatchUrl()) {
>>> +                               // custom json batch format used by the
>>> gadgets
>>> +                               $responses =
>>> $this->handleJsonBatchRequest($token);
>>> +
>>> $outputConverter->outputJsonBatch($responses, $token);
>>> +                       } elseif ($this->isBatchProxyUrl()) {
>>> +                               // spec compliant batch proxy
>>> +                               $this->noHeaders = true;
>>> +                               $responses =
>>> $this->handleBatchProxyRequest($token);
>>> +                               $outputConverter->outputBatch($responses,
>>> $token);
>>> +                       } else {
>>> +                               // single rest request
>>> +                               $response = $this->handleRequest($token,
>>> $method);
>>> +
>>> $outputConverter->outputResponse($response['response'],
>>> $response['request']);
>>> +                       }
>>> +               } catch (Exception $e) {
>>> +                       header("HTTP/1.0 500 Internal Server Error");
>>> +                       echo "<html><body><h1>500 Internal Server
>>> Error</h1>";
>>> +                       if (Config::get('debug')) {
>>> +                               echo "Message: ".$e->getMessage()."<br
>>> />\n";
>>> +                               echo "<pre>\n";
>>> +                               print_r(debug_backtrace());
>>> +                               echo "\n</pre>";
>>> +                       }
>>> +                       echo "</body></html>";
>>>                }
>>>        }
>>>
>>> -       private function handleSingleRequest($token, $method)
>>> +       private function handleRequest($token, $method)
>>>        {
>>>                $params = $this->getListParams();
>>>                $requestItem = new RestRequestItem();
>>> @@ -109,14 +145,115 @@
>>>                $responseItem = $this->getResponseItem($requestItem);
>>>                return array('request' => $requestItem, 'response' =>
>>> $responseItem);
>>>        }
>>> -
>>> -       private function getRouteFromParameter($pathInfo)
>>> +
>>> +       private function handleJsonBatchRequest($token)
>>>        {
>>> -               $pathInfo = substr($pathInfo, 1);
>>> -               $indexOfNextPathSeparator = strpos($pathInfo, "/");
>>> -               return $indexOfNextPathSeparator != - 1 ?
>>> substr($pathInfo, 0, $indexOfNextPathSeparator) : $pathInfo;
>>> +               // we support both a raw http post (without
>>> application/x-www-form-urlencoded headers) like java does
>>> +               // and a more php / curl safe version of a form post with
>>> 'request' as the post field that holds the request json data
>>> +               if (isset($GLOBALS['HTTP_RAW_POST_DATA']) ||
>>> isset($_POST['request'])) {
>>> +                       $requests = $this->getRequestParams();
>>> +                       $responses = array();
>>> +                       foreach ($requests as $key => $value) {
>>> +                               $requestItem = new RestRequestItem();
>>> +
>>> $requestItem->createRequestItemWithRequest($value, $token);
>>> +                               $responses[$key] =
>>> $this->getResponseItem($requestItem);
>>> +                       }
>>> +                       return $responses;
>>> +               } else {
>>> +                       throw new Exception("No post data set");
>>> +               }
>>> +       }
>>> +
>>> +       private function handleBatchProxyRequest($token)
>>> +       {
>>> +               // Is this is a multipath/mixed post? Check content type:
>>> +               if (isset($GLOBALS['HTTP_RAW_POST_DATA']) &&
>>> strpos($_SERVER['CONTENT_TYPE'], 'multipart/mixed') !== false &&
>>> strpos($_SERVER['CONTENT_TYPE'],'boundary=') !== false) {
>>> +                       // Ok looks swell, see what our boundry is..
>>> +                       $boundry = substr($_SERVER['CONTENT_TYPE'],
>>> strpos($_SERVER['CONTENT_TYPE'],'boundary=') + strlen('boundary='));
>>> +                       // Split up requests per boundry
>>> +                       $requests = explode($boundry,
>>> $GLOBALS['HTTP_RAW_POST_DATA']);
>>> +                       $responses = array();
>>> +                       foreach ($requests as $request) {
>>> +                               $request = trim($request);
>>> +                               if (!empty($request)) {
>>> +                                       // extractBatchRequest() does the
>>> magic parsing of the raw post data to a meaninful request array
>>> +                                       $request =
>>> $this->extractBatchRequest($request);
>>> +                                       $requestItem = new
>>> RestRequestItem();
>>> +
>>> $requestItem->createRequestItemWithRequest($request, $token);
>>> +                                       $responses[] = array('request' =>
>>> $requestItem, 'response' => $this->getResponseItem($requestItem));
>>> +                               }
>>> +                       }
>>> +               } else {
>>> +                       $this->outputError(new ResponseItem(BAD_REQUEST,
>>> "Invalid multipart/mixed request"));
>>> +               }
>>> +               return $responses;
>>>        }
>>>
>>> +       private function extractBatchRequest($request)
>>> +       {
>>> +               /* Multipart request is formatted like:
>>> +                * -batch-a73hdj3dy3mm347ddjjdf
>>> +                * Content-Type: application/http;version=1.1
>>> +                * Content-Transfer-Encoding: binary
>>> +                *
>>> +                * GET
>>> /people/@me/@friends?startPage=5&count=10&format=json
>>> +                * Host: api.example.org
>>> +                * If-None-Match: "837dyfkdi39df"
>>> +                *
>>> +                * but we only want to have the last bit (the actual
>>> request), this filters that down first
>>> +                */
>>> +               $emptyFound = false;
>>> +               $requestLines = explode("\n", $request);
>>> +               $request = '';
>>> +               foreach ($requestLines as $line) {
>>> +                       if ($emptyFound) {
>>> +                               $request .= $line."\n";
>>> +                       } elseif (empty($line)) {
>>> +                               $emptyFound = true;
>>> +                       }
>>> +               }
>>> +               // Now that we have the basic request in $request, split
>>> that up again & parse it into a meaningful representation
>>> +               $firstFound = $emptyFound = false;
>>> +               $requestLines = explode("\n", $request);
>>> +               $request = array();
>>> +               $request['headers'] = array();
>>> +               $request['postData'] = '';
>>> +               foreach ($requestLines as $line) {
>>> +                       if (!$firstFound) {
>>> +                               $firstFound = true;
>>> +                               $parts = explode(' ', trim($line));
>>> +                               if (count($parts) != 2) {
>>> +                                       throw new Exception("Mallshaped
>>> request uri in multipart block");
>>> +                               }
>>> +                               $request['method'] =
>>> strtoupper(trim($parts[0]));
>>> +                               // cut it down to an actual meaningful
>>> url without the prefix/social/rest part right away
>>> +                               $request['url'] = substr(trim($parts[1]),
>>> strlen(Config::get('web_prefix') . '/social/rest'));
>>> +                       } elseif (!$emptyFound && !empty($line)) {
>>> +                               // convert the key to the PHP
>>> 'CONTENT_TYPE' style naming convention.. it's ugly but consitent
>>> +                               $key = str_replace('-', '_',
>>> strtoupper(trim(substr($line, 0, strpos($line, ':')))));
>>> +                               $val = trim(substr($line, strpos($line,
>>> ':') + 1));
>>> +                               $request['headers'][$key] = $val;
>>> +                       } elseif (!$emptyFound && empty($line)) {
>>> +                               $emptyFound = true;
>>> +                       } else {
>>> +                               if (get_magic_quotes_gpc()) {
>>> +                                       $line = stripslashes($line);
>>> +                               }
>>> +                               $request['postData'] .= $line."\n";
>>> +                       }
>>> +               }
>>> +               if (empty($request['postData'])) {
>>> +                       // don't trip the requestItem into thinking there
>>> is postData when there's not
>>> +                       unset($request['postData']);
>>> +               } else {
>>> +                       // if there was a post data blob present, decode
>>> it into an array, the format is based on the
>>> +                       // content type header, which is either
>>> application/json or
>>> +                       $format =
>>> isset($request['headers']['CONTENT_TYPE']) &&
>>> strtolower($request['headers']['CONTENT_TYPE']) == 'application/atom+xml' ?
>>> 'atom' : 'json';
>>> +                       $request['postData'] =
>>> $this->decodeRequests($request['postData'], $format);
>>> +               }
>>> +               return $request;
>>> +       }
>>> +
>>>        private function getResponseItem(RestRequestItem $requestItem)
>>>        {
>>>                $path =
>>> $this->getRouteFromParameter($requestItem->getUrl());
>>> @@ -140,12 +277,26 @@
>>>                        $class = new $class(null);
>>>                        $response = $class->handleMethod($requestItem);
>>>                }
>>> -               if ($response->getError() != null &&
>>> !$this->isBatchUrl()) {
>>> +               if ($response->getError() != null &&
>>> !$this->isJsonBatchUrl() && !$this->isBatchProxyUrl()) {
>>>                        // Can't use http error codes in batch mode,
>>> instead we return the error code in the response item
>>>                        $this->outputError($response);
>>>                }
>>>                return $response;
>>>        }
>>> +
>>> +       private function decodeRequests($requestParam, $format = 'json')
>>> +       {
>>> +               // temp hack until i know what the intended way to detect
>>> format is
>>> +               if ($format == 'json') {
>>> +                       return json_decode($requestParam, true);
>>> +               } elseif ($format == 'atom') {
>>> +                       $xml = simplexml_load_string($requestParam);
>>> +                       print_r($xml);
>>> +                       return $xml;
>>> +               } else {
>>> +                       throw Exception("Invalid or unsupported input
>>> format");
>>> +               }
>>> +       }
>>>
>>>        private function getRequestParams()
>>>        {
>>> @@ -154,44 +305,14 @@
>>>                if (get_magic_quotes_gpc()) {
>>>                        $requestParam = stripslashes($requestParam);
>>>                }
>>> -               $requests = json_decode($requestParam);
>>> -               if ($requests == (isset($GLOBALS['HTTP_RAW_POST_DATA']) ?
>>> $GLOBALS['HTTP_RAW_POST_DATA'] : $post)) {
>>> -                       return new ResponseItem(BAD_REQUEST, "Malformed
>>> json string");
>>> -               }
>>> -               return $requests;
>>> -       }
>>> -
>>> -       private function handleBatchRequest($token)
>>> -       {
>>> -               // we support both a raw http post (without
>>> application/x-www-form-urlencoded headers) like java does
>>> -               // and a more php / curl safe version of a form post with
>>> 'request' as the post field that holds the request json data
>>> -               if (isset($GLOBALS['HTTP_RAW_POST_DATA']) ||
>>> isset($_POST['request'])) {
>>> -                       $requests = $this->getRequestParams();
>>> -                       $responses = array();
>>> -                       foreach ($requests as $key => $value) {
>>> -                               $requestItem = new RestRequestItem();
>>> -
>>> $requestItem->createRequestItemWithRequest($value, $token);
>>> -                               $responses[$key] =
>>> $this->getResponseItem($requestItem);
>>> -                       }
>>> -                       return $responses;
>>> -               } else {
>>> -                       throw new Exception("No post data set");
>>> -               }
>>> -       }
>>> -
>>> -       public function doGet()
>>> -       {
>>> -               $this->doPost('GET');
>>> -       }
>>> -
>>> -       public function doPut()
>>> -       {
>>> -               $this->doPost('PUT');
>>> +               return $this->decodeRequests($requestParam);
>>>        }
>>>
>>> -       public function doDelete()
>>> +       private function getRouteFromParameter($pathInfo)
>>>        {
>>> -               $this->doPost('DELETE');
>>> +               $pathInfo = substr($pathInfo, 1);
>>> +               $indexOfNextPathSeparator = strpos($pathInfo, "/");
>>> +               return $indexOfNextPathSeparator != - 1 ?
>>> substr($pathInfo, 0, $indexOfNextPathSeparator) : $pathInfo;
>>>        }
>>>
>>>        private function outputError(ResponseItem $response)
>>> @@ -217,7 +338,6 @@
>>>                        default:
>>>                                $code = '500 Internal Server Error';
>>>                                break;
>>> -
>>>                }
>>>                header("HTTP/1.0 $code", true);
>>>                echo "$code - $errorMessage";
>>> @@ -265,8 +385,13 @@
>>>                return substr($_SERVER["REQUEST_URI"],
>>> strlen(Config::get('web_prefix') . '/social/rest'));
>>>        }
>>>
>>> -       public function isBatchUrl()
>>> +       public function isJsonBatchUrl()
>>>        {
>>>                return strrpos($_SERVER["REQUEST_URI"],
>>> RestServlet::$JSON_BATCH_ROUTE) > 0;
>>>        }
>>> +
>>> +       public function isBatchProxyUrl()
>>> +       {
>>> +               return strrpos($_SERVER["REQUEST_URI"],
>>> RestServlet::$BATCH_PROXY_ROUTE) > 0;
>>> +       }
>>> }
>>>
>>> Modified:
>>> incubator/shindig/trunk/php/src/social-api/samplecontainer/BasicAppDataService.php
>>> URL:
>>> http://svn.apache.org/viewvc/incubator/shindig/trunk/php/src/social-api/samplecontainer/BasicAppDataService.php?rev=678068&r1=678067&r2=678068&view=diff
>>>
>>> ==============================================================================
>>> ---
>>> incubator/shindig/trunk/php/src/social-api/samplecontainer/BasicAppDataService.php
>>> (original)
>>> +++
>>> incubator/shindig/trunk/php/src/social-api/samplecontainer/BasicAppDataService.php
>>> Fri Jul 18 15:47:41 2008
>>> @@ -85,7 +85,7 @@
>>>                switch($groupId->getType()) {
>>>                        case 'self':
>>>                                foreach ($fields as $key) {
>>> -                                       $value = isset($values->$key) ?
>>> $values->$key : (@isset($values[$key]) ? @$values[$key] : null);
>>> +                                       $value = isset($values[$key]) ?
>>> @$values[$key] : null;
>>>
>>>  XmlStateFileFetcher::get()->setAppData($userId->getUserId($token), $key,
>>> $value);
>>>                                }
>>>                                break;
>>>
>>> Modified:
>>> incubator/shindig/trunk/php/src/social-api/samplecontainer/BasicPeopleService.php
>>> URL:
>>> http://svn.apache.org/viewvc/incubator/shindig/trunk/php/src/social-api/samplecontainer/BasicPeopleService.php?rev=678068&r1=678067&r2=678068&view=diff
>>>
>>> ==============================================================================
>>> ---
>>> incubator/shindig/trunk/php/src/social-api/samplecontainer/BasicPeopleService.php
>>> (original)
>>> +++
>>> incubator/shindig/trunk/php/src/social-api/samplecontainer/BasicPeopleService.php
>>> Fri Jul 18 15:47:41 2008
>>> @@ -76,7 +76,7 @@
>>>                                if ($id == $token->getOwnerId()) {
>>>                                        $person->setIsOwner(true);
>>>                                }
>>> -                               if (is_array($profileDetails) &&
>>> count($profileDetails)) {
>>> +                               if (is_array($profileDetails) &&
>>> count($profileDetails) && !in_array('all', $profileDetails)) {
>>>                                        $newPerson = array();
>>>                                        $newPerson['isOwner'] =
>>> $person->isOwner;
>>>                                        $newPerson['isViewer'] =
>>> $person->isViewer;
>>>
>>>
>>
>

RESTful API progress

Posted by Chris Chabot <ch...@xs4all.nl>.
Atom format input is now supported for creating activities and setting  
app data, send a Content-Type: application/atom+xml header to tell  
shindig to expect atom xml :) In other words it now supports all the  
write actions that the gadgets have access too.

'Extra' actions (like adding friend relationships by doing a put to / 
people/...) isn't supported yet, and to be honest i would first like  
to see an overview of the expected actions that we should add before i  
start on that, because i don't like guessing :)

Also I'm not clear yet on how requestShareApp should work through the  
RESTful API, so i'm waiting for input on that before i can put that in  
place. (cc to John Panzer, is that information available?)

Also PUT /messages/{guid}/outbox style message creation has been  
added, though the sample container implementation just returns a  
NOT_SUPPORTED error, but the structure is in place (and has been  
tested) so containers that choose to support this now can.

With that the RESTful API support is pretty much 'done' on the PHP  
side.. (It could use some extra testing, but it works with my test  
scripts that tries all possible actions that i could come up with).

www.partuza.nl / modules.partuza.nl is running the latest code, so if  
you feel like kicking the tires, feel free :)

	-- Chris

On Jul 19, 2008, at 1:04 AM, Chris Chabot wrote:

> This has been a bit of a headache to develop (see spec list) but  
> it's starting to take some shape. It's also quite difficult to test  
> for me, since nothing in PHP really supports multipart http posts...  
> So i had to hand craft both a test client, the input parsing in  
> shindig, and the multi part output construct too.
>
> In other words, it works with the input that my test client creates,  
> but i have no way to absolutely verify it is all completely valid  
> http protocol compliant stuff ... ah the joys of developing in the  
> dark with blindfolds on :)
>
> The batch proxy is advertised through XRDS simple:
> http://www.chabotc.com/xrds-test.php?url=http://www.partuza.nl
>
> When i shoot a hand crafted multipart payload into it it works & you  
> can specify how you want your (presumably http multipart compliant)  
> output on the main url (to batchProxy) with ?format=atom or ? 
> format=json (json is assumed by default). You could even poke at it  
> at http://modules.partuza.nl/social/rest/batchProxy .. just make  
> sure to grab a valid token from a gadget iframe first to use in your  
> requests and your golden :) The only missing bits are OAuth support,  
> and Atom format input support, they will hopefully follow shortly.
>
> However my main question ... does anyone have any useful tools for  
> actually testing and verifying multipart http posts & the multipart  
> response ? :)
>
> 	-- Chris
>
> Begin forwarded message:
>
>> From: chabotc@apache.org
>> Date: July 19, 2008 12:47:41 AM GMT+02:00
>> To: shindig-commits@incubator.apache.org
>> Subject: svn commit: r678068 - in /incubator/shindig/trunk/php/src/ 
>> social-api: converters/ dataservice/ http/ samplecontainer/
>> Reply-To: shindig-dev@incubator.apache.org
>>
>> Author: chabotc
>> Date: Fri Jul 18 15:47:41 2008
>> New Revision: 678068
>>
>> URL: http://svn.apache.org/viewvc?rev=678068&view=rev
>> Log:
>> Initial support for the batch proxy request type.
>>
>> The url is /social/rest/batchProxy and it uses the http multipart
>> format as described in the RESTful API specification.
>>
>> It only supports ONE output format for a set of requests (which
>> is determined by the main url ?format=foo param, and defaults to
>> json). Input however will support mixing json and atom.
>>
>> Atom input is still missing (has a debug dump right now) but support
>> for that will follow quickly.
>>
>> OAuth and Atom input is still missing, but once their done PHP
>> Shindig will have full RESTful spec support, we're getting there!
>>
>>
>> Modified:
>>   incubator/shindig/trunk/php/src/social-api/converters/ 
>> OutputAtomConverter.php
>>   incubator/shindig/trunk/php/src/social-api/converters/ 
>> OutputConverter.php
>>   incubator/shindig/trunk/php/src/social-api/converters/ 
>> OutputJsonConverter.php
>>   incubator/shindig/trunk/php/src/social-api/dataservice/ 
>> PeopleHandler.php
>>   incubator/shindig/trunk/php/src/social-api/dataservice/ 
>> RestRequestItem.php
>>   incubator/shindig/trunk/php/src/social-api/http/RestServlet.php
>>   incubator/shindig/trunk/php/src/social-api/samplecontainer/ 
>> BasicAppDataService.php
>>   incubator/shindig/trunk/php/src/social-api/samplecontainer/ 
>> BasicPeopleService.php
>>
>> Modified: incubator/shindig/trunk/php/src/social-api/converters/ 
>> OutputAtomConverter.php
>> URL: http://svn.apache.org/viewvc/incubator/shindig/trunk/php/src/social-api/converters/OutputAtomConverter.php?rev=678068&r1=678067&r2=678068&view=diff
>> = 
>> = 
>> = 
>> = 
>> = 
>> = 
>> = 
>> = 
>> = 
>> =====================================================================
>> --- incubator/shindig/trunk/php/src/social-api/converters/ 
>> OutputAtomConverter.php (original)
>> +++ incubator/shindig/trunk/php/src/social-api/converters/ 
>> OutputAtomConverter.php Fri Jul 18 15:47:41 2008
>> @@ -1,4 +1,5 @@
>> <?php
>> +
>> /*
>> * Licensed to the Apache Software Foundation (ASF) under one
>> * or more contributor license agreements. See the NOTICE file
>> @@ -19,7 +20,7 @@
>>
>> /**
>> * Format = atom output converter, for format definition see:
>> - * http://www.opensocial.org/Technical-Resources/opensocial-specification----implementation-version-08/restful-api-specification
>> + * http://sites.google.com/a/opensocial.org/opensocial/Technical-Resources/opensocial-spec-v08/restful-api-specification
>> */
>> class OutputAtomConverter extends OutputConverter {
>> 	private static $nameSpace = 'http://www.w3.org/2005/Atom';
>> @@ -40,7 +41,7 @@
>> 		$data = $responseItem->getResponse();
>> 		$userId = $requestItem->getUser()->getUserId($requestItem- 
>> >getToken());
>> 		$guid = 'urn:guid:' . $userId;
>> -		$authorName = $_SERVER['HTTP_HOST'].':'.$userId;
>> +		$authorName = $_SERVER['HTTP_HOST'] . ':' . $userId;
>> 		$updatedAtom = date(DATE_ATOM);
>> 		
>> 		// Check to see if this is a single entry, or a collection, and  
>> construct either an atom
>> @@ -52,20 +53,18 @@
>> 			
>> 			// The root Feed element
>> 			$entry = $this->addNode($doc, 'feed', '', false, self:: 
>> $nameSpace);
>> -
>> +			
>> 			// Required Atom fields
>> 			$endPos = ($startIndex + $itemsPerPage) > $totalResults ?  
>> $totalResults : ($startIndex + $itemsPerPage);
>> -			$this->addNode($entry, 'title', $requestType.' feed for id '. 
>> $authorName.' ('.$startIndex. ' - '. ($endPos - 1).' of '. 
>> $totalResults.')');
>> +			$this->addNode($entry, 'title', $requestType . ' feed for id  
>> ' . $authorName . ' (' . $startIndex . ' - ' . ($endPos - 1) . ' of  
>> ' . $totalResults . ')');
>> 			$author = $this->addNode($entry, 'author');
>> 			$this->addNode($author, 'uri', $guid);
>> -			$this->addNode($author, 'name', $authorName);			
>> +			$this->addNode($author, 'name', $authorName);
>> 			$this->addNode($entry, 'updated', $updatedAtom);
>> 			$this->addNode($entry, 'id', $guid);
>> -			$this->addNode($entry, 'link', '', array('rel' => 'self',  
>> 'href' => 'http://'.$_SERVER['HTTP_HOST'].$_SERVER['REQUEST_URI']));
>> -			
>> +			$this->addNode($entry, 'link', '', array('rel' => 'self',  
>> 'href' => 'http://' . $_SERVER['HTTP_HOST'] .  
>> $_SERVER['REQUEST_URI']));
>> 			// Add osearch & next link to the entry
>> 			$this->addPagingFields($entry, $startIndex, $itemsPerPage,  
>> $totalResults);
>> -			
>> 			// Add response entries to feed
>> 			$responses = $responseItem->getResponse()->getEntry();
>> 			foreach ($responses as $response) {
>> @@ -79,11 +78,10 @@
>> 				$this->addNode($author, 'uri', $guid);
>> 				$this->addNode($author, 'name', $authorName);
>> 				// Special hoisting rules for activities
>> -				
>> 				if ($response instanceof Activity) {
>> 					$this->addNode($feedEntry, 'category', '', array('term' =>  
>> 'status'));
>> 					$this->addNode($feedEntry, 'updated', date(DATE_ATOM,  
>> $response->postedTime));
>> -					$this->addNode($feedEntry, 'id', 'urn:guid:'.$response->id);
>> +					$this->addNode($feedEntry, 'id', 'urn:guid:' . $response->id);
>> 					//FIXME should add a link field but don't have URL's available  
>> yet:
>> 					// <link rel="self" type="application/atom+xml" href="http://api.example.org/activity/feeds/.../af3778 
>> "/>
>> 					$this->addNode($feedEntry, 'title', strip_tags($response- 
>> >title));
>> @@ -94,43 +92,49 @@
>> 					unset($response->title);
>> 					unset($response->body);
>> 				} else {
>> -					$this->addNode($feedEntry, 'id', 'urn:guid:'.$idField);
>> -					$this->addNode($feedEntry, 'title', $requestType.' feed entry  
>> for id '.$idField);
>> +					$this->addNode($feedEntry, 'id', 'urn:guid:' . $idField);
>> +					$this->addNode($feedEntry, 'title', $requestType . ' feed  
>> entry for id ' . $idField);
>> 					$this->addNode($feedEntry, 'updated', $updatedAtom);
>> 				}
>> 				
>> 				// recursively add responseItem data to the xml structure
>> 				$this->addData($content, $requestType, $response, self:: 
>> $osNameSpace);
>> 			}
>> -			
>> 		} else {
>> -			
>> 			// Single entry = Atom:Entry	
>> 			$entry = $doc->appendChild($doc->createElementNS(self:: 
>> $nameSpace, "entry"));
>> -			
>> 			// Atom fields
>> -			$this->addNode($entry, 'title', $requestType.' entry for '. 
>> $authorName);
>> +			$this->addNode($entry, 'title', $requestType . ' entry for ' .  
>> $authorName);
>> 			$author = $this->addNode($entry, 'author');
>> 			$this->addNode($author, 'uri', $guid);
>> 			$this->addNode($author, 'name', $authorName);
>> 			$this->addNode($entry, 'id', $guid);
>> 			$this->addNode($entry, 'updated', $updatedAtom);
>> 			$content = $this->addNode($entry, 'content', '', array('type' =>  
>> 'application/xml'));
>> -			
>> 			// addData loops through the responseItem data recursively  
>> creating a matching XML structure
>> 			$this->addData($content, $requestType, $data, self::$osNameSpace);
>> 		}
>> 		$xml = $doc->saveXML();
>> -		if (self::$includeOsearch && $responseItem->getResponse()  
>> instanceof RestFulCollection) {
>> +		if ($responseItem->getResponse() instanceof RestFulCollection) {
>> 			//FIXME dirty hack until i find a way to add multiple name  
>> spaces using DomXML functions
>> -			$xml = str_replace('<feed xmlns="http://www.w3.org/2005/ 
>> Atom">', '<feed xmlns="http://www.w3.org/2005/Atom" xmlos:osearch="http://a9.com/-/spec/opensearch/1.1 
>> ">' ,$xml);
>> +			$xml = str_replace('<feed xmlns="http://www.w3.org/2005/ 
>> Atom">', '<feed xmlns="http://www.w3.org/2005/Atom" xmlns:osearch="http://a9.com/-/spec/opensearch/1.1 
>> ">', $xml);
>> 		}
>> 		echo $xml;
>> 	}
>>
>> 	function outputBatch(Array $responses, SecurityToken $token)
>> 	{
>> -		//TODO once we support spec compliance batching, this needs to  
>> be added too
>> +		$this->boundryHeaders();
>> +		foreach ($responses as $response) {
>> +			$request = $response['request'];
>> +			$response = $response['response'];
>> +			// output buffering supports multiple levels of it.. it's a  
>> nice feature to abuse :)
>> +			ob_start();
>> +			$this->outputResponse($response, $request);
>> +			$part = ob_get_contents();
>> +			ob_end_clean();
>> +			$this->outputPart($part, $response->getError());
>> +		}
>> 	}
>>
>> 	/**
>> @@ -174,11 +178,9 @@
>> 	 */
>> 	private function addPagingFields($entry, $startIndex,  
>> $itemsPerPage, $totalResults)
>> 	{
>> -		if (self::$includeOsearch) {
>> -			$this->addNode($entry, 'osearch:totalResults', $totalResults);
>> -			$this->addNode($entry, 'osearch:startIndex', $startIndex ?  
>> $startIndex : '0');
>> -			$this->addNode($entry, 'osearch:itemsPerPage', $itemsPerPage);
>> -		}
>> +		$this->addNode($entry, 'osearch:totalResults', $totalResults);
>> +		$this->addNode($entry, 'osearch:startIndex', $startIndex ?  
>> $startIndex : '0');
>> +		$this->addNode($entry, 'osearch:itemsPerPage', $itemsPerPage);
>> 		// Create a 'next' link based on our current url if this is a  
>> pageable collection & there is more to display
>> 		if (($startIndex + $itemsPerPage) < $totalResults) {
>> 			$nextStartIndex = ($startIndex + $itemsPerPage) - 1;
>> @@ -255,6 +257,9 @@
>> 					}
>> 					$this->addData($newElement, $key, $val);
>> 				} else {
>> +					if (is_numeric($key)) {
>> +						$key = is_object($val) ? get_class($val) : $key = $name;
>> +					}					
>> 					$elm = $newElement->appendChild($this->doc- 
>> >createElement($key));
>> 					$elm->appendChild($this->doc->createTextNode($val));
>> 				}
>>
>> Modified: incubator/shindig/trunk/php/src/social-api/converters/ 
>> OutputConverter.php
>> URL: http://svn.apache.org/viewvc/incubator/shindig/trunk/php/src/social-api/converters/OutputConverter.php?rev=678068&r1=678067&r2=678068&view=diff
>> = 
>> = 
>> = 
>> = 
>> = 
>> = 
>> = 
>> = 
>> = 
>> =====================================================================
>> --- incubator/shindig/trunk/php/src/social-api/converters/ 
>> OutputConverter.php (original)
>> +++ incubator/shindig/trunk/php/src/social-api/converters/ 
>> OutputConverter.php Fri Jul 18 15:47:41 2008
>> @@ -22,6 +22,50 @@
>> *
>> */
>> abstract class OutputConverter {
>> +	private $boundry;
>> +	
>> 	abstract function outputResponse(ResponseItem $responseItem,  
>> RestRequestItem $requestItem);
>> 	abstract function outputBatch(Array $responses, SecurityToken  
>> $token);
>> +	
>> +	/**
>> +	 * Output the multipart/mixed headers and returns the boundry  
>> token used
>> +	 *
>> +	 */
>> +	public function boundryHeaders()
>> +	{
>> +		$this->boundry = '--batch-'.md5(rand(0,32000));
>> +		header("HTTP/1.1 200 OK", true);
>> +		header("Content-Type: multipart/mixed; boundary=$this->boundry",  
>> true);
>> +	}
>> +	
>> +	public function outputPart($part, $code)
>> +	{
>> +		$boundryHeader = "{$this->boundry}\n".
>> +				"Content-Type: application/http;version=1.1\n".
>> +				"Content-Transfer-Encoding: binary\n\n";
>> +		echo $boundryHeader;
>> +		switch ($code) {
>> +			case BAD_REQUEST:
>> +				$code = '400 Bad Request';
>> +				break;
>> +			case UNAUTHORIZED:
>> +				$code = '401 Unauthorized';
>> +				break;
>> +			case FORBIDDEN:
>> +				$code = '403 Forbidden';
>> +				break;
>> +			case FORBIDDEN:
>> +				$code = '404 Not Found';
>> +				break;
>> +			case NOT_IMPLEMENTED:
>> +				$code = '501 Not Implemented';
>> +				break;
>> +			case INTERNAL_ERROR:
>> +			default:
>> +				$code = '200 OK';
>> +				break;
>> +		}
>> +		echo "$code\n\n";
>> +		echo $part."\n";
>> +	}
>> }
>>
>> Modified: incubator/shindig/trunk/php/src/social-api/converters/ 
>> OutputJsonConverter.php
>> URL: http://svn.apache.org/viewvc/incubator/shindig/trunk/php/src/social-api/converters/OutputJsonConverter.php?rev=678068&r1=678067&r2=678068&view=diff
>> = 
>> = 
>> = 
>> = 
>> = 
>> = 
>> = 
>> = 
>> = 
>> =====================================================================
>> --- incubator/shindig/trunk/php/src/social-api/converters/ 
>> OutputJsonConverter.php (original)
>> +++ incubator/shindig/trunk/php/src/social-api/converters/ 
>> OutputJsonConverter.php Fri Jul 18 15:47:41 2008
>> @@ -30,6 +30,17 @@
>> 	
>> 	function outputBatch(Array $responses, SecurityToken $token)
>> 	{
>> +		$this->boundryHeaders();
>> +		foreach ($responses as $response) {
>> +			$request = $response['request'];
>> +			$response = $response['response'];
>> +			$part = json_encode($response);
>> +			$this->outputPart($part, $response->getError());
>> +		}
>> +	}
>> +	
>> +	function outputJsonBatch(Array $responses, SecurityToken $token)
>> +	{
>> 		echo json_encode(array("responses" => $responses, "error" =>  
>> false));
>> 	}
>> }
>>
>> Modified: incubator/shindig/trunk/php/src/social-api/dataservice/ 
>> PeopleHandler.php
>> URL: http://svn.apache.org/viewvc/incubator/shindig/trunk/php/src/social-api/dataservice/PeopleHandler.php?rev=678068&r1=678067&r2=678068&view=diff
>> = 
>> = 
>> = 
>> = 
>> = 
>> = 
>> = 
>> = 
>> = 
>> =====================================================================
>> --- incubator/shindig/trunk/php/src/social-api/dataservice/ 
>> PeopleHandler.php (original)
>> +++ incubator/shindig/trunk/php/src/social-api/dataservice/ 
>> PeopleHandler.php Fri Jul 18 15:47:41 2008
>> @@ -20,7 +20,9 @@
>> class PeopleHandler extends DataRequestHandler {
>> 	private $service;
>> 	private static $PEOPLE_PATH = "/people/{userId}/{groupId}/ 
>> {personId}";
>> -	protected static $DEFAULT_PERSON_FIELDS = array("id", "name",  
>> "thumbnailUrl");
>> +	//FIXME change this back to array("id", "name", "thumbnailUrl")  
>> once the dust settles
>> +	// on the spec discussion related to this
>> +	protected static $DEFAULT_PERSON_FIELDS = array('all' => 'all');
>>
>> 	public function __construct()
>> 	{
>>
>> Modified: incubator/shindig/trunk/php/src/social-api/dataservice/ 
>> RestRequestItem.php
>> URL: http://svn.apache.org/viewvc/incubator/shindig/trunk/php/src/social-api/dataservice/RestRequestItem.php?rev=678068&r1=678067&r2=678068&view=diff
>> = 
>> = 
>> = 
>> = 
>> = 
>> = 
>> = 
>> = 
>> = 
>> =====================================================================
>> --- incubator/shindig/trunk/php/src/social-api/dataservice/ 
>> RestRequestItem.php (original)
>> +++ incubator/shindig/trunk/php/src/social-api/dataservice/ 
>> RestRequestItem.php Fri Jul 18 15:47:41 2008
>> @@ -45,12 +45,12 @@
>>
>> 	public function createRequestItemWithRequest($request, $token)
>> 	{
>> -		$this->url = $request->url;
>> -		$this->parameters = $this->createParameterMap($request->url);
>> +		$this->url = $request['url'];
>> +		$this->parameters = $this->createParameterMap($request['url']);
>> 		$this->token = $token;
>> -		$this->method = $request->method;
>> -		if (isset($request->postData)) {
>> -			$this->postData = $request->postData;
>> +		$this->method = $request['method'];
>> +		if (isset($request['postData'])) {
>> +			$this->postData = $request['postData'];
>> 		}
>> 	}
>>
>>
>> Modified: incubator/shindig/trunk/php/src/social-api/http/ 
>> RestServlet.php
>> URL: http://svn.apache.org/viewvc/incubator/shindig/trunk/php/src/social-api/http/RestServlet.php?rev=678068&r1=678067&r2=678068&view=diff
>> = 
>> = 
>> = 
>> = 
>> = 
>> = 
>> = 
>> = 
>> = 
>> =====================================================================
>> --- incubator/shindig/trunk/php/src/social-api/http/RestServlet.php  
>> (original)
>> +++ incubator/shindig/trunk/php/src/social-api/http/RestServlet.php  
>> Fri Jul 18 15:47:41 2008
>> @@ -51,8 +51,6 @@
>> require 'src/social-api/converters/OutputAtomConverter.php';
>> require 'src/social-api/converters/OutputJsonConverter.php';
>>
>> -//FIXME Delete should respond with a 204 No Content to indicate  
>> success
>> -
>> class RestException extends Exception {}
>>
>> /*
>> @@ -64,42 +62,80 @@
>> define('FORBIDDEN', "forbidden");
>> define('BAD_REQUEST', "badRequest");
>> define('INTERNAL_ERROR', "internalError");
>> +//FIXME Delete should respond with a 204 No Content to indicate  
>> success
>>
>> class RestServlet extends HttpServlet {
>> 	
>> +	// The json Batch Route is used by the gadgets
>> 	private static $JSON_BATCH_ROUTE = "jsonBatch";
>> +	// The Batch Proxy route is used the one defined in the RESTful  
>> API specification
>> +	private static $BATCH_PROXY_ROUTE = "batchProxy";
>> +
>> +	public function doGet()
>> +	{
>> +		$this->doPost('GET');
>> +	}
>>
>> +	public function doPut()
>> +	{
>> +		$this->doPost('PUT');
>> +	}
>> +
>> +	public function doDelete()
>> +	{
>> +		$this->doPost('DELETE');
>> +	}
>> +		
>> 	public function doPost($method = 'POST')
>> 	{
>> -		$this->setNoCache(true);
>> -		// if oauth, create a token from it's values instead of one  
>> based on $_get['st']/$_post['st']
>> -		// NOTE : if no token is provided an anonymous one is created  
>> (owner = viewer = appId = modId = 0)
>> -		// keep this in mind when creating your data services..
>> -		$token = $this->getSecurityToken();
>> -		$outputFormat = $this->getOutputFormat();
>> -		switch ($outputFormat) {
>> -			case 'json':
>> -				$this->setContentType('application/json');
>> -				$outputConverter = new OutputJsonConverter();
>> -				break;
>> -			case 'atom':
>> -				$this->setContentType('application/atom+xml');
>> -				$outputConverter = new OutputAtomConverter();
>> -				break;
>> -			default:
>> -				$this->outputError(new ResponseItem(NOT_IMPLEMENTED, "Invalid  
>> output format"));
>> -				break;
>> -		}
>> -		if ($this->isBatchUrl()) {
>> -			$responses = $this->handleBatchRequest($token);
>> -			$outputConverter->outputBatch($responses, $token);
>> -		} else {
>> -			$response = $this->handleSingleRequest($token, $method);
>> -			$outputConverter->outputResponse($response['response'],  
>> $response['request']);
>> +		try {
>> +			$this->setNoCache(true);
>> +			// if oauth, create a token from it's values instead of one  
>> based on $_get['st']/$_post['st']
>> +			// NOTE : if no token is provided an anonymous one is created  
>> (owner = viewer = appId = modId = 0)
>> +			// keep this in mind when creating your data services..
>> +			$token = $this->getSecurityToken();
>> +			$outputFormat = $this->getOutputFormat();
>> +			switch ($outputFormat) {
>> +				case 'json':
>> +					$this->setContentType('application/json');
>> +					$outputConverter = new OutputJsonConverter();
>> +					break;
>> +				case 'atom':
>> +					$this->setContentType('application/atom+xml');
>> +					$outputConverter = new OutputAtomConverter();
>> +					break;
>> +				default:
>> +					$this->outputError(new ResponseItem(NOT_IMPLEMENTED, "Invalid  
>> output format"));
>> +					break;
>> +			}
>> +			if ($this->isJsonBatchUrl()) {
>> +				// custom json batch format used by the gadgets
>> +				$responses = $this->handleJsonBatchRequest($token);
>> +				$outputConverter->outputJsonBatch($responses, $token);
>> +			} elseif ($this->isBatchProxyUrl()) {
>> +				// spec compliant batch proxy
>> +				$this->noHeaders = true;
>> +				$responses = $this->handleBatchProxyRequest($token);
>> +				$outputConverter->outputBatch($responses, $token);
>> +			} else {
>> +				// single rest request
>> +				$response = $this->handleRequest($token, $method);
>> +				$outputConverter->outputResponse($response['response'],  
>> $response['request']);
>> +			}
>> +		} catch (Exception $e) {
>> +			header("HTTP/1.0 500 Internal Server Error");
>> +			echo "<html><body><h1>500 Internal Server Error</h1>";
>> +			if (Config::get('debug')) {
>> +				echo "Message: ".$e->getMessage()."<br />\n";
>> +				echo "<pre>\n";
>> +				print_r(debug_backtrace());
>> +				echo "\n</pre>";
>> +			}
>> +			echo "</body></html>";
>> 		}
>> 	}
>>
>> -	private function handleSingleRequest($token, $method)
>> +	private function handleRequest($token, $method)
>> 	{
>> 		$params = $this->getListParams();
>> 		$requestItem = new RestRequestItem();
>> @@ -109,14 +145,115 @@
>> 		$responseItem = $this->getResponseItem($requestItem);
>> 		return array('request' => $requestItem, 'response' =>  
>> $responseItem);
>> 	}
>> -
>> -	private function getRouteFromParameter($pathInfo)
>> +	
>> +	private function handleJsonBatchRequest($token)
>> 	{
>> -		$pathInfo = substr($pathInfo, 1);
>> -		$indexOfNextPathSeparator = strpos($pathInfo, "/");
>> -		return $indexOfNextPathSeparator != - 1 ? substr($pathInfo, 0,  
>> $indexOfNextPathSeparator) : $pathInfo;
>> +		// we support both a raw http post (without application/x-www- 
>> form-urlencoded headers) like java does
>> +		// and a more php / curl safe version of a form post with  
>> 'request' as the post field that holds the request json data
>> +		if (isset($GLOBALS['HTTP_RAW_POST_DATA']) ||  
>> isset($_POST['request'])) {
>> +			$requests = $this->getRequestParams();
>> +			$responses = array();
>> +			foreach ($requests as $key => $value) {
>> +				$requestItem = new RestRequestItem();
>> +				$requestItem->createRequestItemWithRequest($value, $token);
>> +				$responses[$key] = $this->getResponseItem($requestItem);
>> +			}
>> +			return $responses;
>> +		} else {
>> +			throw new Exception("No post data set");
>> +		}
>> +	}
>> +	
>> +	private function handleBatchProxyRequest($token)
>> +	{
>> +		// Is this is a multipath/mixed post? Check content type:
>> +		if (isset($GLOBALS['HTTP_RAW_POST_DATA']) &&  
>> strpos($_SERVER['CONTENT_TYPE'], 'multipart/mixed') !== false &&  
>> strpos($_SERVER['CONTENT_TYPE'],'boundary=') !== false) {
>> +			// Ok looks swell, see what our boundry is..
>> +			$boundry = substr($_SERVER['CONTENT_TYPE'],  
>> strpos($_SERVER['CONTENT_TYPE'],'boundary=') + strlen('boundary='));
>> +			// Split up requests per boundry
>> +			$requests = explode($boundry, $GLOBALS['HTTP_RAW_POST_DATA']);
>> +			$responses = array();
>> +			foreach ($requests as $request) {
>> +				$request = trim($request);
>> +				if (!empty($request)) {
>> +					// extractBatchRequest() does the magic parsing of the raw  
>> post data to a meaninful request array
>> +					$request = $this->extractBatchRequest($request);
>> +					$requestItem = new RestRequestItem();
>> +					$requestItem->createRequestItemWithRequest($request, $token);
>> +					$responses[] = array('request' => $requestItem, 'response' =>  
>> $this->getResponseItem($requestItem));
>> +				}
>> +			}
>> +		} else {
>> +			$this->outputError(new ResponseItem(BAD_REQUEST, "Invalid  
>> multipart/mixed request"));
>> +		}
>> +		return $responses;
>> 	}
>>
>> +	private function extractBatchRequest($request)
>> +	{
>> +		/* Multipart request is formatted like:
>> +		 * -batch-a73hdj3dy3mm347ddjjdf
>> +		 * Content-Type: application/http;version=1.1
>> +		 * Content-Transfer-Encoding: binary
>> +		 *
>> +		 * GET /people/@me/@friends?startPage=5&count=10&format=json
>> +		 * Host: api.example.org
>> +		 * If-None-Match: "837dyfkdi39df"
>> +		 *
>> +		 * but we only want to have the last bit (the actual request),  
>> this filters that down first
>> +		 */
>> +		$emptyFound = false;
>> +		$requestLines = explode("\n", $request);
>> +		$request = '';
>> +		foreach ($requestLines as $line) {
>> +			if ($emptyFound) {
>> +				$request .= $line."\n";
>> +			} elseif (empty($line)) {
>> +				$emptyFound = true;
>> +			}
>> +		}
>> +		// Now that we have the basic request in $request, split that up  
>> again & parse it into a meaningful representation
>> +		$firstFound = $emptyFound = false;
>> +		$requestLines = explode("\n", $request);
>> +		$request = array();
>> +		$request['headers'] = array();
>> +		$request['postData'] = '';
>> +		foreach ($requestLines as $line) {
>> +			if (!$firstFound) {
>> +				$firstFound = true;
>> +				$parts = explode(' ', trim($line));
>> +				if (count($parts) != 2) {
>> +					throw new Exception("Mallshaped request uri in multipart  
>> block");
>> +				}
>> +				$request['method'] = strtoupper(trim($parts[0]));
>> +				// cut it down to an actual meaningful url without the prefix/ 
>> social/rest part right away
>> +				$request['url'] = substr(trim($parts[1]),  
>> strlen(Config::get('web_prefix') . '/social/rest'));
>> +			} elseif (!$emptyFound && !empty($line)) {
>> +				// convert the key to the PHP 'CONTENT_TYPE' style naming  
>> convention.. it's ugly but consitent
>> +				$key = str_replace('-', '_', strtoupper(trim(substr($line, 0,  
>> strpos($line, ':')))));
>> +				$val = trim(substr($line, strpos($line, ':') + 1));
>> +				$request['headers'][$key] = $val;
>> +			} elseif (!$emptyFound && empty($line)) {
>> +				$emptyFound = true;
>> +			} else {
>> +				if (get_magic_quotes_gpc()) {
>> +					$line = stripslashes($line);
>> +				}
>> +				$request['postData'] .= $line."\n";
>> +			}
>> +		}
>> +		if (empty($request['postData'])) {
>> +			// don't trip the requestItem into thinking there is postData  
>> when there's not
>> +			unset($request['postData']);
>> +		} else {
>> +			// if there was a post data blob present, decode it into an  
>> array, the format is based on the
>> +			// content type header, which is either application/json or
>> +			$format = isset($request['headers']['CONTENT_TYPE']) &&  
>> strtolower($request['headers']['CONTENT_TYPE']) == 'application/atom 
>> +xml' ? 'atom' : 'json';
>> +			$request['postData'] = $this- 
>> >decodeRequests($request['postData'], $format);
>> +		}
>> +		return $request;
>> +	}
>> +	
>> 	private function getResponseItem(RestRequestItem $requestItem)
>> 	{
>> 		$path = $this->getRouteFromParameter($requestItem->getUrl());
>> @@ -140,12 +277,26 @@
>> 			$class = new $class(null);
>> 			$response = $class->handleMethod($requestItem);
>> 		}
>> -		if ($response->getError() != null && !$this->isBatchUrl()) {
>> +		if ($response->getError() != null && !$this->isJsonBatchUrl()  
>> && !$this->isBatchProxyUrl()) {
>> 			// Can't use http error codes in batch mode, instead we return  
>> the error code in the response item
>> 			$this->outputError($response);
>> 		}
>> 		return $response;
>> 	}
>> +	
>> +	private function decodeRequests($requestParam, $format = 'json')
>> +	{
>> +		// temp hack until i know what the intended way to detect format  
>> is
>> +		if ($format == 'json') {
>> +			return json_decode($requestParam, true);
>> +		} elseif ($format == 'atom') {
>> +			$xml = simplexml_load_string($requestParam);
>> +			print_r($xml);
>> +			return $xml;
>> +		} else {
>> +			throw Exception("Invalid or unsupported input format");
>> +		}
>> +	}
>>
>> 	private function getRequestParams()
>> 	{
>> @@ -154,44 +305,14 @@
>> 		if (get_magic_quotes_gpc()) {
>> 			$requestParam = stripslashes($requestParam);
>> 		}
>> -		$requests = json_decode($requestParam);
>> -		if ($requests == (isset($GLOBALS['HTTP_RAW_POST_DATA']) ?  
>> $GLOBALS['HTTP_RAW_POST_DATA'] : $post)) {
>> -			return new ResponseItem(BAD_REQUEST, "Malformed json string");
>> -		}
>> -		return $requests;
>> -	}
>> -
>> -	private function handleBatchRequest($token)
>> -	{
>> -		// we support both a raw http post (without application/x-www- 
>> form-urlencoded headers) like java does
>> -		// and a more php / curl safe version of a form post with  
>> 'request' as the post field that holds the request json data
>> -		if (isset($GLOBALS['HTTP_RAW_POST_DATA']) ||  
>> isset($_POST['request'])) {
>> -			$requests = $this->getRequestParams();
>> -			$responses = array();
>> -			foreach ($requests as $key => $value) {
>> -				$requestItem = new RestRequestItem();
>> -				$requestItem->createRequestItemWithRequest($value, $token);
>> -				$responses[$key] = $this->getResponseItem($requestItem);
>> -			}
>> -			return $responses;
>> -		} else {
>> -			throw new Exception("No post data set");
>> -		}
>> -	}
>> -
>> -	public function doGet()
>> -	{
>> -		$this->doPost('GET');
>> -	}
>> -
>> -	public function doPut()
>> -	{
>> -		$this->doPost('PUT');
>> +		return $this->decodeRequests($requestParam);
>> 	}
>>
>> -	public function doDelete()
>> +	private function getRouteFromParameter($pathInfo)
>> 	{
>> -		$this->doPost('DELETE');
>> +		$pathInfo = substr($pathInfo, 1);
>> +		$indexOfNextPathSeparator = strpos($pathInfo, "/");
>> +		return $indexOfNextPathSeparator != - 1 ? substr($pathInfo, 0,  
>> $indexOfNextPathSeparator) : $pathInfo;
>> 	}
>>
>> 	private function outputError(ResponseItem $response)
>> @@ -217,7 +338,6 @@
>> 			default:
>> 				$code = '500 Internal Server Error';
>> 				break;
>> -		
>> 		}
>> 		header("HTTP/1.0 $code", true);
>> 		echo "$code - $errorMessage";
>> @@ -265,8 +385,13 @@
>> 		return substr($_SERVER["REQUEST_URI"],  
>> strlen(Config::get('web_prefix') . '/social/rest'));
>> 	}
>>
>> -	public function isBatchUrl()
>> +	public function isJsonBatchUrl()
>> 	{
>> 		return strrpos($_SERVER["REQUEST_URI"], RestServlet:: 
>> $JSON_BATCH_ROUTE) > 0;
>> 	}
>> +
>> +	public function isBatchProxyUrl()
>> +	{
>> +		return strrpos($_SERVER["REQUEST_URI"], RestServlet:: 
>> $BATCH_PROXY_ROUTE) > 0;
>> +	}
>> }
>>
>> Modified: incubator/shindig/trunk/php/src/social-api/ 
>> samplecontainer/BasicAppDataService.php
>> URL: http://svn.apache.org/viewvc/incubator/shindig/trunk/php/src/social-api/samplecontainer/BasicAppDataService.php?rev=678068&r1=678067&r2=678068&view=diff
>> = 
>> = 
>> = 
>> = 
>> = 
>> = 
>> = 
>> = 
>> = 
>> =====================================================================
>> --- incubator/shindig/trunk/php/src/social-api/samplecontainer/ 
>> BasicAppDataService.php (original)
>> +++ incubator/shindig/trunk/php/src/social-api/samplecontainer/ 
>> BasicAppDataService.php Fri Jul 18 15:47:41 2008
>> @@ -85,7 +85,7 @@
>> 		switch($groupId->getType()) {
>> 			case 'self':
>> 				foreach ($fields as $key) {
>> -					$value = isset($values->$key) ? $values->$key :  
>> (@isset($values[$key]) ? @$values[$key] : null);
>> +					$value = isset($values[$key]) ? @$values[$key] : null;
>> 					XmlStateFileFetcher::get()->setAppData($userId- 
>> >getUserId($token), $key, $value);	
>> 				}
>> 				break;
>>
>> Modified: incubator/shindig/trunk/php/src/social-api/ 
>> samplecontainer/BasicPeopleService.php
>> URL: http://svn.apache.org/viewvc/incubator/shindig/trunk/php/src/social-api/samplecontainer/BasicPeopleService.php?rev=678068&r1=678067&r2=678068&view=diff
>> = 
>> = 
>> = 
>> = 
>> = 
>> = 
>> = 
>> = 
>> = 
>> =====================================================================
>> --- incubator/shindig/trunk/php/src/social-api/samplecontainer/ 
>> BasicPeopleService.php (original)
>> +++ incubator/shindig/trunk/php/src/social-api/samplecontainer/ 
>> BasicPeopleService.php Fri Jul 18 15:47:41 2008
>> @@ -76,7 +76,7 @@
>> 				if ($id == $token->getOwnerId()) {
>> 					$person->setIsOwner(true);
>> 				}
>> -				if (is_array($profileDetails) && count($profileDetails)) {
>> +				if (is_array($profileDetails) && count($profileDetails) && ! 
>> in_array('all', $profileDetails)) {
>> 					$newPerson = array();
>> 					$newPerson['isOwner'] = $person->isOwner;
>> 					$newPerson['isViewer'] = $person->isViewer;
>>
>


Re: initial proxyBatch support

Posted by Ropu <ro...@gmail.com>.
hmmm, if you send a file though a simple <form> isnt it multiparted by the
browser?

ropu

On Fri, Jul 18, 2008 at 8:04 PM, Chris Chabot <ch...@xs4all.nl> wrote:

> This has been a bit of a headache to develop (see spec list) but it's
> starting to take some shape. It's also quite difficult to test for me, since
> nothing in PHP really supports multipart http posts... So i had to hand
> craft both a test client, the input parsing in shindig, and the multi part
> output construct too.
>
> In other words, it works with the input that my test client creates, but i
> have no way to absolutely verify it is all completely valid http protocol
> compliant stuff ... ah the joys of developing in the dark with blindfolds on
> :)
>
> The batch proxy is advertised through XRDS simple:
> http://www.chabotc.com/xrds-test.php?url=http://www.partuza.nl
>
> When i shoot a hand crafted multipart payload into it it works & you can
> specify how you want your (presumably http multipart compliant) output on
> the main url (to batchProxy) with ?format=atom or ?format=json (json is
> assumed by default). You could even poke at it at
> http://modules.partuza.nl/social/rest/batchProxy .. just make sure to grab
> a valid token from a gadget iframe first to use in your requests and your
> golden :) The only missing bits are OAuth support, and Atom format input
> support, they will hopefully follow shortly.
>
> However my main question ... does anyone have any useful tools for actually
> testing and verifying multipart http posts & the multipart response ? :)
>
>        -- Chris
>
> Begin forwarded message:
>
>  From: chabotc@apache.org
>> Date: July 19, 2008 12:47:41 AM GMT+02:00
>> To: shindig-commits@incubator.apache.org
>> Subject: svn commit: r678068 - in
>> /incubator/shindig/trunk/php/src/social-api: converters/ dataservice/ http/
>> samplecontainer/
>> Reply-To: shindig-dev@incubator.apache.org
>>
>> Author: chabotc
>> Date: Fri Jul 18 15:47:41 2008
>> New Revision: 678068
>>
>> URL: http://svn.apache.org/viewvc?rev=678068&view=rev
>> Log:
>> Initial support for the batch proxy request type.
>>
>> The url is /social/rest/batchProxy and it uses the http multipart
>> format as described in the RESTful API specification.
>>
>> It only supports ONE output format for a set of requests (which
>> is determined by the main url ?format=foo param, and defaults to
>> json). Input however will support mixing json and atom.
>>
>> Atom input is still missing (has a debug dump right now) but support
>> for that will follow quickly.
>>
>> OAuth and Atom input is still missing, but once their done PHP
>> Shindig will have full RESTful spec support, we're getting there!
>>
>>
>> Modified:
>>
>> incubator/shindig/trunk/php/src/social-api/converters/OutputAtomConverter.php
>>
>> incubator/shindig/trunk/php/src/social-api/converters/OutputConverter.php
>>
>> incubator/shindig/trunk/php/src/social-api/converters/OutputJsonConverter.php
>>   incubator/shindig/trunk/php/src/social-api/dataservice/PeopleHandler.php
>>
>> incubator/shindig/trunk/php/src/social-api/dataservice/RestRequestItem.php
>>   incubator/shindig/trunk/php/src/social-api/http/RestServlet.php
>>
>> incubator/shindig/trunk/php/src/social-api/samplecontainer/BasicAppDataService.php
>>
>> incubator/shindig/trunk/php/src/social-api/samplecontainer/BasicPeopleService.php
>>
>> Modified:
>> incubator/shindig/trunk/php/src/social-api/converters/OutputAtomConverter.php
>> URL:
>> http://svn.apache.org/viewvc/incubator/shindig/trunk/php/src/social-api/converters/OutputAtomConverter.php?rev=678068&r1=678067&r2=678068&view=diff
>>
>> ==============================================================================
>> ---
>> incubator/shindig/trunk/php/src/social-api/converters/OutputAtomConverter.php
>> (original)
>> +++
>> incubator/shindig/trunk/php/src/social-api/converters/OutputAtomConverter.php
>> Fri Jul 18 15:47:41 2008
>> @@ -1,4 +1,5 @@
>> <?php
>> +
>> /*
>>  * Licensed to the Apache Software Foundation (ASF) under one
>>  * or more contributor license agreements. See the NOTICE file
>> @@ -19,7 +20,7 @@
>>
>> /**
>>  * Format = atom output converter, for format definition see:
>> - *
>> http://www.opensocial.org/Technical-Resources/opensocial-specification----implementation-version-08/restful-api-specification
>> + *
>> http://sites.google.com/a/opensocial.org/opensocial/Technical-Resources/opensocial-spec-v08/restful-api-specification
>>  */
>> class OutputAtomConverter extends OutputConverter {
>>        private static $nameSpace = 'http://www.w3.org/2005/Atom';
>> @@ -40,7 +41,7 @@
>>                $data = $responseItem->getResponse();
>>                $userId =
>> $requestItem->getUser()->getUserId($requestItem->getToken());
>>                $guid = 'urn:guid:' . $userId;
>> -               $authorName = $_SERVER['HTTP_HOST'].':'.$userId;
>> +               $authorName = $_SERVER['HTTP_HOST'] . ':' . $userId;
>>                $updatedAtom = date(DATE_ATOM);
>>
>>                // Check to see if this is a single entry, or a collection,
>> and construct either an atom
>> @@ -52,20 +53,18 @@
>>
>>                        // The root Feed element
>>                        $entry = $this->addNode($doc, 'feed', '', false,
>> self::$nameSpace);
>> -
>> +
>>                        // Required Atom fields
>>                        $endPos = ($startIndex + $itemsPerPage) >
>> $totalResults ? $totalResults : ($startIndex + $itemsPerPage);
>> -                       $this->addNode($entry, 'title', $requestType.'
>> feed for id '.$authorName.' ('.$startIndex. ' - '. ($endPos - 1).' of
>> '.$totalResults.')');
>> +                       $this->addNode($entry, 'title', $requestType . '
>> feed for id ' . $authorName . ' (' . $startIndex . ' - ' . ($endPos - 1) . '
>> of ' . $totalResults . ')');
>>                        $author = $this->addNode($entry, 'author');
>>                        $this->addNode($author, 'uri', $guid);
>> -                       $this->addNode($author, 'name', $authorName);
>>
>> +                       $this->addNode($author, 'name', $authorName);
>>                        $this->addNode($entry, 'updated', $updatedAtom);
>>                        $this->addNode($entry, 'id', $guid);
>> -                       $this->addNode($entry, 'link', '', array('rel' =>
>> 'self', 'href' => 'http://
>> '.$_SERVER['HTTP_HOST'].$_SERVER['REQUEST_URI']));
>> -
>> +                       $this->addNode($entry, 'link', '', array('rel' =>
>> 'self', 'href' => 'http://' . $_SERVER['HTTP_HOST'] .
>> $_SERVER['REQUEST_URI']));
>>                        // Add osearch & next link to the entry
>>                        $this->addPagingFields($entry, $startIndex,
>> $itemsPerPage, $totalResults);
>> -
>>                        // Add response entries to feed
>>                        $responses =
>> $responseItem->getResponse()->getEntry();
>>                        foreach ($responses as $response) {
>> @@ -79,11 +78,10 @@
>>                                $this->addNode($author, 'uri', $guid);
>>                                $this->addNode($author, 'name',
>> $authorName);
>>                                // Special hoisting rules for activities
>> -
>>                                if ($response instanceof Activity) {
>>                                        $this->addNode($feedEntry,
>> 'category', '', array('term' => 'status'));
>>                                        $this->addNode($feedEntry,
>> 'updated', date(DATE_ATOM, $response->postedTime));
>> -                                       $this->addNode($feedEntry, 'id',
>> 'urn:guid:'.$response->id);
>> +                                       $this->addNode($feedEntry, 'id',
>> 'urn:guid:' . $response->id);
>>                                        //FIXME should add a link field but
>> don't have URL's available yet:
>>                                        // <link rel="self"
>> type="application/atom+xml" href="
>> http://api.example.org/activity/feeds/.../af3778"/>
>>                                        $this->addNode($feedEntry, 'title',
>> strip_tags($response->title));
>> @@ -94,43 +92,49 @@
>>                                        unset($response->title);
>>                                        unset($response->body);
>>                                } else {
>> -                                       $this->addNode($feedEntry, 'id',
>> 'urn:guid:'.$idField);
>> -                                       $this->addNode($feedEntry,
>> 'title', $requestType.' feed entry for id '.$idField);
>> +                                       $this->addNode($feedEntry, 'id',
>> 'urn:guid:' . $idField);
>> +                                       $this->addNode($feedEntry,
>> 'title', $requestType . ' feed entry for id ' . $idField);
>>                                        $this->addNode($feedEntry,
>> 'updated', $updatedAtom);
>>                                }
>>
>>                                // recursively add responseItem data to the
>> xml structure
>>                                $this->addData($content, $requestType,
>> $response, self::$osNameSpace);
>>                        }
>> -
>>                } else {
>> -
>>                        // Single entry = Atom:Entry
>>                        $entry =
>> $doc->appendChild($doc->createElementNS(self::$nameSpace, "entry"));
>> -
>>                        // Atom fields
>> -                       $this->addNode($entry, 'title', $requestType.'
>> entry for '.$authorName);
>> +                       $this->addNode($entry, 'title', $requestType . '
>> entry for ' . $authorName);
>>                        $author = $this->addNode($entry, 'author');
>>                        $this->addNode($author, 'uri', $guid);
>>                        $this->addNode($author, 'name', $authorName);
>>                        $this->addNode($entry, 'id', $guid);
>>                        $this->addNode($entry, 'updated', $updatedAtom);
>>                        $content = $this->addNode($entry, 'content', '',
>> array('type' => 'application/xml'));
>> -
>>                        // addData loops through the responseItem data
>> recursively creating a matching XML structure
>>                        $this->addData($content, $requestType, $data,
>> self::$osNameSpace);
>>                }
>>                $xml = $doc->saveXML();
>> -               if (self::$includeOsearch && $responseItem->getResponse()
>> instanceof RestFulCollection) {
>> +               if ($responseItem->getResponse() instanceof
>> RestFulCollection) {
>>                        //FIXME dirty hack until i find a way to add
>> multiple name spaces using DomXML functions
>> -                       $xml = str_replace('<feed xmlns="
>> http://www.w3.org/2005/Atom">', '<feed xmlns="http://www.w3.org/2005/Atom"
>> xmlos:osearch="http://a9.com/-/spec/opensearch/1.1">' ,$xml);
>> +                       $xml = str_replace('<feed xmlns="
>> http://www.w3.org/2005/Atom">', '<feed xmlns="http://www.w3.org/2005/Atom"
>> xmlns:osearch="http://a9.com/-/spec/opensearch/1.1">', $xml);
>>                }
>>                echo $xml;
>>        }
>>
>>        function outputBatch(Array $responses, SecurityToken $token)
>>        {
>> -               //TODO once we support spec compliance batching, this
>> needs to be added too
>> +               $this->boundryHeaders();
>> +               foreach ($responses as $response) {
>> +                       $request = $response['request'];
>> +                       $response = $response['response'];
>> +                       // output buffering supports multiple levels of
>> it.. it's a nice feature to abuse :)
>> +                       ob_start();
>> +                       $this->outputResponse($response, $request);
>> +                       $part = ob_get_contents();
>> +                       ob_end_clean();
>> +                       $this->outputPart($part, $response->getError());
>> +               }
>>        }
>>
>>        /**
>> @@ -174,11 +178,9 @@
>>         */
>>        private function addPagingFields($entry, $startIndex,
>> $itemsPerPage, $totalResults)
>>        {
>> -               if (self::$includeOsearch) {
>> -                       $this->addNode($entry, 'osearch:totalResults',
>> $totalResults);
>> -                       $this->addNode($entry, 'osearch:startIndex',
>> $startIndex ? $startIndex : '0');
>> -                       $this->addNode($entry, 'osearch:itemsPerPage',
>> $itemsPerPage);
>> -               }
>> +               $this->addNode($entry, 'osearch:totalResults',
>> $totalResults);
>> +               $this->addNode($entry, 'osearch:startIndex', $startIndex ?
>> $startIndex : '0');
>> +               $this->addNode($entry, 'osearch:itemsPerPage',
>> $itemsPerPage);
>>                // Create a 'next' link based on our current url if this is
>> a pageable collection & there is more to display
>>                if (($startIndex + $itemsPerPage) < $totalResults) {
>>                        $nextStartIndex = ($startIndex + $itemsPerPage) -
>> 1;
>> @@ -255,6 +257,9 @@
>>                                        }
>>                                        $this->addData($newElement, $key,
>> $val);
>>                                } else {
>> +                                       if (is_numeric($key)) {
>> +                                               $key = is_object($val) ?
>> get_class($val) : $key = $name;
>> +                                       }
>>
>>                                        $elm =
>> $newElement->appendChild($this->doc->createElement($key));
>>
>>  $elm->appendChild($this->doc->createTextNode($val));
>>                                }
>>
>> Modified:
>> incubator/shindig/trunk/php/src/social-api/converters/OutputConverter.php
>> URL:
>> http://svn.apache.org/viewvc/incubator/shindig/trunk/php/src/social-api/converters/OutputConverter.php?rev=678068&r1=678067&r2=678068&view=diff
>>
>> ==============================================================================
>> ---
>> incubator/shindig/trunk/php/src/social-api/converters/OutputConverter.php
>> (original)
>> +++
>> incubator/shindig/trunk/php/src/social-api/converters/OutputConverter.php
>> Fri Jul 18 15:47:41 2008
>> @@ -22,6 +22,50 @@
>>  *
>>  */
>> abstract class OutputConverter {
>> +       private $boundry;
>> +
>>        abstract function outputResponse(ResponseItem $responseItem,
>> RestRequestItem $requestItem);
>>        abstract function outputBatch(Array $responses, SecurityToken
>> $token);
>> +
>> +       /**
>> +        * Output the multipart/mixed headers and returns the boundry
>> token used
>> +        *
>> +        */
>> +       public function boundryHeaders()
>> +       {
>> +               $this->boundry = '--batch-'.md5(rand(0,32000));
>> +               header("HTTP/1.1 200 OK", true);
>> +               header("Content-Type: multipart/mixed;
>> boundary=$this->boundry", true);
>> +       }
>> +
>> +       public function outputPart($part, $code)
>> +       {
>> +               $boundryHeader = "{$this->boundry}\n".
>> +                               "Content-Type:
>> application/http;version=1.1\n".
>> +                               "Content-Transfer-Encoding: binary\n\n";
>> +               echo $boundryHeader;
>> +               switch ($code) {
>> +                       case BAD_REQUEST:
>> +                               $code = '400 Bad Request';
>> +                               break;
>> +                       case UNAUTHORIZED:
>> +                               $code = '401 Unauthorized';
>> +                               break;
>> +                       case FORBIDDEN:
>> +                               $code = '403 Forbidden';
>> +                               break;
>> +                       case FORBIDDEN:
>> +                               $code = '404 Not Found';
>> +                               break;
>> +                       case NOT_IMPLEMENTED:
>> +                               $code = '501 Not Implemented';
>> +                               break;
>> +                       case INTERNAL_ERROR:
>> +                       default:
>> +                               $code = '200 OK';
>> +                               break;
>> +               }
>> +               echo "$code\n\n";
>> +               echo $part."\n";
>> +       }
>> }
>>
>> Modified:
>> incubator/shindig/trunk/php/src/social-api/converters/OutputJsonConverter.php
>> URL:
>> http://svn.apache.org/viewvc/incubator/shindig/trunk/php/src/social-api/converters/OutputJsonConverter.php?rev=678068&r1=678067&r2=678068&view=diff
>>
>> ==============================================================================
>> ---
>> incubator/shindig/trunk/php/src/social-api/converters/OutputJsonConverter.php
>> (original)
>> +++
>> incubator/shindig/trunk/php/src/social-api/converters/OutputJsonConverter.php
>> Fri Jul 18 15:47:41 2008
>> @@ -30,6 +30,17 @@
>>
>>        function outputBatch(Array $responses, SecurityToken $token)
>>        {
>> +               $this->boundryHeaders();
>> +               foreach ($responses as $response) {
>> +                       $request = $response['request'];
>> +                       $response = $response['response'];
>> +                       $part = json_encode($response);
>> +                       $this->outputPart($part, $response->getError());
>> +               }
>> +       }
>> +
>> +       function outputJsonBatch(Array $responses, SecurityToken $token)
>> +       {
>>                echo json_encode(array("responses" => $responses, "error"
>> => false));
>>        }
>> }
>>
>> Modified:
>> incubator/shindig/trunk/php/src/social-api/dataservice/PeopleHandler.php
>> URL:
>> http://svn.apache.org/viewvc/incubator/shindig/trunk/php/src/social-api/dataservice/PeopleHandler.php?rev=678068&r1=678067&r2=678068&view=diff
>>
>> ==============================================================================
>> ---
>> incubator/shindig/trunk/php/src/social-api/dataservice/PeopleHandler.php
>> (original)
>> +++
>> incubator/shindig/trunk/php/src/social-api/dataservice/PeopleHandler.php Fri
>> Jul 18 15:47:41 2008
>> @@ -20,7 +20,9 @@
>> class PeopleHandler extends DataRequestHandler {
>>        private $service;
>>        private static $PEOPLE_PATH =
>> "/people/{userId}/{groupId}/{personId}";
>> -       protected static $DEFAULT_PERSON_FIELDS = array("id", "name",
>> "thumbnailUrl");
>> +       //FIXME change this back to array("id", "name", "thumbnailUrl")
>> once the dust settles
>> +       // on the spec discussion related to this
>> +       protected static $DEFAULT_PERSON_FIELDS = array('all' => 'all');
>>
>>        public function __construct()
>>        {
>>
>> Modified:
>> incubator/shindig/trunk/php/src/social-api/dataservice/RestRequestItem.php
>> URL:
>> http://svn.apache.org/viewvc/incubator/shindig/trunk/php/src/social-api/dataservice/RestRequestItem.php?rev=678068&r1=678067&r2=678068&view=diff
>>
>> ==============================================================================
>> ---
>> incubator/shindig/trunk/php/src/social-api/dataservice/RestRequestItem.php
>> (original)
>> +++
>> incubator/shindig/trunk/php/src/social-api/dataservice/RestRequestItem.php
>> Fri Jul 18 15:47:41 2008
>> @@ -45,12 +45,12 @@
>>
>>        public function createRequestItemWithRequest($request, $token)
>>        {
>> -               $this->url = $request->url;
>> -               $this->parameters =
>> $this->createParameterMap($request->url);
>> +               $this->url = $request['url'];
>> +               $this->parameters =
>> $this->createParameterMap($request['url']);
>>                $this->token = $token;
>> -               $this->method = $request->method;
>> -               if (isset($request->postData)) {
>> -                       $this->postData = $request->postData;
>> +               $this->method = $request['method'];
>> +               if (isset($request['postData'])) {
>> +                       $this->postData = $request['postData'];
>>                }
>>        }
>>
>>
>> Modified: incubator/shindig/trunk/php/src/social-api/http/RestServlet.php
>> URL:
>> http://svn.apache.org/viewvc/incubator/shindig/trunk/php/src/social-api/http/RestServlet.php?rev=678068&r1=678067&r2=678068&view=diff
>>
>> ==============================================================================
>> --- incubator/shindig/trunk/php/src/social-api/http/RestServlet.php
>> (original)
>> +++ incubator/shindig/trunk/php/src/social-api/http/RestServlet.php Fri
>> Jul 18 15:47:41 2008
>> @@ -51,8 +51,6 @@
>> require 'src/social-api/converters/OutputAtomConverter.php';
>> require 'src/social-api/converters/OutputJsonConverter.php';
>>
>> -//FIXME Delete should respond with a 204 No Content to indicate success
>> -
>> class RestException extends Exception {}
>>
>> /*
>> @@ -64,42 +62,80 @@
>> define('FORBIDDEN', "forbidden");
>> define('BAD_REQUEST', "badRequest");
>> define('INTERNAL_ERROR', "internalError");
>> +//FIXME Delete should respond with a 204 No Content to indicate success
>>
>> class RestServlet extends HttpServlet {
>>
>> +       // The json Batch Route is used by the gadgets
>>        private static $JSON_BATCH_ROUTE = "jsonBatch";
>> +       // The Batch Proxy route is used the one defined in the RESTful
>> API specification
>> +       private static $BATCH_PROXY_ROUTE = "batchProxy";
>> +
>> +       public function doGet()
>> +       {
>> +               $this->doPost('GET');
>> +       }
>>
>> +       public function doPut()
>> +       {
>> +               $this->doPost('PUT');
>> +       }
>> +
>> +       public function doDelete()
>> +       {
>> +               $this->doPost('DELETE');
>> +       }
>> +
>>        public function doPost($method = 'POST')
>>        {
>> -               $this->setNoCache(true);
>> -               // if oauth, create a token from it's values instead of
>> one based on $_get['st']/$_post['st']
>> -               // NOTE : if no token is provided an anonymous one is
>> created (owner = viewer = appId = modId = 0)
>> -               // keep this in mind when creating your data services..
>> -               $token = $this->getSecurityToken();
>> -               $outputFormat = $this->getOutputFormat();
>> -               switch ($outputFormat) {
>> -                       case 'json':
>> -                               $this->setContentType('application/json');
>> -                               $outputConverter = new
>> OutputJsonConverter();
>> -                               break;
>> -                       case 'atom':
>> -
>> $this->setContentType('application/atom+xml');
>> -                               $outputConverter = new
>> OutputAtomConverter();
>> -                               break;
>> -                       default:
>> -                               $this->outputError(new
>> ResponseItem(NOT_IMPLEMENTED, "Invalid output format"));
>> -                               break;
>> -               }
>> -               if ($this->isBatchUrl()) {
>> -                       $responses = $this->handleBatchRequest($token);
>> -                       $outputConverter->outputBatch($responses, $token);
>> -               } else {
>> -                       $response = $this->handleSingleRequest($token,
>> $method);
>> -
>> $outputConverter->outputResponse($response['response'],
>> $response['request']);
>> +               try {
>> +                       $this->setNoCache(true);
>> +                       // if oauth, create a token from it's values
>> instead of one based on $_get['st']/$_post['st']
>> +                       // NOTE : if no token is provided an anonymous one
>> is created (owner = viewer = appId = modId = 0)
>> +                       // keep this in mind when creating your data
>> services..
>> +                       $token = $this->getSecurityToken();
>> +                       $outputFormat = $this->getOutputFormat();
>> +                       switch ($outputFormat) {
>> +                               case 'json':
>> +
>> $this->setContentType('application/json');
>> +                                       $outputConverter = new
>> OutputJsonConverter();
>> +                                       break;
>> +                               case 'atom':
>> +
>> $this->setContentType('application/atom+xml');
>> +                                       $outputConverter = new
>> OutputAtomConverter();
>> +                                       break;
>> +                               default:
>> +                                       $this->outputError(new
>> ResponseItem(NOT_IMPLEMENTED, "Invalid output format"));
>> +                                       break;
>> +                       }
>> +                       if ($this->isJsonBatchUrl()) {
>> +                               // custom json batch format used by the
>> gadgets
>> +                               $responses =
>> $this->handleJsonBatchRequest($token);
>> +
>> $outputConverter->outputJsonBatch($responses, $token);
>> +                       } elseif ($this->isBatchProxyUrl()) {
>> +                               // spec compliant batch proxy
>> +                               $this->noHeaders = true;
>> +                               $responses =
>> $this->handleBatchProxyRequest($token);
>> +                               $outputConverter->outputBatch($responses,
>> $token);
>> +                       } else {
>> +                               // single rest request
>> +                               $response = $this->handleRequest($token,
>> $method);
>> +
>> $outputConverter->outputResponse($response['response'],
>> $response['request']);
>> +                       }
>> +               } catch (Exception $e) {
>> +                       header("HTTP/1.0 500 Internal Server Error");
>> +                       echo "<html><body><h1>500 Internal Server
>> Error</h1>";
>> +                       if (Config::get('debug')) {
>> +                               echo "Message: ".$e->getMessage()."<br
>> />\n";
>> +                               echo "<pre>\n";
>> +                               print_r(debug_backtrace());
>> +                               echo "\n</pre>";
>> +                       }
>> +                       echo "</body></html>";
>>                }
>>        }
>>
>> -       private function handleSingleRequest($token, $method)
>> +       private function handleRequest($token, $method)
>>        {
>>                $params = $this->getListParams();
>>                $requestItem = new RestRequestItem();
>> @@ -109,14 +145,115 @@
>>                $responseItem = $this->getResponseItem($requestItem);
>>                return array('request' => $requestItem, 'response' =>
>> $responseItem);
>>        }
>> -
>> -       private function getRouteFromParameter($pathInfo)
>> +
>> +       private function handleJsonBatchRequest($token)
>>        {
>> -               $pathInfo = substr($pathInfo, 1);
>> -               $indexOfNextPathSeparator = strpos($pathInfo, "/");
>> -               return $indexOfNextPathSeparator != - 1 ?
>> substr($pathInfo, 0, $indexOfNextPathSeparator) : $pathInfo;
>> +               // we support both a raw http post (without
>> application/x-www-form-urlencoded headers) like java does
>> +               // and a more php / curl safe version of a form post with
>> 'request' as the post field that holds the request json data
>> +               if (isset($GLOBALS['HTTP_RAW_POST_DATA']) ||
>> isset($_POST['request'])) {
>> +                       $requests = $this->getRequestParams();
>> +                       $responses = array();
>> +                       foreach ($requests as $key => $value) {
>> +                               $requestItem = new RestRequestItem();
>> +
>> $requestItem->createRequestItemWithRequest($value, $token);
>> +                               $responses[$key] =
>> $this->getResponseItem($requestItem);
>> +                       }
>> +                       return $responses;
>> +               } else {
>> +                       throw new Exception("No post data set");
>> +               }
>> +       }
>> +
>> +       private function handleBatchProxyRequest($token)
>> +       {
>> +               // Is this is a multipath/mixed post? Check content type:
>> +               if (isset($GLOBALS['HTTP_RAW_POST_DATA']) &&
>> strpos($_SERVER['CONTENT_TYPE'], 'multipart/mixed') !== false &&
>> strpos($_SERVER['CONTENT_TYPE'],'boundary=') !== false) {
>> +                       // Ok looks swell, see what our boundry is..
>> +                       $boundry = substr($_SERVER['CONTENT_TYPE'],
>> strpos($_SERVER['CONTENT_TYPE'],'boundary=') + strlen('boundary='));
>> +                       // Split up requests per boundry
>> +                       $requests = explode($boundry,
>> $GLOBALS['HTTP_RAW_POST_DATA']);
>> +                       $responses = array();
>> +                       foreach ($requests as $request) {
>> +                               $request = trim($request);
>> +                               if (!empty($request)) {
>> +                                       // extractBatchRequest() does the
>> magic parsing of the raw post data to a meaninful request array
>> +                                       $request =
>> $this->extractBatchRequest($request);
>> +                                       $requestItem = new
>> RestRequestItem();
>> +
>> $requestItem->createRequestItemWithRequest($request, $token);
>> +                                       $responses[] = array('request' =>
>> $requestItem, 'response' => $this->getResponseItem($requestItem));
>> +                               }
>> +                       }
>> +               } else {
>> +                       $this->outputError(new ResponseItem(BAD_REQUEST,
>> "Invalid multipart/mixed request"));
>> +               }
>> +               return $responses;
>>        }
>>
>> +       private function extractBatchRequest($request)
>> +       {
>> +               /* Multipart request is formatted like:
>> +                * -batch-a73hdj3dy3mm347ddjjdf
>> +                * Content-Type: application/http;version=1.1
>> +                * Content-Transfer-Encoding: binary
>> +                *
>> +                * GET
>> /people/@me/@friends?startPage=5&count=10&format=json
>> +                * Host: api.example.org
>> +                * If-None-Match: "837dyfkdi39df"
>> +                *
>> +                * but we only want to have the last bit (the actual
>> request), this filters that down first
>> +                */
>> +               $emptyFound = false;
>> +               $requestLines = explode("\n", $request);
>> +               $request = '';
>> +               foreach ($requestLines as $line) {
>> +                       if ($emptyFound) {
>> +                               $request .= $line."\n";
>> +                       } elseif (empty($line)) {
>> +                               $emptyFound = true;
>> +                       }
>> +               }
>> +               // Now that we have the basic request in $request, split
>> that up again & parse it into a meaningful representation
>> +               $firstFound = $emptyFound = false;
>> +               $requestLines = explode("\n", $request);
>> +               $request = array();
>> +               $request['headers'] = array();
>> +               $request['postData'] = '';
>> +               foreach ($requestLines as $line) {
>> +                       if (!$firstFound) {
>> +                               $firstFound = true;
>> +                               $parts = explode(' ', trim($line));
>> +                               if (count($parts) != 2) {
>> +                                       throw new Exception("Mallshaped
>> request uri in multipart block");
>> +                               }
>> +                               $request['method'] =
>> strtoupper(trim($parts[0]));
>> +                               // cut it down to an actual meaningful url
>> without the prefix/social/rest part right away
>> +                               $request['url'] = substr(trim($parts[1]),
>> strlen(Config::get('web_prefix') . '/social/rest'));
>> +                       } elseif (!$emptyFound && !empty($line)) {
>> +                               // convert the key to the PHP
>> 'CONTENT_TYPE' style naming convention.. it's ugly but consitent
>> +                               $key = str_replace('-', '_',
>> strtoupper(trim(substr($line, 0, strpos($line, ':')))));
>> +                               $val = trim(substr($line, strpos($line,
>> ':') + 1));
>> +                               $request['headers'][$key] = $val;
>> +                       } elseif (!$emptyFound && empty($line)) {
>> +                               $emptyFound = true;
>> +                       } else {
>> +                               if (get_magic_quotes_gpc()) {
>> +                                       $line = stripslashes($line);
>> +                               }
>> +                               $request['postData'] .= $line."\n";
>> +                       }
>> +               }
>> +               if (empty($request['postData'])) {
>> +                       // don't trip the requestItem into thinking there
>> is postData when there's not
>> +                       unset($request['postData']);
>> +               } else {
>> +                       // if there was a post data blob present, decode
>> it into an array, the format is based on the
>> +                       // content type header, which is either
>> application/json or
>> +                       $format =
>> isset($request['headers']['CONTENT_TYPE']) &&
>> strtolower($request['headers']['CONTENT_TYPE']) == 'application/atom+xml' ?
>> 'atom' : 'json';
>> +                       $request['postData'] =
>> $this->decodeRequests($request['postData'], $format);
>> +               }
>> +               return $request;
>> +       }
>> +
>>        private function getResponseItem(RestRequestItem $requestItem)
>>        {
>>                $path =
>> $this->getRouteFromParameter($requestItem->getUrl());
>> @@ -140,12 +277,26 @@
>>                        $class = new $class(null);
>>                        $response = $class->handleMethod($requestItem);
>>                }
>> -               if ($response->getError() != null && !$this->isBatchUrl())
>> {
>> +               if ($response->getError() != null &&
>> !$this->isJsonBatchUrl() && !$this->isBatchProxyUrl()) {
>>                        // Can't use http error codes in batch mode,
>> instead we return the error code in the response item
>>                        $this->outputError($response);
>>                }
>>                return $response;
>>        }
>> +
>> +       private function decodeRequests($requestParam, $format = 'json')
>> +       {
>> +               // temp hack until i know what the intended way to detect
>> format is
>> +               if ($format == 'json') {
>> +                       return json_decode($requestParam, true);
>> +               } elseif ($format == 'atom') {
>> +                       $xml = simplexml_load_string($requestParam);
>> +                       print_r($xml);
>> +                       return $xml;
>> +               } else {
>> +                       throw Exception("Invalid or unsupported input
>> format");
>> +               }
>> +       }
>>
>>        private function getRequestParams()
>>        {
>> @@ -154,44 +305,14 @@
>>                if (get_magic_quotes_gpc()) {
>>                        $requestParam = stripslashes($requestParam);
>>                }
>> -               $requests = json_decode($requestParam);
>> -               if ($requests == (isset($GLOBALS['HTTP_RAW_POST_DATA']) ?
>> $GLOBALS['HTTP_RAW_POST_DATA'] : $post)) {
>> -                       return new ResponseItem(BAD_REQUEST, "Malformed
>> json string");
>> -               }
>> -               return $requests;
>> -       }
>> -
>> -       private function handleBatchRequest($token)
>> -       {
>> -               // we support both a raw http post (without
>> application/x-www-form-urlencoded headers) like java does
>> -               // and a more php / curl safe version of a form post with
>> 'request' as the post field that holds the request json data
>> -               if (isset($GLOBALS['HTTP_RAW_POST_DATA']) ||
>> isset($_POST['request'])) {
>> -                       $requests = $this->getRequestParams();
>> -                       $responses = array();
>> -                       foreach ($requests as $key => $value) {
>> -                               $requestItem = new RestRequestItem();
>> -
>> $requestItem->createRequestItemWithRequest($value, $token);
>> -                               $responses[$key] =
>> $this->getResponseItem($requestItem);
>> -                       }
>> -                       return $responses;
>> -               } else {
>> -                       throw new Exception("No post data set");
>> -               }
>> -       }
>> -
>> -       public function doGet()
>> -       {
>> -               $this->doPost('GET');
>> -       }
>> -
>> -       public function doPut()
>> -       {
>> -               $this->doPost('PUT');
>> +               return $this->decodeRequests($requestParam);
>>        }
>>
>> -       public function doDelete()
>> +       private function getRouteFromParameter($pathInfo)
>>        {
>> -               $this->doPost('DELETE');
>> +               $pathInfo = substr($pathInfo, 1);
>> +               $indexOfNextPathSeparator = strpos($pathInfo, "/");
>> +               return $indexOfNextPathSeparator != - 1 ?
>> substr($pathInfo, 0, $indexOfNextPathSeparator) : $pathInfo;
>>        }
>>
>>        private function outputError(ResponseItem $response)
>> @@ -217,7 +338,6 @@
>>                        default:
>>                                $code = '500 Internal Server Error';
>>                                break;
>> -
>>                }
>>                header("HTTP/1.0 $code", true);
>>                echo "$code - $errorMessage";
>> @@ -265,8 +385,13 @@
>>                return substr($_SERVER["REQUEST_URI"],
>> strlen(Config::get('web_prefix') . '/social/rest'));
>>        }
>>
>> -       public function isBatchUrl()
>> +       public function isJsonBatchUrl()
>>        {
>>                return strrpos($_SERVER["REQUEST_URI"],
>> RestServlet::$JSON_BATCH_ROUTE) > 0;
>>        }
>> +
>> +       public function isBatchProxyUrl()
>> +       {
>> +               return strrpos($_SERVER["REQUEST_URI"],
>> RestServlet::$BATCH_PROXY_ROUTE) > 0;
>> +       }
>> }
>>
>> Modified:
>> incubator/shindig/trunk/php/src/social-api/samplecontainer/BasicAppDataService.php
>> URL:
>> http://svn.apache.org/viewvc/incubator/shindig/trunk/php/src/social-api/samplecontainer/BasicAppDataService.php?rev=678068&r1=678067&r2=678068&view=diff
>>
>> ==============================================================================
>> ---
>> incubator/shindig/trunk/php/src/social-api/samplecontainer/BasicAppDataService.php
>> (original)
>> +++
>> incubator/shindig/trunk/php/src/social-api/samplecontainer/BasicAppDataService.php
>> Fri Jul 18 15:47:41 2008
>> @@ -85,7 +85,7 @@
>>                switch($groupId->getType()) {
>>                        case 'self':
>>                                foreach ($fields as $key) {
>> -                                       $value = isset($values->$key) ?
>> $values->$key : (@isset($values[$key]) ? @$values[$key] : null);
>> +                                       $value = isset($values[$key]) ?
>> @$values[$key] : null;
>>
>>  XmlStateFileFetcher::get()->setAppData($userId->getUserId($token), $key,
>> $value);
>>                                }
>>                                break;
>>
>> Modified:
>> incubator/shindig/trunk/php/src/social-api/samplecontainer/BasicPeopleService.php
>> URL:
>> http://svn.apache.org/viewvc/incubator/shindig/trunk/php/src/social-api/samplecontainer/BasicPeopleService.php?rev=678068&r1=678067&r2=678068&view=diff
>>
>> ==============================================================================
>> ---
>> incubator/shindig/trunk/php/src/social-api/samplecontainer/BasicPeopleService.php
>> (original)
>> +++
>> incubator/shindig/trunk/php/src/social-api/samplecontainer/BasicPeopleService.php
>> Fri Jul 18 15:47:41 2008
>> @@ -76,7 +76,7 @@
>>                                if ($id == $token->getOwnerId()) {
>>                                        $person->setIsOwner(true);
>>                                }
>> -                               if (is_array($profileDetails) &&
>> count($profileDetails)) {
>> +                               if (is_array($profileDetails) &&
>> count($profileDetails) && !in_array('all', $profileDetails)) {
>>                                        $newPerson = array();
>>                                        $newPerson['isOwner'] =
>> $person->isOwner;
>>                                        $newPerson['isViewer'] =
>> $person->isViewer;
>>
>>
>


-- 
.-. --- .--. ..-
R o p u

Re: initial proxyBatch support

Posted by Ropu <ro...@gmail.com>.
hmmm, if you send a file though a simple <form> isnt it multiparted by the
browser?

ropu

On Fri, Jul 18, 2008 at 8:04 PM, Chris Chabot <ch...@xs4all.nl> wrote:

> This has been a bit of a headache to develop (see spec list) but it's
> starting to take some shape. It's also quite difficult to test for me, since
> nothing in PHP really supports multipart http posts... So i had to hand
> craft both a test client, the input parsing in shindig, and the multi part
> output construct too.
>
> In other words, it works with the input that my test client creates, but i
> have no way to absolutely verify it is all completely valid http protocol
> compliant stuff ... ah the joys of developing in the dark with blindfolds on
> :)
>
> The batch proxy is advertised through XRDS simple:
> http://www.chabotc.com/xrds-test.php?url=http://www.partuza.nl
>
> When i shoot a hand crafted multipart payload into it it works & you can
> specify how you want your (presumably http multipart compliant) output on
> the main url (to batchProxy) with ?format=atom or ?format=json (json is
> assumed by default). You could even poke at it at
> http://modules.partuza.nl/social/rest/batchProxy .. just make sure to grab
> a valid token from a gadget iframe first to use in your requests and your
> golden :) The only missing bits are OAuth support, and Atom format input
> support, they will hopefully follow shortly.
>
> However my main question ... does anyone have any useful tools for actually
> testing and verifying multipart http posts & the multipart response ? :)
>
>        -- Chris
>
> Begin forwarded message:
>
>  From: chabotc@apache.org
>> Date: July 19, 2008 12:47:41 AM GMT+02:00
>> To: shindig-commits@incubator.apache.org
>> Subject: svn commit: r678068 - in
>> /incubator/shindig/trunk/php/src/social-api: converters/ dataservice/ http/
>> samplecontainer/
>> Reply-To: shindig-dev@incubator.apache.org
>>
>> Author: chabotc
>> Date: Fri Jul 18 15:47:41 2008
>> New Revision: 678068
>>
>> URL: http://svn.apache.org/viewvc?rev=678068&view=rev
>> Log:
>> Initial support for the batch proxy request type.
>>
>> The url is /social/rest/batchProxy and it uses the http multipart
>> format as described in the RESTful API specification.
>>
>> It only supports ONE output format for a set of requests (which
>> is determined by the main url ?format=foo param, and defaults to
>> json). Input however will support mixing json and atom.
>>
>> Atom input is still missing (has a debug dump right now) but support
>> for that will follow quickly.
>>
>> OAuth and Atom input is still missing, but once their done PHP
>> Shindig will have full RESTful spec support, we're getting there!
>>
>>
>> Modified:
>>
>> incubator/shindig/trunk/php/src/social-api/converters/OutputAtomConverter.php
>>
>> incubator/shindig/trunk/php/src/social-api/converters/OutputConverter.php
>>
>> incubator/shindig/trunk/php/src/social-api/converters/OutputJsonConverter.php
>>   incubator/shindig/trunk/php/src/social-api/dataservice/PeopleHandler.php
>>
>> incubator/shindig/trunk/php/src/social-api/dataservice/RestRequestItem.php
>>   incubator/shindig/trunk/php/src/social-api/http/RestServlet.php
>>
>> incubator/shindig/trunk/php/src/social-api/samplecontainer/BasicAppDataService.php
>>
>> incubator/shindig/trunk/php/src/social-api/samplecontainer/BasicPeopleService.php
>>
>> Modified:
>> incubator/shindig/trunk/php/src/social-api/converters/OutputAtomConverter.php
>> URL:
>> http://svn.apache.org/viewvc/incubator/shindig/trunk/php/src/social-api/converters/OutputAtomConverter.php?rev=678068&r1=678067&r2=678068&view=diff
>>
>> ==============================================================================
>> ---
>> incubator/shindig/trunk/php/src/social-api/converters/OutputAtomConverter.php
>> (original)
>> +++
>> incubator/shindig/trunk/php/src/social-api/converters/OutputAtomConverter.php
>> Fri Jul 18 15:47:41 2008
>> @@ -1,4 +1,5 @@
>> <?php
>> +
>> /*
>>  * Licensed to the Apache Software Foundation (ASF) under one
>>  * or more contributor license agreements. See the NOTICE file
>> @@ -19,7 +20,7 @@
>>
>> /**
>>  * Format = atom output converter, for format definition see:
>> - *
>> http://www.opensocial.org/Technical-Resources/opensocial-specification----implementation-version-08/restful-api-specification
>> + *
>> http://sites.google.com/a/opensocial.org/opensocial/Technical-Resources/opensocial-spec-v08/restful-api-specification
>>  */
>> class OutputAtomConverter extends OutputConverter {
>>        private static $nameSpace = 'http://www.w3.org/2005/Atom';
>> @@ -40,7 +41,7 @@
>>                $data = $responseItem->getResponse();
>>                $userId =
>> $requestItem->getUser()->getUserId($requestItem->getToken());
>>                $guid = 'urn:guid:' . $userId;
>> -               $authorName = $_SERVER['HTTP_HOST'].':'.$userId;
>> +               $authorName = $_SERVER['HTTP_HOST'] . ':' . $userId;
>>                $updatedAtom = date(DATE_ATOM);
>>
>>                // Check to see if this is a single entry, or a collection,
>> and construct either an atom
>> @@ -52,20 +53,18 @@
>>
>>                        // The root Feed element
>>                        $entry = $this->addNode($doc, 'feed', '', false,
>> self::$nameSpace);
>> -
>> +
>>                        // Required Atom fields
>>                        $endPos = ($startIndex + $itemsPerPage) >
>> $totalResults ? $totalResults : ($startIndex + $itemsPerPage);
>> -                       $this->addNode($entry, 'title', $requestType.'
>> feed for id '.$authorName.' ('.$startIndex. ' - '. ($endPos - 1).' of
>> '.$totalResults.')');
>> +                       $this->addNode($entry, 'title', $requestType . '
>> feed for id ' . $authorName . ' (' . $startIndex . ' - ' . ($endPos - 1) . '
>> of ' . $totalResults . ')');
>>                        $author = $this->addNode($entry, 'author');
>>                        $this->addNode($author, 'uri', $guid);
>> -                       $this->addNode($author, 'name', $authorName);
>>
>> +                       $this->addNode($author, 'name', $authorName);
>>                        $this->addNode($entry, 'updated', $updatedAtom);
>>                        $this->addNode($entry, 'id', $guid);
>> -                       $this->addNode($entry, 'link', '', array('rel' =>
>> 'self', 'href' => 'http://
>> '.$_SERVER['HTTP_HOST'].$_SERVER['REQUEST_URI']));
>> -
>> +                       $this->addNode($entry, 'link', '', array('rel' =>
>> 'self', 'href' => 'http://' . $_SERVER['HTTP_HOST'] .
>> $_SERVER['REQUEST_URI']));
>>                        // Add osearch & next link to the entry
>>                        $this->addPagingFields($entry, $startIndex,
>> $itemsPerPage, $totalResults);
>> -
>>                        // Add response entries to feed
>>                        $responses =
>> $responseItem->getResponse()->getEntry();
>>                        foreach ($responses as $response) {
>> @@ -79,11 +78,10 @@
>>                                $this->addNode($author, 'uri', $guid);
>>                                $this->addNode($author, 'name',
>> $authorName);
>>                                // Special hoisting rules for activities
>> -
>>                                if ($response instanceof Activity) {
>>                                        $this->addNode($feedEntry,
>> 'category', '', array('term' => 'status'));
>>                                        $this->addNode($feedEntry,
>> 'updated', date(DATE_ATOM, $response->postedTime));
>> -                                       $this->addNode($feedEntry, 'id',
>> 'urn:guid:'.$response->id);
>> +                                       $this->addNode($feedEntry, 'id',
>> 'urn:guid:' . $response->id);
>>                                        //FIXME should add a link field but
>> don't have URL's available yet:
>>                                        // <link rel="self"
>> type="application/atom+xml" href="
>> http://api.example.org/activity/feeds/.../af3778"/>
>>                                        $this->addNode($feedEntry, 'title',
>> strip_tags($response->title));
>> @@ -94,43 +92,49 @@
>>                                        unset($response->title);
>>                                        unset($response->body);
>>                                } else {
>> -                                       $this->addNode($feedEntry, 'id',
>> 'urn:guid:'.$idField);
>> -                                       $this->addNode($feedEntry,
>> 'title', $requestType.' feed entry for id '.$idField);
>> +                                       $this->addNode($feedEntry, 'id',
>> 'urn:guid:' . $idField);
>> +                                       $this->addNode($feedEntry,
>> 'title', $requestType . ' feed entry for id ' . $idField);
>>                                        $this->addNode($feedEntry,
>> 'updated', $updatedAtom);
>>                                }
>>
>>                                // recursively add responseItem data to the
>> xml structure
>>                                $this->addData($content, $requestType,
>> $response, self::$osNameSpace);
>>                        }
>> -
>>                } else {
>> -
>>                        // Single entry = Atom:Entry
>>                        $entry =
>> $doc->appendChild($doc->createElementNS(self::$nameSpace, "entry"));
>> -
>>                        // Atom fields
>> -                       $this->addNode($entry, 'title', $requestType.'
>> entry for '.$authorName);
>> +                       $this->addNode($entry, 'title', $requestType . '
>> entry for ' . $authorName);
>>                        $author = $this->addNode($entry, 'author');
>>                        $this->addNode($author, 'uri', $guid);
>>                        $this->addNode($author, 'name', $authorName);
>>                        $this->addNode($entry, 'id', $guid);
>>                        $this->addNode($entry, 'updated', $updatedAtom);
>>                        $content = $this->addNode($entry, 'content', '',
>> array('type' => 'application/xml'));
>> -
>>                        // addData loops through the responseItem data
>> recursively creating a matching XML structure
>>                        $this->addData($content, $requestType, $data,
>> self::$osNameSpace);
>>                }
>>                $xml = $doc->saveXML();
>> -               if (self::$includeOsearch && $responseItem->getResponse()
>> instanceof RestFulCollection) {
>> +               if ($responseItem->getResponse() instanceof
>> RestFulCollection) {
>>                        //FIXME dirty hack until i find a way to add
>> multiple name spaces using DomXML functions
>> -                       $xml = str_replace('<feed xmlns="
>> http://www.w3.org/2005/Atom">', '<feed xmlns="http://www.w3.org/2005/Atom"
>> xmlos:osearch="http://a9.com/-/spec/opensearch/1.1">' ,$xml);
>> +                       $xml = str_replace('<feed xmlns="
>> http://www.w3.org/2005/Atom">', '<feed xmlns="http://www.w3.org/2005/Atom"
>> xmlns:osearch="http://a9.com/-/spec/opensearch/1.1">', $xml);
>>                }
>>                echo $xml;
>>        }
>>
>>        function outputBatch(Array $responses, SecurityToken $token)
>>        {
>> -               //TODO once we support spec compliance batching, this
>> needs to be added too
>> +               $this->boundryHeaders();
>> +               foreach ($responses as $response) {
>> +                       $request = $response['request'];
>> +                       $response = $response['response'];
>> +                       // output buffering supports multiple levels of
>> it.. it's a nice feature to abuse :)
>> +                       ob_start();
>> +                       $this->outputResponse($response, $request);
>> +                       $part = ob_get_contents();
>> +                       ob_end_clean();
>> +                       $this->outputPart($part, $response->getError());
>> +               }
>>        }
>>
>>        /**
>> @@ -174,11 +178,9 @@
>>         */
>>        private function addPagingFields($entry, $startIndex,
>> $itemsPerPage, $totalResults)
>>        {
>> -               if (self::$includeOsearch) {
>> -                       $this->addNode($entry, 'osearch:totalResults',
>> $totalResults);
>> -                       $this->addNode($entry, 'osearch:startIndex',
>> $startIndex ? $startIndex : '0');
>> -                       $this->addNode($entry, 'osearch:itemsPerPage',
>> $itemsPerPage);
>> -               }
>> +               $this->addNode($entry, 'osearch:totalResults',
>> $totalResults);
>> +               $this->addNode($entry, 'osearch:startIndex', $startIndex ?
>> $startIndex : '0');
>> +               $this->addNode($entry, 'osearch:itemsPerPage',
>> $itemsPerPage);
>>                // Create a 'next' link based on our current url if this is
>> a pageable collection & there is more to display
>>                if (($startIndex + $itemsPerPage) < $totalResults) {
>>                        $nextStartIndex = ($startIndex + $itemsPerPage) -
>> 1;
>> @@ -255,6 +257,9 @@
>>                                        }
>>                                        $this->addData($newElement, $key,
>> $val);
>>                                } else {
>> +                                       if (is_numeric($key)) {
>> +                                               $key = is_object($val) ?
>> get_class($val) : $key = $name;
>> +                                       }
>>
>>                                        $elm =
>> $newElement->appendChild($this->doc->createElement($key));
>>
>>  $elm->appendChild($this->doc->createTextNode($val));
>>                                }
>>
>> Modified:
>> incubator/shindig/trunk/php/src/social-api/converters/OutputConverter.php
>> URL:
>> http://svn.apache.org/viewvc/incubator/shindig/trunk/php/src/social-api/converters/OutputConverter.php?rev=678068&r1=678067&r2=678068&view=diff
>>
>> ==============================================================================
>> ---
>> incubator/shindig/trunk/php/src/social-api/converters/OutputConverter.php
>> (original)
>> +++
>> incubator/shindig/trunk/php/src/social-api/converters/OutputConverter.php
>> Fri Jul 18 15:47:41 2008
>> @@ -22,6 +22,50 @@
>>  *
>>  */
>> abstract class OutputConverter {
>> +       private $boundry;
>> +
>>        abstract function outputResponse(ResponseItem $responseItem,
>> RestRequestItem $requestItem);
>>        abstract function outputBatch(Array $responses, SecurityToken
>> $token);
>> +
>> +       /**
>> +        * Output the multipart/mixed headers and returns the boundry
>> token used
>> +        *
>> +        */
>> +       public function boundryHeaders()
>> +       {
>> +               $this->boundry = '--batch-'.md5(rand(0,32000));
>> +               header("HTTP/1.1 200 OK", true);
>> +               header("Content-Type: multipart/mixed;
>> boundary=$this->boundry", true);
>> +       }
>> +
>> +       public function outputPart($part, $code)
>> +       {
>> +               $boundryHeader = "{$this->boundry}\n".
>> +                               "Content-Type:
>> application/http;version=1.1\n".
>> +                               "Content-Transfer-Encoding: binary\n\n";
>> +               echo $boundryHeader;
>> +               switch ($code) {
>> +                       case BAD_REQUEST:
>> +                               $code = '400 Bad Request';
>> +                               break;
>> +                       case UNAUTHORIZED:
>> +                               $code = '401 Unauthorized';
>> +                               break;
>> +                       case FORBIDDEN:
>> +                               $code = '403 Forbidden';
>> +                               break;
>> +                       case FORBIDDEN:
>> +                               $code = '404 Not Found';
>> +                               break;
>> +                       case NOT_IMPLEMENTED:
>> +                               $code = '501 Not Implemented';
>> +                               break;
>> +                       case INTERNAL_ERROR:
>> +                       default:
>> +                               $code = '200 OK';
>> +                               break;
>> +               }
>> +               echo "$code\n\n";
>> +               echo $part."\n";
>> +       }
>> }
>>
>> Modified:
>> incubator/shindig/trunk/php/src/social-api/converters/OutputJsonConverter.php
>> URL:
>> http://svn.apache.org/viewvc/incubator/shindig/trunk/php/src/social-api/converters/OutputJsonConverter.php?rev=678068&r1=678067&r2=678068&view=diff
>>
>> ==============================================================================
>> ---
>> incubator/shindig/trunk/php/src/social-api/converters/OutputJsonConverter.php
>> (original)
>> +++
>> incubator/shindig/trunk/php/src/social-api/converters/OutputJsonConverter.php
>> Fri Jul 18 15:47:41 2008
>> @@ -30,6 +30,17 @@
>>
>>        function outputBatch(Array $responses, SecurityToken $token)
>>        {
>> +               $this->boundryHeaders();
>> +               foreach ($responses as $response) {
>> +                       $request = $response['request'];
>> +                       $response = $response['response'];
>> +                       $part = json_encode($response);
>> +                       $this->outputPart($part, $response->getError());
>> +               }
>> +       }
>> +
>> +       function outputJsonBatch(Array $responses, SecurityToken $token)
>> +       {
>>                echo json_encode(array("responses" => $responses, "error"
>> => false));
>>        }
>> }
>>
>> Modified:
>> incubator/shindig/trunk/php/src/social-api/dataservice/PeopleHandler.php
>> URL:
>> http://svn.apache.org/viewvc/incubator/shindig/trunk/php/src/social-api/dataservice/PeopleHandler.php?rev=678068&r1=678067&r2=678068&view=diff
>>
>> ==============================================================================
>> ---
>> incubator/shindig/trunk/php/src/social-api/dataservice/PeopleHandler.php
>> (original)
>> +++
>> incubator/shindig/trunk/php/src/social-api/dataservice/PeopleHandler.php Fri
>> Jul 18 15:47:41 2008
>> @@ -20,7 +20,9 @@
>> class PeopleHandler extends DataRequestHandler {
>>        private $service;
>>        private static $PEOPLE_PATH =
>> "/people/{userId}/{groupId}/{personId}";
>> -       protected static $DEFAULT_PERSON_FIELDS = array("id", "name",
>> "thumbnailUrl");
>> +       //FIXME change this back to array("id", "name", "thumbnailUrl")
>> once the dust settles
>> +       // on the spec discussion related to this
>> +       protected static $DEFAULT_PERSON_FIELDS = array('all' => 'all');
>>
>>        public function __construct()
>>        {
>>
>> Modified:
>> incubator/shindig/trunk/php/src/social-api/dataservice/RestRequestItem.php
>> URL:
>> http://svn.apache.org/viewvc/incubator/shindig/trunk/php/src/social-api/dataservice/RestRequestItem.php?rev=678068&r1=678067&r2=678068&view=diff
>>
>> ==============================================================================
>> ---
>> incubator/shindig/trunk/php/src/social-api/dataservice/RestRequestItem.php
>> (original)
>> +++
>> incubator/shindig/trunk/php/src/social-api/dataservice/RestRequestItem.php
>> Fri Jul 18 15:47:41 2008
>> @@ -45,12 +45,12 @@
>>
>>        public function createRequestItemWithRequest($request, $token)
>>        {
>> -               $this->url = $request->url;
>> -               $this->parameters =
>> $this->createParameterMap($request->url);
>> +               $this->url = $request['url'];
>> +               $this->parameters =
>> $this->createParameterMap($request['url']);
>>                $this->token = $token;
>> -               $this->method = $request->method;
>> -               if (isset($request->postData)) {
>> -                       $this->postData = $request->postData;
>> +               $this->method = $request['method'];
>> +               if (isset($request['postData'])) {
>> +                       $this->postData = $request['postData'];
>>                }
>>        }
>>
>>
>> Modified: incubator/shindig/trunk/php/src/social-api/http/RestServlet.php
>> URL:
>> http://svn.apache.org/viewvc/incubator/shindig/trunk/php/src/social-api/http/RestServlet.php?rev=678068&r1=678067&r2=678068&view=diff
>>
>> ==============================================================================
>> --- incubator/shindig/trunk/php/src/social-api/http/RestServlet.php
>> (original)
>> +++ incubator/shindig/trunk/php/src/social-api/http/RestServlet.php Fri
>> Jul 18 15:47:41 2008
>> @@ -51,8 +51,6 @@
>> require 'src/social-api/converters/OutputAtomConverter.php';
>> require 'src/social-api/converters/OutputJsonConverter.php';
>>
>> -//FIXME Delete should respond with a 204 No Content to indicate success
>> -
>> class RestException extends Exception {}
>>
>> /*
>> @@ -64,42 +62,80 @@
>> define('FORBIDDEN', "forbidden");
>> define('BAD_REQUEST', "badRequest");
>> define('INTERNAL_ERROR', "internalError");
>> +//FIXME Delete should respond with a 204 No Content to indicate success
>>
>> class RestServlet extends HttpServlet {
>>
>> +       // The json Batch Route is used by the gadgets
>>        private static $JSON_BATCH_ROUTE = "jsonBatch";
>> +       // The Batch Proxy route is used the one defined in the RESTful
>> API specification
>> +       private static $BATCH_PROXY_ROUTE = "batchProxy";
>> +
>> +       public function doGet()
>> +       {
>> +               $this->doPost('GET');
>> +       }
>>
>> +       public function doPut()
>> +       {
>> +               $this->doPost('PUT');
>> +       }
>> +
>> +       public function doDelete()
>> +       {
>> +               $this->doPost('DELETE');
>> +       }
>> +
>>        public function doPost($method = 'POST')
>>        {
>> -               $this->setNoCache(true);
>> -               // if oauth, create a token from it's values instead of
>> one based on $_get['st']/$_post['st']
>> -               // NOTE : if no token is provided an anonymous one is
>> created (owner = viewer = appId = modId = 0)
>> -               // keep this in mind when creating your data services..
>> -               $token = $this->getSecurityToken();
>> -               $outputFormat = $this->getOutputFormat();
>> -               switch ($outputFormat) {
>> -                       case 'json':
>> -                               $this->setContentType('application/json');
>> -                               $outputConverter = new
>> OutputJsonConverter();
>> -                               break;
>> -                       case 'atom':
>> -
>> $this->setContentType('application/atom+xml');
>> -                               $outputConverter = new
>> OutputAtomConverter();
>> -                               break;
>> -                       default:
>> -                               $this->outputError(new
>> ResponseItem(NOT_IMPLEMENTED, "Invalid output format"));
>> -                               break;
>> -               }
>> -               if ($this->isBatchUrl()) {
>> -                       $responses = $this->handleBatchRequest($token);
>> -                       $outputConverter->outputBatch($responses, $token);
>> -               } else {
>> -                       $response = $this->handleSingleRequest($token,
>> $method);
>> -
>> $outputConverter->outputResponse($response['response'],
>> $response['request']);
>> +               try {
>> +                       $this->setNoCache(true);
>> +                       // if oauth, create a token from it's values
>> instead of one based on $_get['st']/$_post['st']
>> +                       // NOTE : if no token is provided an anonymous one
>> is created (owner = viewer = appId = modId = 0)
>> +                       // keep this in mind when creating your data
>> services..
>> +                       $token = $this->getSecurityToken();
>> +                       $outputFormat = $this->getOutputFormat();
>> +                       switch ($outputFormat) {
>> +                               case 'json':
>> +
>> $this->setContentType('application/json');
>> +                                       $outputConverter = new
>> OutputJsonConverter();
>> +                                       break;
>> +                               case 'atom':
>> +
>> $this->setContentType('application/atom+xml');
>> +                                       $outputConverter = new
>> OutputAtomConverter();
>> +                                       break;
>> +                               default:
>> +                                       $this->outputError(new
>> ResponseItem(NOT_IMPLEMENTED, "Invalid output format"));
>> +                                       break;
>> +                       }
>> +                       if ($this->isJsonBatchUrl()) {
>> +                               // custom json batch format used by the
>> gadgets
>> +                               $responses =
>> $this->handleJsonBatchRequest($token);
>> +
>> $outputConverter->outputJsonBatch($responses, $token);
>> +                       } elseif ($this->isBatchProxyUrl()) {
>> +                               // spec compliant batch proxy
>> +                               $this->noHeaders = true;
>> +                               $responses =
>> $this->handleBatchProxyRequest($token);
>> +                               $outputConverter->outputBatch($responses,
>> $token);
>> +                       } else {
>> +                               // single rest request
>> +                               $response = $this->handleRequest($token,
>> $method);
>> +
>> $outputConverter->outputResponse($response['response'],
>> $response['request']);
>> +                       }
>> +               } catch (Exception $e) {
>> +                       header("HTTP/1.0 500 Internal Server Error");
>> +                       echo "<html><body><h1>500 Internal Server
>> Error</h1>";
>> +                       if (Config::get('debug')) {
>> +                               echo "Message: ".$e->getMessage()."<br
>> />\n";
>> +                               echo "<pre>\n";
>> +                               print_r(debug_backtrace());
>> +                               echo "\n</pre>";
>> +                       }
>> +                       echo "</body></html>";
>>                }
>>        }
>>
>> -       private function handleSingleRequest($token, $method)
>> +       private function handleRequest($token, $method)
>>        {
>>                $params = $this->getListParams();
>>                $requestItem = new RestRequestItem();
>> @@ -109,14 +145,115 @@
>>                $responseItem = $this->getResponseItem($requestItem);
>>                return array('request' => $requestItem, 'response' =>
>> $responseItem);
>>        }
>> -
>> -       private function getRouteFromParameter($pathInfo)
>> +
>> +       private function handleJsonBatchRequest($token)
>>        {
>> -               $pathInfo = substr($pathInfo, 1);
>> -               $indexOfNextPathSeparator = strpos($pathInfo, "/");
>> -               return $indexOfNextPathSeparator != - 1 ?
>> substr($pathInfo, 0, $indexOfNextPathSeparator) : $pathInfo;
>> +               // we support both a raw http post (without
>> application/x-www-form-urlencoded headers) like java does
>> +               // and a more php / curl safe version of a form post with
>> 'request' as the post field that holds the request json data
>> +               if (isset($GLOBALS['HTTP_RAW_POST_DATA']) ||
>> isset($_POST['request'])) {
>> +                       $requests = $this->getRequestParams();
>> +                       $responses = array();
>> +                       foreach ($requests as $key => $value) {
>> +                               $requestItem = new RestRequestItem();
>> +
>> $requestItem->createRequestItemWithRequest($value, $token);
>> +                               $responses[$key] =
>> $this->getResponseItem($requestItem);
>> +                       }
>> +                       return $responses;
>> +               } else {
>> +                       throw new Exception("No post data set");
>> +               }
>> +       }
>> +
>> +       private function handleBatchProxyRequest($token)
>> +       {
>> +               // Is this is a multipath/mixed post? Check content type:
>> +               if (isset($GLOBALS['HTTP_RAW_POST_DATA']) &&
>> strpos($_SERVER['CONTENT_TYPE'], 'multipart/mixed') !== false &&
>> strpos($_SERVER['CONTENT_TYPE'],'boundary=') !== false) {
>> +                       // Ok looks swell, see what our boundry is..
>> +                       $boundry = substr($_SERVER['CONTENT_TYPE'],
>> strpos($_SERVER['CONTENT_TYPE'],'boundary=') + strlen('boundary='));
>> +                       // Split up requests per boundry
>> +                       $requests = explode($boundry,
>> $GLOBALS['HTTP_RAW_POST_DATA']);
>> +                       $responses = array();
>> +                       foreach ($requests as $request) {
>> +                               $request = trim($request);
>> +                               if (!empty($request)) {
>> +                                       // extractBatchRequest() does the
>> magic parsing of the raw post data to a meaninful request array
>> +                                       $request =
>> $this->extractBatchRequest($request);
>> +                                       $requestItem = new
>> RestRequestItem();
>> +
>> $requestItem->createRequestItemWithRequest($request, $token);
>> +                                       $responses[] = array('request' =>
>> $requestItem, 'response' => $this->getResponseItem($requestItem));
>> +                               }
>> +                       }
>> +               } else {
>> +                       $this->outputError(new ResponseItem(BAD_REQUEST,
>> "Invalid multipart/mixed request"));
>> +               }
>> +               return $responses;
>>        }
>>
>> +       private function extractBatchRequest($request)
>> +       {
>> +               /* Multipart request is formatted like:
>> +                * -batch-a73hdj3dy3mm347ddjjdf
>> +                * Content-Type: application/http;version=1.1
>> +                * Content-Transfer-Encoding: binary
>> +                *
>> +                * GET
>> /people/@me/@friends?startPage=5&count=10&format=json
>> +                * Host: api.example.org
>> +                * If-None-Match: "837dyfkdi39df"
>> +                *
>> +                * but we only want to have the last bit (the actual
>> request), this filters that down first
>> +                */
>> +               $emptyFound = false;
>> +               $requestLines = explode("\n", $request);
>> +               $request = '';
>> +               foreach ($requestLines as $line) {
>> +                       if ($emptyFound) {
>> +                               $request .= $line."\n";
>> +                       } elseif (empty($line)) {
>> +                               $emptyFound = true;
>> +                       }
>> +               }
>> +               // Now that we have the basic request in $request, split
>> that up again & parse it into a meaningful representation
>> +               $firstFound = $emptyFound = false;
>> +               $requestLines = explode("\n", $request);
>> +               $request = array();
>> +               $request['headers'] = array();
>> +               $request['postData'] = '';
>> +               foreach ($requestLines as $line) {
>> +                       if (!$firstFound) {
>> +                               $firstFound = true;
>> +                               $parts = explode(' ', trim($line));
>> +                               if (count($parts) != 2) {
>> +                                       throw new Exception("Mallshaped
>> request uri in multipart block");
>> +                               }
>> +                               $request['method'] =
>> strtoupper(trim($parts[0]));
>> +                               // cut it down to an actual meaningful url
>> without the prefix/social/rest part right away
>> +                               $request['url'] = substr(trim($parts[1]),
>> strlen(Config::get('web_prefix') . '/social/rest'));
>> +                       } elseif (!$emptyFound && !empty($line)) {
>> +                               // convert the key to the PHP
>> 'CONTENT_TYPE' style naming convention.. it's ugly but consitent
>> +                               $key = str_replace('-', '_',
>> strtoupper(trim(substr($line, 0, strpos($line, ':')))));
>> +                               $val = trim(substr($line, strpos($line,
>> ':') + 1));
>> +                               $request['headers'][$key] = $val;
>> +                       } elseif (!$emptyFound && empty($line)) {
>> +                               $emptyFound = true;
>> +                       } else {
>> +                               if (get_magic_quotes_gpc()) {
>> +                                       $line = stripslashes($line);
>> +                               }
>> +                               $request['postData'] .= $line."\n";
>> +                       }
>> +               }
>> +               if (empty($request['postData'])) {
>> +                       // don't trip the requestItem into thinking there
>> is postData when there's not
>> +                       unset($request['postData']);
>> +               } else {
>> +                       // if there was a post data blob present, decode
>> it into an array, the format is based on the
>> +                       // content type header, which is either
>> application/json or
>> +                       $format =
>> isset($request['headers']['CONTENT_TYPE']) &&
>> strtolower($request['headers']['CONTENT_TYPE']) == 'application/atom+xml' ?
>> 'atom' : 'json';
>> +                       $request['postData'] =
>> $this->decodeRequests($request['postData'], $format);
>> +               }
>> +               return $request;
>> +       }
>> +
>>        private function getResponseItem(RestRequestItem $requestItem)
>>        {
>>                $path =
>> $this->getRouteFromParameter($requestItem->getUrl());
>> @@ -140,12 +277,26 @@
>>                        $class = new $class(null);
>>                        $response = $class->handleMethod($requestItem);
>>                }
>> -               if ($response->getError() != null && !$this->isBatchUrl())
>> {
>> +               if ($response->getError() != null &&
>> !$this->isJsonBatchUrl() && !$this->isBatchProxyUrl()) {
>>                        // Can't use http error codes in batch mode,
>> instead we return the error code in the response item
>>                        $this->outputError($response);
>>                }
>>                return $response;
>>        }
>> +
>> +       private function decodeRequests($requestParam, $format = 'json')
>> +       {
>> +               // temp hack until i know what the intended way to detect
>> format is
>> +               if ($format == 'json') {
>> +                       return json_decode($requestParam, true);
>> +               } elseif ($format == 'atom') {
>> +                       $xml = simplexml_load_string($requestParam);
>> +                       print_r($xml);
>> +                       return $xml;
>> +               } else {
>> +                       throw Exception("Invalid or unsupported input
>> format");
>> +               }
>> +       }
>>
>>        private function getRequestParams()
>>        {
>> @@ -154,44 +305,14 @@
>>                if (get_magic_quotes_gpc()) {
>>                        $requestParam = stripslashes($requestParam);
>>                }
>> -               $requests = json_decode($requestParam);
>> -               if ($requests == (isset($GLOBALS['HTTP_RAW_POST_DATA']) ?
>> $GLOBALS['HTTP_RAW_POST_DATA'] : $post)) {
>> -                       return new ResponseItem(BAD_REQUEST, "Malformed
>> json string");
>> -               }
>> -               return $requests;
>> -       }
>> -
>> -       private function handleBatchRequest($token)
>> -       {
>> -               // we support both a raw http post (without
>> application/x-www-form-urlencoded headers) like java does
>> -               // and a more php / curl safe version of a form post with
>> 'request' as the post field that holds the request json data
>> -               if (isset($GLOBALS['HTTP_RAW_POST_DATA']) ||
>> isset($_POST['request'])) {
>> -                       $requests = $this->getRequestParams();
>> -                       $responses = array();
>> -                       foreach ($requests as $key => $value) {
>> -                               $requestItem = new RestRequestItem();
>> -
>> $requestItem->createRequestItemWithRequest($value, $token);
>> -                               $responses[$key] =
>> $this->getResponseItem($requestItem);
>> -                       }
>> -                       return $responses;
>> -               } else {
>> -                       throw new Exception("No post data set");
>> -               }
>> -       }
>> -
>> -       public function doGet()
>> -       {
>> -               $this->doPost('GET');
>> -       }
>> -
>> -       public function doPut()
>> -       {
>> -               $this->doPost('PUT');
>> +               return $this->decodeRequests($requestParam);
>>        }
>>
>> -       public function doDelete()
>> +       private function getRouteFromParameter($pathInfo)
>>        {
>> -               $this->doPost('DELETE');
>> +               $pathInfo = substr($pathInfo, 1);
>> +               $indexOfNextPathSeparator = strpos($pathInfo, "/");
>> +               return $indexOfNextPathSeparator != - 1 ?
>> substr($pathInfo, 0, $indexOfNextPathSeparator) : $pathInfo;
>>        }
>>
>>        private function outputError(ResponseItem $response)
>> @@ -217,7 +338,6 @@
>>                        default:
>>                                $code = '500 Internal Server Error';
>>                                break;
>> -
>>                }
>>                header("HTTP/1.0 $code", true);
>>                echo "$code - $errorMessage";
>> @@ -265,8 +385,13 @@
>>                return substr($_SERVER["REQUEST_URI"],
>> strlen(Config::get('web_prefix') . '/social/rest'));
>>        }
>>
>> -       public function isBatchUrl()
>> +       public function isJsonBatchUrl()
>>        {
>>                return strrpos($_SERVER["REQUEST_URI"],
>> RestServlet::$JSON_BATCH_ROUTE) > 0;
>>        }
>> +
>> +       public function isBatchProxyUrl()
>> +       {
>> +               return strrpos($_SERVER["REQUEST_URI"],
>> RestServlet::$BATCH_PROXY_ROUTE) > 0;
>> +       }
>> }
>>
>> Modified:
>> incubator/shindig/trunk/php/src/social-api/samplecontainer/BasicAppDataService.php
>> URL:
>> http://svn.apache.org/viewvc/incubator/shindig/trunk/php/src/social-api/samplecontainer/BasicAppDataService.php?rev=678068&r1=678067&r2=678068&view=diff
>>
>> ==============================================================================
>> ---
>> incubator/shindig/trunk/php/src/social-api/samplecontainer/BasicAppDataService.php
>> (original)
>> +++
>> incubator/shindig/trunk/php/src/social-api/samplecontainer/BasicAppDataService.php
>> Fri Jul 18 15:47:41 2008
>> @@ -85,7 +85,7 @@
>>                switch($groupId->getType()) {
>>                        case 'self':
>>                                foreach ($fields as $key) {
>> -                                       $value = isset($values->$key) ?
>> $values->$key : (@isset($values[$key]) ? @$values[$key] : null);
>> +                                       $value = isset($values[$key]) ?
>> @$values[$key] : null;
>>
>>  XmlStateFileFetcher::get()->setAppData($userId->getUserId($token), $key,
>> $value);
>>                                }
>>                                break;
>>
>> Modified:
>> incubator/shindig/trunk/php/src/social-api/samplecontainer/BasicPeopleService.php
>> URL:
>> http://svn.apache.org/viewvc/incubator/shindig/trunk/php/src/social-api/samplecontainer/BasicPeopleService.php?rev=678068&r1=678067&r2=678068&view=diff
>>
>> ==============================================================================
>> ---
>> incubator/shindig/trunk/php/src/social-api/samplecontainer/BasicPeopleService.php
>> (original)
>> +++
>> incubator/shindig/trunk/php/src/social-api/samplecontainer/BasicPeopleService.php
>> Fri Jul 18 15:47:41 2008
>> @@ -76,7 +76,7 @@
>>                                if ($id == $token->getOwnerId()) {
>>                                        $person->setIsOwner(true);
>>                                }
>> -                               if (is_array($profileDetails) &&
>> count($profileDetails)) {
>> +                               if (is_array($profileDetails) &&
>> count($profileDetails) && !in_array('all', $profileDetails)) {
>>                                        $newPerson = array();
>>                                        $newPerson['isOwner'] =
>> $person->isOwner;
>>                                        $newPerson['isViewer'] =
>> $person->isViewer;
>>
>>
>


-- 
.-. --- .--. ..-
R o p u

initial proxyBatch support

Posted by Chris Chabot <ch...@xs4all.nl>.
This has been a bit of a headache to develop (see spec list) but it's  
starting to take some shape. It's also quite difficult to test for me,  
since nothing in PHP really supports multipart http posts... So i had  
to hand craft both a test client, the input parsing in shindig, and  
the multi part output construct too.

In other words, it works with the input that my test client creates,  
but i have no way to absolutely verify it is all completely valid http  
protocol compliant stuff ... ah the joys of developing in the dark  
with blindfolds on :)

The batch proxy is advertised through XRDS simple:
http://www.chabotc.com/xrds-test.php?url=http://www.partuza.nl

When i shoot a hand crafted multipart payload into it it works & you  
can specify how you want your (presumably http multipart compliant)  
output on the main url (to batchProxy) with ?format=atom or ? 
format=json (json is assumed by default). You could even poke at it at http://modules.partuza.nl/social/rest/batchProxy 
  .. just make sure to grab a valid token from a gadget iframe first  
to use in your requests and your golden :) The only missing bits are  
OAuth support, and Atom format input support, they will hopefully  
follow shortly.

However my main question ... does anyone have any useful tools for  
actually testing and verifying multipart http posts & the multipart  
response ? :)

	-- Chris

Begin forwarded message:

> From: chabotc@apache.org
> Date: July 19, 2008 12:47:41 AM GMT+02:00
> To: shindig-commits@incubator.apache.org
> Subject: svn commit: r678068 - in /incubator/shindig/trunk/php/src/ 
> social-api: converters/ dataservice/ http/ samplecontainer/
> Reply-To: shindig-dev@incubator.apache.org
>
> Author: chabotc
> Date: Fri Jul 18 15:47:41 2008
> New Revision: 678068
>
> URL: http://svn.apache.org/viewvc?rev=678068&view=rev
> Log:
> Initial support for the batch proxy request type.
>
> The url is /social/rest/batchProxy and it uses the http multipart
> format as described in the RESTful API specification.
>
> It only supports ONE output format for a set of requests (which
> is determined by the main url ?format=foo param, and defaults to
> json). Input however will support mixing json and atom.
>
> Atom input is still missing (has a debug dump right now) but support
> for that will follow quickly.
>
> OAuth and Atom input is still missing, but once their done PHP
> Shindig will have full RESTful spec support, we're getting there!
>
>
> Modified:
>    incubator/shindig/trunk/php/src/social-api/converters/ 
> OutputAtomConverter.php
>    incubator/shindig/trunk/php/src/social-api/converters/ 
> OutputConverter.php
>    incubator/shindig/trunk/php/src/social-api/converters/ 
> OutputJsonConverter.php
>    incubator/shindig/trunk/php/src/social-api/dataservice/ 
> PeopleHandler.php
>    incubator/shindig/trunk/php/src/social-api/dataservice/ 
> RestRequestItem.php
>    incubator/shindig/trunk/php/src/social-api/http/RestServlet.php
>    incubator/shindig/trunk/php/src/social-api/samplecontainer/ 
> BasicAppDataService.php
>    incubator/shindig/trunk/php/src/social-api/samplecontainer/ 
> BasicPeopleService.php
>
> Modified: incubator/shindig/trunk/php/src/social-api/converters/ 
> OutputAtomConverter.php
> URL: http://svn.apache.org/viewvc/incubator/shindig/trunk/php/src/social-api/converters/OutputAtomConverter.php?rev=678068&r1=678067&r2=678068&view=diff
> = 
> = 
> = 
> = 
> = 
> = 
> = 
> = 
> ======================================================================
> --- incubator/shindig/trunk/php/src/social-api/converters/ 
> OutputAtomConverter.php (original)
> +++ incubator/shindig/trunk/php/src/social-api/converters/ 
> OutputAtomConverter.php Fri Jul 18 15:47:41 2008
> @@ -1,4 +1,5 @@
> <?php
> +
> /*
>  * Licensed to the Apache Software Foundation (ASF) under one
>  * or more contributor license agreements. See the NOTICE file
> @@ -19,7 +20,7 @@
>
> /**
>  * Format = atom output converter, for format definition see:
> - * http://www.opensocial.org/Technical-Resources/opensocial-specification----implementation-version-08/restful-api-specification
> + * http://sites.google.com/a/opensocial.org/opensocial/Technical-Resources/opensocial-spec-v08/restful-api-specification
>  */
> class OutputAtomConverter extends OutputConverter {
> 	private static $nameSpace = 'http://www.w3.org/2005/Atom';
> @@ -40,7 +41,7 @@
> 		$data = $responseItem->getResponse();
> 		$userId = $requestItem->getUser()->getUserId($requestItem- 
> >getToken());
> 		$guid = 'urn:guid:' . $userId;
> -		$authorName = $_SERVER['HTTP_HOST'].':'.$userId;
> +		$authorName = $_SERVER['HTTP_HOST'] . ':' . $userId;
> 		$updatedAtom = date(DATE_ATOM);
> 		
> 		// Check to see if this is a single entry, or a collection, and  
> construct either an atom
> @@ -52,20 +53,18 @@
> 			
> 			// The root Feed element
> 			$entry = $this->addNode($doc, 'feed', '', false, self::$nameSpace);
> -
> +			
> 			// Required Atom fields
> 			$endPos = ($startIndex + $itemsPerPage) > $totalResults ?  
> $totalResults : ($startIndex + $itemsPerPage);
> -			$this->addNode($entry, 'title', $requestType.' feed for id '. 
> $authorName.' ('.$startIndex. ' - '. ($endPos - 1).' of '. 
> $totalResults.')');
> +			$this->addNode($entry, 'title', $requestType . ' feed for id ' .  
> $authorName . ' (' . $startIndex . ' - ' . ($endPos - 1) . ' of ' .  
> $totalResults . ')');
> 			$author = $this->addNode($entry, 'author');
> 			$this->addNode($author, 'uri', $guid);
> -			$this->addNode($author, 'name', $authorName);			
> +			$this->addNode($author, 'name', $authorName);
> 			$this->addNode($entry, 'updated', $updatedAtom);
> 			$this->addNode($entry, 'id', $guid);
> -			$this->addNode($entry, 'link', '', array('rel' => 'self', 'href'  
> => 'http://'.$_SERVER['HTTP_HOST'].$_SERVER['REQUEST_URI']));
> -			
> +			$this->addNode($entry, 'link', '', array('rel' => 'self', 'href'  
> => 'http://' . $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI']));
> 			// Add osearch & next link to the entry
> 			$this->addPagingFields($entry, $startIndex, $itemsPerPage,  
> $totalResults);
> -			
> 			// Add response entries to feed
> 			$responses = $responseItem->getResponse()->getEntry();
> 			foreach ($responses as $response) {
> @@ -79,11 +78,10 @@
> 				$this->addNode($author, 'uri', $guid);
> 				$this->addNode($author, 'name', $authorName);
> 				// Special hoisting rules for activities
> -				
> 				if ($response instanceof Activity) {
> 					$this->addNode($feedEntry, 'category', '', array('term' =>  
> 'status'));
> 					$this->addNode($feedEntry, 'updated', date(DATE_ATOM, $response- 
> >postedTime));
> -					$this->addNode($feedEntry, 'id', 'urn:guid:'.$response->id);
> +					$this->addNode($feedEntry, 'id', 'urn:guid:' . $response->id);
> 					//FIXME should add a link field but don't have URL's available  
> yet:
> 					// <link rel="self" type="application/atom+xml" href="http://api.example.org/activity/feeds/.../af3778 
> "/>
> 					$this->addNode($feedEntry, 'title', strip_tags($response- 
> >title));
> @@ -94,43 +92,49 @@
> 					unset($response->title);
> 					unset($response->body);
> 				} else {
> -					$this->addNode($feedEntry, 'id', 'urn:guid:'.$idField);
> -					$this->addNode($feedEntry, 'title', $requestType.' feed entry  
> for id '.$idField);
> +					$this->addNode($feedEntry, 'id', 'urn:guid:' . $idField);
> +					$this->addNode($feedEntry, 'title', $requestType . ' feed  
> entry for id ' . $idField);
> 					$this->addNode($feedEntry, 'updated', $updatedAtom);
> 				}
> 				
> 				// recursively add responseItem data to the xml structure
> 				$this->addData($content, $requestType, $response, self:: 
> $osNameSpace);
> 			}
> -			
> 		} else {
> -			
> 			// Single entry = Atom:Entry	
> 			$entry = $doc->appendChild($doc->createElementNS(self:: 
> $nameSpace, "entry"));
> -			
> 			// Atom fields
> -			$this->addNode($entry, 'title', $requestType.' entry for '. 
> $authorName);
> +			$this->addNode($entry, 'title', $requestType . ' entry for ' .  
> $authorName);
> 			$author = $this->addNode($entry, 'author');
> 			$this->addNode($author, 'uri', $guid);
> 			$this->addNode($author, 'name', $authorName);
> 			$this->addNode($entry, 'id', $guid);
> 			$this->addNode($entry, 'updated', $updatedAtom);
> 			$content = $this->addNode($entry, 'content', '', array('type' =>  
> 'application/xml'));
> -			
> 			// addData loops through the responseItem data recursively  
> creating a matching XML structure
> 			$this->addData($content, $requestType, $data, self::$osNameSpace);
> 		}
> 		$xml = $doc->saveXML();
> -		if (self::$includeOsearch && $responseItem->getResponse()  
> instanceof RestFulCollection) {
> +		if ($responseItem->getResponse() instanceof RestFulCollection) {
> 			//FIXME dirty hack until i find a way to add multiple name spaces  
> using DomXML functions
> -			$xml = str_replace('<feed xmlns="http://www.w3.org/2005/Atom">',  
> '<feed xmlns="http://www.w3.org/2005/Atom" xmlos:osearch="http://a9.com/-/spec/opensearch/1.1 
> ">' ,$xml);
> +			$xml = str_replace('<feed xmlns="http://www.w3.org/2005/Atom">',  
> '<feed xmlns="http://www.w3.org/2005/Atom" xmlns:osearch="http://a9.com/-/spec/opensearch/1.1 
> ">', $xml);
> 		}
> 		echo $xml;
> 	}
>
> 	function outputBatch(Array $responses, SecurityToken $token)
> 	{
> -		//TODO once we support spec compliance batching, this needs to be  
> added too
> +		$this->boundryHeaders();
> +		foreach ($responses as $response) {
> +			$request = $response['request'];
> +			$response = $response['response'];
> +			// output buffering supports multiple levels of it.. it's a nice  
> feature to abuse :)
> +			ob_start();
> +			$this->outputResponse($response, $request);
> +			$part = ob_get_contents();
> +			ob_end_clean();
> +			$this->outputPart($part, $response->getError());
> +		}
> 	}
>
> 	/**
> @@ -174,11 +178,9 @@
> 	 */
> 	private function addPagingFields($entry, $startIndex,  
> $itemsPerPage, $totalResults)
> 	{
> -		if (self::$includeOsearch) {
> -			$this->addNode($entry, 'osearch:totalResults', $totalResults);
> -			$this->addNode($entry, 'osearch:startIndex', $startIndex ?  
> $startIndex : '0');
> -			$this->addNode($entry, 'osearch:itemsPerPage', $itemsPerPage);
> -		}
> +		$this->addNode($entry, 'osearch:totalResults', $totalResults);
> +		$this->addNode($entry, 'osearch:startIndex', $startIndex ?  
> $startIndex : '0');
> +		$this->addNode($entry, 'osearch:itemsPerPage', $itemsPerPage);
> 		// Create a 'next' link based on our current url if this is a  
> pageable collection & there is more to display
> 		if (($startIndex + $itemsPerPage) < $totalResults) {
> 			$nextStartIndex = ($startIndex + $itemsPerPage) - 1;
> @@ -255,6 +257,9 @@
> 					}
> 					$this->addData($newElement, $key, $val);
> 				} else {
> +					if (is_numeric($key)) {
> +						$key = is_object($val) ? get_class($val) : $key = $name;
> +					}					
> 					$elm = $newElement->appendChild($this->doc->createElement($key));
> 					$elm->appendChild($this->doc->createTextNode($val));
> 				}
>
> Modified: incubator/shindig/trunk/php/src/social-api/converters/ 
> OutputConverter.php
> URL: http://svn.apache.org/viewvc/incubator/shindig/trunk/php/src/social-api/converters/OutputConverter.php?rev=678068&r1=678067&r2=678068&view=diff
> = 
> = 
> = 
> = 
> = 
> = 
> = 
> = 
> ======================================================================
> --- incubator/shindig/trunk/php/src/social-api/converters/ 
> OutputConverter.php (original)
> +++ incubator/shindig/trunk/php/src/social-api/converters/ 
> OutputConverter.php Fri Jul 18 15:47:41 2008
> @@ -22,6 +22,50 @@
>  *
>  */
> abstract class OutputConverter {
> +	private $boundry;
> +	
> 	abstract function outputResponse(ResponseItem $responseItem,  
> RestRequestItem $requestItem);
> 	abstract function outputBatch(Array $responses, SecurityToken  
> $token);
> +	
> +	/**
> +	 * Output the multipart/mixed headers and returns the boundry  
> token used
> +	 *
> +	 */
> +	public function boundryHeaders()
> +	{
> +		$this->boundry = '--batch-'.md5(rand(0,32000));
> +		header("HTTP/1.1 200 OK", true);
> +		header("Content-Type: multipart/mixed; boundary=$this->boundry",  
> true);
> +	}
> +	
> +	public function outputPart($part, $code)
> +	{
> +		$boundryHeader = "{$this->boundry}\n".
> +				"Content-Type: application/http;version=1.1\n".
> +				"Content-Transfer-Encoding: binary\n\n";
> +		echo $boundryHeader;
> +		switch ($code) {
> +			case BAD_REQUEST:
> +				$code = '400 Bad Request';
> +				break;
> +			case UNAUTHORIZED:
> +				$code = '401 Unauthorized';
> +				break;
> +			case FORBIDDEN:
> +				$code = '403 Forbidden';
> +				break;
> +			case FORBIDDEN:
> +				$code = '404 Not Found';
> +				break;
> +			case NOT_IMPLEMENTED:
> +				$code = '501 Not Implemented';
> +				break;
> +			case INTERNAL_ERROR:
> +			default:
> +				$code = '200 OK';
> +				break;
> +		}
> +		echo "$code\n\n";
> +		echo $part."\n";
> +	}
> }
>
> Modified: incubator/shindig/trunk/php/src/social-api/converters/ 
> OutputJsonConverter.php
> URL: http://svn.apache.org/viewvc/incubator/shindig/trunk/php/src/social-api/converters/OutputJsonConverter.php?rev=678068&r1=678067&r2=678068&view=diff
> = 
> = 
> = 
> = 
> = 
> = 
> = 
> = 
> ======================================================================
> --- incubator/shindig/trunk/php/src/social-api/converters/ 
> OutputJsonConverter.php (original)
> +++ incubator/shindig/trunk/php/src/social-api/converters/ 
> OutputJsonConverter.php Fri Jul 18 15:47:41 2008
> @@ -30,6 +30,17 @@
> 	
> 	function outputBatch(Array $responses, SecurityToken $token)
> 	{
> +		$this->boundryHeaders();
> +		foreach ($responses as $response) {
> +			$request = $response['request'];
> +			$response = $response['response'];
> +			$part = json_encode($response);
> +			$this->outputPart($part, $response->getError());
> +		}
> +	}
> +	
> +	function outputJsonBatch(Array $responses, SecurityToken $token)
> +	{
> 		echo json_encode(array("responses" => $responses, "error" =>  
> false));
> 	}
> }
>
> Modified: incubator/shindig/trunk/php/src/social-api/dataservice/ 
> PeopleHandler.php
> URL: http://svn.apache.org/viewvc/incubator/shindig/trunk/php/src/social-api/dataservice/PeopleHandler.php?rev=678068&r1=678067&r2=678068&view=diff
> = 
> = 
> = 
> = 
> = 
> = 
> = 
> = 
> ======================================================================
> --- incubator/shindig/trunk/php/src/social-api/dataservice/ 
> PeopleHandler.php (original)
> +++ incubator/shindig/trunk/php/src/social-api/dataservice/ 
> PeopleHandler.php Fri Jul 18 15:47:41 2008
> @@ -20,7 +20,9 @@
> class PeopleHandler extends DataRequestHandler {
> 	private $service;
> 	private static $PEOPLE_PATH = "/people/{userId}/{groupId}/ 
> {personId}";
> -	protected static $DEFAULT_PERSON_FIELDS = array("id", "name",  
> "thumbnailUrl");
> +	//FIXME change this back to array("id", "name", "thumbnailUrl")  
> once the dust settles
> +	// on the spec discussion related to this
> +	protected static $DEFAULT_PERSON_FIELDS = array('all' => 'all');
>
> 	public function __construct()
> 	{
>
> Modified: incubator/shindig/trunk/php/src/social-api/dataservice/ 
> RestRequestItem.php
> URL: http://svn.apache.org/viewvc/incubator/shindig/trunk/php/src/social-api/dataservice/RestRequestItem.php?rev=678068&r1=678067&r2=678068&view=diff
> = 
> = 
> = 
> = 
> = 
> = 
> = 
> = 
> ======================================================================
> --- incubator/shindig/trunk/php/src/social-api/dataservice/ 
> RestRequestItem.php (original)
> +++ incubator/shindig/trunk/php/src/social-api/dataservice/ 
> RestRequestItem.php Fri Jul 18 15:47:41 2008
> @@ -45,12 +45,12 @@
>
> 	public function createRequestItemWithRequest($request, $token)
> 	{
> -		$this->url = $request->url;
> -		$this->parameters = $this->createParameterMap($request->url);
> +		$this->url = $request['url'];
> +		$this->parameters = $this->createParameterMap($request['url']);
> 		$this->token = $token;
> -		$this->method = $request->method;
> -		if (isset($request->postData)) {
> -			$this->postData = $request->postData;
> +		$this->method = $request['method'];
> +		if (isset($request['postData'])) {
> +			$this->postData = $request['postData'];
> 		}
> 	}
>
>
> Modified: incubator/shindig/trunk/php/src/social-api/http/ 
> RestServlet.php
> URL: http://svn.apache.org/viewvc/incubator/shindig/trunk/php/src/social-api/http/RestServlet.php?rev=678068&r1=678067&r2=678068&view=diff
> = 
> = 
> = 
> = 
> = 
> = 
> = 
> = 
> ======================================================================
> --- incubator/shindig/trunk/php/src/social-api/http/RestServlet.php  
> (original)
> +++ incubator/shindig/trunk/php/src/social-api/http/RestServlet.php  
> Fri Jul 18 15:47:41 2008
> @@ -51,8 +51,6 @@
> require 'src/social-api/converters/OutputAtomConverter.php';
> require 'src/social-api/converters/OutputJsonConverter.php';
>
> -//FIXME Delete should respond with a 204 No Content to indicate  
> success
> -
> class RestException extends Exception {}
>
> /*
> @@ -64,42 +62,80 @@
> define('FORBIDDEN', "forbidden");
> define('BAD_REQUEST', "badRequest");
> define('INTERNAL_ERROR', "internalError");
> +//FIXME Delete should respond with a 204 No Content to indicate  
> success
>
> class RestServlet extends HttpServlet {
> 	
> +	// The json Batch Route is used by the gadgets
> 	private static $JSON_BATCH_ROUTE = "jsonBatch";
> +	// The Batch Proxy route is used the one defined in the RESTful  
> API specification
> +	private static $BATCH_PROXY_ROUTE = "batchProxy";
> +
> +	public function doGet()
> +	{
> +		$this->doPost('GET');
> +	}
>
> +	public function doPut()
> +	{
> +		$this->doPost('PUT');
> +	}
> +
> +	public function doDelete()
> +	{
> +		$this->doPost('DELETE');
> +	}
> +		
> 	public function doPost($method = 'POST')
> 	{
> -		$this->setNoCache(true);
> -		// if oauth, create a token from it's values instead of one based  
> on $_get['st']/$_post['st']
> -		// NOTE : if no token is provided an anonymous one is created  
> (owner = viewer = appId = modId = 0)
> -		// keep this in mind when creating your data services..
> -		$token = $this->getSecurityToken();
> -		$outputFormat = $this->getOutputFormat();
> -		switch ($outputFormat) {
> -			case 'json':
> -				$this->setContentType('application/json');
> -				$outputConverter = new OutputJsonConverter();
> -				break;
> -			case 'atom':
> -				$this->setContentType('application/atom+xml');
> -				$outputConverter = new OutputAtomConverter();
> -				break;
> -			default:
> -				$this->outputError(new ResponseItem(NOT_IMPLEMENTED, "Invalid  
> output format"));
> -				break;
> -		}
> -		if ($this->isBatchUrl()) {
> -			$responses = $this->handleBatchRequest($token);
> -			$outputConverter->outputBatch($responses, $token);
> -		} else {
> -			$response = $this->handleSingleRequest($token, $method);
> -			$outputConverter->outputResponse($response['response'],  
> $response['request']);
> +		try {
> +			$this->setNoCache(true);
> +			// if oauth, create a token from it's values instead of one  
> based on $_get['st']/$_post['st']
> +			// NOTE : if no token is provided an anonymous one is created  
> (owner = viewer = appId = modId = 0)
> +			// keep this in mind when creating your data services..
> +			$token = $this->getSecurityToken();
> +			$outputFormat = $this->getOutputFormat();
> +			switch ($outputFormat) {
> +				case 'json':
> +					$this->setContentType('application/json');
> +					$outputConverter = new OutputJsonConverter();
> +					break;
> +				case 'atom':
> +					$this->setContentType('application/atom+xml');
> +					$outputConverter = new OutputAtomConverter();
> +					break;
> +				default:
> +					$this->outputError(new ResponseItem(NOT_IMPLEMENTED, "Invalid  
> output format"));
> +					break;
> +			}
> +			if ($this->isJsonBatchUrl()) {
> +				// custom json batch format used by the gadgets
> +				$responses = $this->handleJsonBatchRequest($token);
> +				$outputConverter->outputJsonBatch($responses, $token);
> +			} elseif ($this->isBatchProxyUrl()) {
> +				// spec compliant batch proxy
> +				$this->noHeaders = true;
> +				$responses = $this->handleBatchProxyRequest($token);
> +				$outputConverter->outputBatch($responses, $token);
> +			} else {
> +				// single rest request
> +				$response = $this->handleRequest($token, $method);
> +				$outputConverter->outputResponse($response['response'],  
> $response['request']);
> +			}
> +		} catch (Exception $e) {
> +			header("HTTP/1.0 500 Internal Server Error");
> +			echo "<html><body><h1>500 Internal Server Error</h1>";
> +			if (Config::get('debug')) {
> +				echo "Message: ".$e->getMessage()."<br />\n";
> +				echo "<pre>\n";
> +				print_r(debug_backtrace());
> +				echo "\n</pre>";
> +			}
> +			echo "</body></html>";
> 		}
> 	}
>
> -	private function handleSingleRequest($token, $method)
> +	private function handleRequest($token, $method)
> 	{
> 		$params = $this->getListParams();
> 		$requestItem = new RestRequestItem();
> @@ -109,14 +145,115 @@
> 		$responseItem = $this->getResponseItem($requestItem);
> 		return array('request' => $requestItem, 'response' =>  
> $responseItem);
> 	}
> -
> -	private function getRouteFromParameter($pathInfo)
> +	
> +	private function handleJsonBatchRequest($token)
> 	{
> -		$pathInfo = substr($pathInfo, 1);
> -		$indexOfNextPathSeparator = strpos($pathInfo, "/");
> -		return $indexOfNextPathSeparator != - 1 ? substr($pathInfo, 0,  
> $indexOfNextPathSeparator) : $pathInfo;
> +		// we support both a raw http post (without application/x-www- 
> form-urlencoded headers) like java does
> +		// and a more php / curl safe version of a form post with  
> 'request' as the post field that holds the request json data
> +		if (isset($GLOBALS['HTTP_RAW_POST_DATA']) ||  
> isset($_POST['request'])) {
> +			$requests = $this->getRequestParams();
> +			$responses = array();
> +			foreach ($requests as $key => $value) {
> +				$requestItem = new RestRequestItem();
> +				$requestItem->createRequestItemWithRequest($value, $token);
> +				$responses[$key] = $this->getResponseItem($requestItem);
> +			}
> +			return $responses;
> +		} else {
> +			throw new Exception("No post data set");
> +		}
> +	}
> +	
> +	private function handleBatchProxyRequest($token)
> +	{
> +		// Is this is a multipath/mixed post? Check content type:
> +		if (isset($GLOBALS['HTTP_RAW_POST_DATA']) &&  
> strpos($_SERVER['CONTENT_TYPE'], 'multipart/mixed') !== false &&  
> strpos($_SERVER['CONTENT_TYPE'],'boundary=') !== false) {
> +			// Ok looks swell, see what our boundry is..
> +			$boundry = substr($_SERVER['CONTENT_TYPE'],  
> strpos($_SERVER['CONTENT_TYPE'],'boundary=') + strlen('boundary='));
> +			// Split up requests per boundry
> +			$requests = explode($boundry, $GLOBALS['HTTP_RAW_POST_DATA']);
> +			$responses = array();
> +			foreach ($requests as $request) {
> +				$request = trim($request);
> +				if (!empty($request)) {
> +					// extractBatchRequest() does the magic parsing of the raw  
> post data to a meaninful request array
> +					$request = $this->extractBatchRequest($request);
> +					$requestItem = new RestRequestItem();
> +					$requestItem->createRequestItemWithRequest($request, $token);
> +					$responses[] = array('request' => $requestItem, 'response' =>  
> $this->getResponseItem($requestItem));
> +				}
> +			}
> +		} else {
> +			$this->outputError(new ResponseItem(BAD_REQUEST, "Invalid  
> multipart/mixed request"));
> +		}
> +		return $responses;
> 	}
>
> +	private function extractBatchRequest($request)
> +	{
> +		/* Multipart request is formatted like:
> +		 * -batch-a73hdj3dy3mm347ddjjdf
> +		 * Content-Type: application/http;version=1.1
> +		 * Content-Transfer-Encoding: binary
> +		 *
> +		 * GET /people/@me/@friends?startPage=5&count=10&format=json
> +		 * Host: api.example.org
> +		 * If-None-Match: "837dyfkdi39df"
> +		 *
> +		 * but we only want to have the last bit (the actual request),  
> this filters that down first
> +		 */
> +		$emptyFound = false;
> +		$requestLines = explode("\n", $request);
> +		$request = '';
> +		foreach ($requestLines as $line) {
> +			if ($emptyFound) {
> +				$request .= $line."\n";
> +			} elseif (empty($line)) {
> +				$emptyFound = true;
> +			}
> +		}
> +		// Now that we have the basic request in $request, split that up  
> again & parse it into a meaningful representation
> +		$firstFound = $emptyFound = false;
> +		$requestLines = explode("\n", $request);
> +		$request = array();
> +		$request['headers'] = array();
> +		$request['postData'] = '';
> +		foreach ($requestLines as $line) {
> +			if (!$firstFound) {
> +				$firstFound = true;
> +				$parts = explode(' ', trim($line));
> +				if (count($parts) != 2) {
> +					throw new Exception("Mallshaped request uri in multipart  
> block");
> +				}
> +				$request['method'] = strtoupper(trim($parts[0]));
> +				// cut it down to an actual meaningful url without the prefix/ 
> social/rest part right away
> +				$request['url'] = substr(trim($parts[1]),  
> strlen(Config::get('web_prefix') . '/social/rest'));
> +			} elseif (!$emptyFound && !empty($line)) {
> +				// convert the key to the PHP 'CONTENT_TYPE' style naming  
> convention.. it's ugly but consitent
> +				$key = str_replace('-', '_', strtoupper(trim(substr($line, 0,  
> strpos($line, ':')))));
> +				$val = trim(substr($line, strpos($line, ':') + 1));
> +				$request['headers'][$key] = $val;
> +			} elseif (!$emptyFound && empty($line)) {
> +				$emptyFound = true;
> +			} else {
> +				if (get_magic_quotes_gpc()) {
> +					$line = stripslashes($line);
> +				}
> +				$request['postData'] .= $line."\n";
> +			}
> +		}
> +		if (empty($request['postData'])) {
> +			// don't trip the requestItem into thinking there is postData  
> when there's not
> +			unset($request['postData']);
> +		} else {
> +			// if there was a post data blob present, decode it into an  
> array, the format is based on the
> +			// content type header, which is either application/json or
> +			$format = isset($request['headers']['CONTENT_TYPE']) &&  
> strtolower($request['headers']['CONTENT_TYPE']) == 'application/atom 
> +xml' ? 'atom' : 'json';
> +			$request['postData'] = $this- 
> >decodeRequests($request['postData'], $format);
> +		}
> +		return $request;
> +	}
> +	
> 	private function getResponseItem(RestRequestItem $requestItem)
> 	{
> 		$path = $this->getRouteFromParameter($requestItem->getUrl());
> @@ -140,12 +277,26 @@
> 			$class = new $class(null);
> 			$response = $class->handleMethod($requestItem);
> 		}
> -		if ($response->getError() != null && !$this->isBatchUrl()) {
> +		if ($response->getError() != null && !$this->isJsonBatchUrl() && ! 
> $this->isBatchProxyUrl()) {
> 			// Can't use http error codes in batch mode, instead we return  
> the error code in the response item
> 			$this->outputError($response);
> 		}
> 		return $response;
> 	}
> +	
> +	private function decodeRequests($requestParam, $format = 'json')
> +	{
> +		// temp hack until i know what the intended way to detect format is
> +		if ($format == 'json') {
> +			return json_decode($requestParam, true);
> +		} elseif ($format == 'atom') {
> +			$xml = simplexml_load_string($requestParam);
> +			print_r($xml);
> +			return $xml;
> +		} else {
> +			throw Exception("Invalid or unsupported input format");
> +		}
> +	}
>
> 	private function getRequestParams()
> 	{
> @@ -154,44 +305,14 @@
> 		if (get_magic_quotes_gpc()) {
> 			$requestParam = stripslashes($requestParam);
> 		}
> -		$requests = json_decode($requestParam);
> -		if ($requests == (isset($GLOBALS['HTTP_RAW_POST_DATA']) ?  
> $GLOBALS['HTTP_RAW_POST_DATA'] : $post)) {
> -			return new ResponseItem(BAD_REQUEST, "Malformed json string");
> -		}
> -		return $requests;
> -	}
> -
> -	private function handleBatchRequest($token)
> -	{
> -		// we support both a raw http post (without application/x-www- 
> form-urlencoded headers) like java does
> -		// and a more php / curl safe version of a form post with  
> 'request' as the post field that holds the request json data
> -		if (isset($GLOBALS['HTTP_RAW_POST_DATA']) ||  
> isset($_POST['request'])) {
> -			$requests = $this->getRequestParams();
> -			$responses = array();
> -			foreach ($requests as $key => $value) {
> -				$requestItem = new RestRequestItem();
> -				$requestItem->createRequestItemWithRequest($value, $token);
> -				$responses[$key] = $this->getResponseItem($requestItem);
> -			}
> -			return $responses;
> -		} else {
> -			throw new Exception("No post data set");
> -		}
> -	}
> -
> -	public function doGet()
> -	{
> -		$this->doPost('GET');
> -	}
> -
> -	public function doPut()
> -	{
> -		$this->doPost('PUT');
> +		return $this->decodeRequests($requestParam);
> 	}
>
> -	public function doDelete()
> +	private function getRouteFromParameter($pathInfo)
> 	{
> -		$this->doPost('DELETE');
> +		$pathInfo = substr($pathInfo, 1);
> +		$indexOfNextPathSeparator = strpos($pathInfo, "/");
> +		return $indexOfNextPathSeparator != - 1 ? substr($pathInfo, 0,  
> $indexOfNextPathSeparator) : $pathInfo;
> 	}
>
> 	private function outputError(ResponseItem $response)
> @@ -217,7 +338,6 @@
> 			default:
> 				$code = '500 Internal Server Error';
> 				break;
> -		
> 		}
> 		header("HTTP/1.0 $code", true);
> 		echo "$code - $errorMessage";
> @@ -265,8 +385,13 @@
> 		return substr($_SERVER["REQUEST_URI"],  
> strlen(Config::get('web_prefix') . '/social/rest'));
> 	}
>
> -	public function isBatchUrl()
> +	public function isJsonBatchUrl()
> 	{
> 		return strrpos($_SERVER["REQUEST_URI"], RestServlet:: 
> $JSON_BATCH_ROUTE) > 0;
> 	}
> +
> +	public function isBatchProxyUrl()
> +	{
> +		return strrpos($_SERVER["REQUEST_URI"], RestServlet:: 
> $BATCH_PROXY_ROUTE) > 0;
> +	}
> }
>
> Modified: incubator/shindig/trunk/php/src/social-api/samplecontainer/ 
> BasicAppDataService.php
> URL: http://svn.apache.org/viewvc/incubator/shindig/trunk/php/src/social-api/samplecontainer/BasicAppDataService.php?rev=678068&r1=678067&r2=678068&view=diff
> = 
> = 
> = 
> = 
> = 
> = 
> = 
> = 
> ======================================================================
> --- incubator/shindig/trunk/php/src/social-api/samplecontainer/ 
> BasicAppDataService.php (original)
> +++ incubator/shindig/trunk/php/src/social-api/samplecontainer/ 
> BasicAppDataService.php Fri Jul 18 15:47:41 2008
> @@ -85,7 +85,7 @@
> 		switch($groupId->getType()) {
> 			case 'self':
> 				foreach ($fields as $key) {
> -					$value = isset($values->$key) ? $values->$key :  
> (@isset($values[$key]) ? @$values[$key] : null);
> +					$value = isset($values[$key]) ? @$values[$key] : null;
> 					XmlStateFileFetcher::get()->setAppData($userId- 
> >getUserId($token), $key, $value);	
> 				}
> 				break;
>
> Modified: incubator/shindig/trunk/php/src/social-api/samplecontainer/ 
> BasicPeopleService.php
> URL: http://svn.apache.org/viewvc/incubator/shindig/trunk/php/src/social-api/samplecontainer/BasicPeopleService.php?rev=678068&r1=678067&r2=678068&view=diff
> = 
> = 
> = 
> = 
> = 
> = 
> = 
> = 
> ======================================================================
> --- incubator/shindig/trunk/php/src/social-api/samplecontainer/ 
> BasicPeopleService.php (original)
> +++ incubator/shindig/trunk/php/src/social-api/samplecontainer/ 
> BasicPeopleService.php Fri Jul 18 15:47:41 2008
> @@ -76,7 +76,7 @@
> 				if ($id == $token->getOwnerId()) {
> 					$person->setIsOwner(true);
> 				}
> -				if (is_array($profileDetails) && count($profileDetails)) {
> +				if (is_array($profileDetails) && count($profileDetails) && ! 
> in_array('all', $profileDetails)) {
> 					$newPerson = array();
> 					$newPerson['isOwner'] = $person->isOwner;
> 					$newPerson['isViewer'] = $person->isViewer;
>