Ejemplo de Java: envío de aplicación con opciones de juego y tráileres
En este artículo se proporcionan ejemplos de código java que muestran cómo usar la API de envío de Microsoft Store para estas tareas:
- Obtenga un token de acceso de Azure AD para usarlo con la API de envío de Microsoft Store.
- Crear un envío de aplicación
- Configure los datos de la descripción de la Store para el envío de aplicación, incluidas las opciones de descripciones avanzadas de juegos y avances.
- Cargue el archivo ZIP que contiene los paquetes, las imágenes de descripciones y los archivos de avances para el envío de aplicación.
- Confirme el envío de aplicación.
Crear un envío de aplicación
La CreateAndSubmitSubmissionExample
clase implementa un main
programa que llama a otros métodos de ejemplo para usar la API de envío de Microsoft Store para crear y confirmar un envío de aplicación que contiene opciones de juego y un finalizador. Para adaptar este código a su propio uso:
- Asigne la variable
tenantId
al id. de inquilino para la aplicación y asigne las variablesclientId
yclientSecret
al identificador de cliente y clave para la aplicación. Para obtener más información, consulte Asociación de una aplicación de Azure AD a la cuenta del Centro de partners - Asigne la variable
applicationId
al Id. de la Store de la aplicación para la cual quiere crear un envío.
import java.io.IOException;
import java.text.MessageFormat;
import javax.json.Json;
import javax.json.JsonArrayBuilder;
import javax.json.JsonObject;
import javax.json.JsonObjectBuilder;
import org.apache.http.client.ClientProtocolException;
public class CreateAndSubmitSubmissionExample {
public static void main(String[] args) {
// Add your tenant ID, client ID, and client secret here.
String tenantId = "";
String clientId = "";
String clientSecret = "";
DevCenterAccessTokenClient accessTokenClient = new DevCenterAccessTokenClient(tenantId, clientId, clientSecret);
String accessToken = accessTokenClient.getAccessToken("https://manage.devcenter.microsoft.com");
// The application ID is taken from your app dashboard page's URI in Dev Center,
// e.g. https://developer.microsoft.com/en-us/dashboard/apps/{application_id}/
String applicationId = "";
DevCenterClient devCenter = new DevCenterClient("https://manage.devcenter.microsoft.com", accessToken);
try {
// Get the application object, and cancel any in progress submissions.
JsonObject app = devCenter.getApplicationJsonObject(applicationId);
JsonObject inProgressSubmission = app.getJsonObject("pendingApplicationSubmission");
if (inProgressSubmission != null) {
String inProgressSubmissionId = inProgressSubmission.getString("id");
devCenter.cancelInProgressSubmission(applicationId, inProgressSubmissionId);
}
// Create a new submission, based on the last published submission.
JsonObject submission = devCenter.createSubmission(applicationId);
String submissionId = submission.getString("id");
// JsonObjects are immutable, so we'll build up our changes to the submission and
// then merge it with the submission object.
JsonObjectBuilder submissionChanges = Json.createObjectBuilder();
// The following fields are required:
submissionChanges.add("applicationCategory", "Games_Fighting");
submissionChanges.add("listings", getListingsObject());
submissionChanges.add("pricing", getPricingObject());
submissionChanges.add("packages", Json.createArrayBuilder().add(getPackageObject()));
submissionChanges.add("allowTargetFutureDeviceFamilies", getDeviceFamiliesObject());
// Add new Gaming Options to the submission.
JsonObject gamingOptions = getGamingOptionsJsonObject();
submissionChanges.add("gamingOptions", Json.createArrayBuilder().add(gamingOptions));
// Add new Trailers to the submission.
JsonObject trailer = getTrailerObject();
submissionChanges.add("trailers", Json.createArrayBuilder().add(trailer));
// Continue updating the submission_json object with additional options as needed.
// After you've finished, call the Update API with the code below to save it:
JsonObject submissionToUpdate = mergeJsonObjects(submission, submissionChanges.build());
JsonObject updatedSubmission = devCenter.updateSubmission(applicationId, submissionId, submissionToUpdate);
// All images and packages should be located in a single ZIP file. In the submission JSON,
// the file names for all objects requiring them (icons, packages, etc.) must exactly
// match the file names from the ZIP file.
String zipFilePath = "";
devCenter.uploadZipFileForSubmission(applicationId, submissionId, zipFilePath);
// Committing the submission will start the submission process for it. Once committed,
// the submission can no longer be changed.
devCenter.commitSubmission(applicationId, submissionId);
// After committing, you can poll the commit API for the status of the submission's process using
// the following code.
boolean waitingForCommitToStart = true;
while (waitingForCommitToStart) {
String status = devCenter.getSubmissionStatus(applicationId, submissionId);
System.out.println(MessageFormat.format("Submission status: {0}", status));
waitingForCommitToStart = status.equals("CommitStarted");
if (waitingForCommitToStart) {
try {
Thread.sleep(60000); // Wait one minute to check Dev Center again.
} catch (InterruptedException iex) {
System.out.println("The sleep was interrupted. Checking Dev Center now.");
}
}
}
} catch (ClientProtocolException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
private static JsonObject mergeJsonObjects(JsonObject baseObject, JsonObject mergeObject) {
JsonObjectBuilder builder = Json.createObjectBuilder();
for (String baseKey : baseObject.keySet()) {
if (mergeObject.containsKey(baseKey)) {
builder.add(baseKey, mergeObject.get(baseKey));
} else {
builder.add(baseKey, baseObject.get(baseKey));
}
}
for (String mergeKey : mergeObject.keySet()) {
if (!baseObject.containsKey(mergeKey)) {
builder.add(mergeKey, mergeObject.get(mergeKey));
}
}
return builder.build();
}
private static JsonObject getListingsObject() {
// This structure holds basic information to display in the store.
JsonObjectBuilder baseListing = Json.createObjectBuilder();
baseListing.add("copyrightAndTrademarkInfo", "(C) 2017 Microsoft");
baseListing.add("licenseTerms", "http://example.com/licenseTerms.aspx");
baseListing.add("privacyPolicy", "http://example.com/privacyPolicy.aspx");
baseListing.add("supportContact", "support@example.com");
baseListing.add("websiteUrl", "http://example.com");
baseListing.add("description", "A sample game showing off gameplay options code.");
baseListing.add("releaseNotes", "Initial release");
// The title of the app must match a reserved name for the app in Dev Center.
// If it doesn't, attempting to update the submission will fail.
baseListing.add("title", "Super Dev Center API Simulator 2017");
// Up to 7 keywords may be provided in a listing.
JsonArrayBuilder keywords = Json.createArrayBuilder();
keywords.add("SampleApp").add("SampleFightingGame").add("GameOptions");
baseListing.add("keywords", keywords);
JsonArrayBuilder features = Json.createArrayBuilder();
features.add("Doesn't crash");
features.add("Likes to eat chips");
baseListing.add("features", features);
// If your app works better with specific hardware (or needs it), you can
// add or update values here.
JsonArrayBuilder hardwarePreferences = Json.createArrayBuilder();
hardwarePreferences.add("Keyboard");
hardwarePreferences.add("Mouse");
baseListing.add("hardwarePreferences", hardwarePreferences);
JsonArrayBuilder images = Json.createArrayBuilder();
// There are several types of images available; at least one screenshot
// is required.
JsonObjectBuilder image = Json.createObjectBuilder();
image.add("fileName", "tile.png");
image.add("description", "The tile as it appears in the store.");
image.add("imageType", "Icon");
images.add(image);
baseListing.add("images", images);
JsonObjectBuilder listing = Json.createObjectBuilder();
listing.add("baseListing", baseListing);
// If there are any specific overrides to above information for Windows 8,
// Windows 8.1, Windows Phone 7.1, 8.0, or 8.1, you can add information here.
listing.add("platformOverrides", Json.createObjectBuilder());
JsonObjectBuilder listings = Json.createObjectBuilder();
// Each listing is targeted at a specific language-locale code, e.g. EN-US.
listings.add("en-us", listing);
return listings.build();
}
private static JsonObject getPackageObject() {
JsonObjectBuilder pkg = Json.createObjectBuilder();
// The file name is relative to the root of the uploaded ZIP file.
pkg.add("fileName", "bin/super_dev_ctr_api_sim.appxupload");
// If you haven't begun to upload the file yet, set this value to "PendingUpload".
pkg.add("fileStatus", "PendingUpload");
return pkg.build();
}
private static JsonObject getPricingObject() {
JsonObjectBuilder pricing = Json.createObjectBuilder();
// How long the trial period is, if one is allowed. Valid values are NoFreeTrial,
// OneDay, SevenDays, FifteenDays, ThirtyDays, or TrialNeverExpires.
pricing.add("trialPeriod", "NoFreeTrial");
// Maps to the default price for the app.
pricing.add("priceId", "Free");
// If you'd like to offer your app in different markets at different prices, you
// can provide priceId values per language/locale code.
pricing.add("marketSpecificPricing", Json.createObjectBuilder().build());
return pricing.build();
}
private static JsonObject getDeviceFamiliesObject() {
JsonObjectBuilder futureDeviceFamilies = Json.createObjectBuilder();
// Supported values are Desktop, Mobile, Xbox, and Holographic. To make
// the app available on that specific platform, set the value to True.
futureDeviceFamilies.add("Desktop", true);
futureDeviceFamilies.add("Mobile", false);
futureDeviceFamilies.add("Xbox", true);
futureDeviceFamilies.add("Holographic", false);
return futureDeviceFamilies.build();
}
private static JsonObject getGamingOptionsJsonObject() {
JsonObjectBuilder gamingOptions = Json.createObjectBuilder();
// The genres of your game.
JsonArrayBuilder genres = Json.createArrayBuilder();
genres.add("Games_Fighting");
gamingOptions.add("genres", genres);
// Set this to true if your game supports local multiplayer. This field
// is required.
gamingOptions.add("isLocalMultiplayer", true);
// If local multiplayer is supported, you must provide the minimum and
// maximum players supported. Valid values are between 2 and 1000 inclusive.
gamingOptions.add("localMultiplayerMinPlayers", 2);
gamingOptions.add("localMultiplayerMaxPlayers", 4);
// Set this to True if your game supports local co-op play. This field is required.
gamingOptions.add("isLocalCooperative", true);
// If local co-op is supported, you must provide the minimum and maximum players
// supported. Valid values are between 2 and 1000 inclusive.
gamingOptions.add("localCooperativeMinPlayers", 2);
gamingOptions.add("localCooperativeMaxPlayers", 4);
// Set this to True if your game supports online multiplayer. This field is required.
gamingOptions.add("isOnlineMultiplayer", true);
// If online multiplayer is supported, you must provide the minimum and maximum players
// supported. Valid values are between 2 and 1000 inclusive.
gamingOptions.add("onlineMultiplayerMinPlayers", 2);
gamingOptions.add("onlineMultiplayerMaxPlayers", 4);
// Set this to true if your game supports online co-op play. This field is required.
gamingOptions.add("isOnlineCooperative", true);
// If online co-op is supported, you must provide the minimum and maximum players
// supported. Valid values are between 2 and 1000 inclusive.
gamingOptions.add("onlineCooperativeMinPlayers", 2);
gamingOptions.add("onlineCooperativeMaxPlayers", 4);
// If your game supports broadcasting a stream to other players, set this field to True.
// This field is required.
gamingOptions.add("isBroadcastingPrivilegeGranted", true);
// If your game supports cross-device play (e.g. a player can play on an Xbox One with
// their friend who's playing on a PC), set this field to True. This field is required.
gamingOptions.add("isCrossPlayEnabled", true);
// If your game supports Kinect usage, set this field to "Enabled", otherwise, set it to
// "Disabled". This field is required.
gamingOptions.add("kinectDataForExternal", "Disabled");
// Free text about any other peripherals that your game supports. This field is optional.
gamingOptions.add("otherPeripherals", "Supports the usage of all fighting joysticks.");
return gamingOptions.build();
}
private static JsonObject getTrailerObject() {
JsonObjectBuilder trailer = Json.createObjectBuilder();
// This is the filename of the trailer. The file name is a relative path to the
// root of the ZIP file to be uploaded to the API.
trailer.add("VideoFileName", "trailers/main/my_awesome_trailer.mpeg");
// Aside from the video itself, a trailer can have metadata assets including a title and images
// such as screenshots or alternate images. These are keyed by market code (see the end of this
// method for an example.
JsonObjectBuilder trailerAssetsByCountry = Json.createObjectBuilder();
JsonObjectBuilder trailerAssetSet = Json.createObjectBuilder();
// The title of the trailer to display in the store.
trailerAssetSet.add("Title", "Main Trailer");
// The list of images provided with the trailer that are shown when the trailer isn't playing.
JsonArrayBuilder trailerImageAssets = Json.createArrayBuilder();
JsonObjectBuilder mainTrailerImage = Json.createObjectBuilder();
// The file name of the image. The file name is a relative path to the root of the ZIP
// file to be uploaded to the API.
mainTrailerImage.add("FileName", "trailers/main/thumbnail.png");
// A plaintext description of what the image represents.
mainTrailerImage.add("Description", "The thumbnail for the trailer shown before the user clicks play");
trailerImageAssets.add(mainTrailerImage);
// Add a second image.
JsonObjectBuilder altImage = Json.createObjectBuilder();
altImage.add("FileName", "trailers/main/alt-img.png");
altImage.add("Description", "The image to show after the trailer plays");
trailerImageAssets.add(altImage);
trailerAssetSet.add("ImageList", trailerImageAssets);
// This line creates the trailer asset for en-us.
trailerAssetsByCountry.add("en-us", trailerAssetSet);
trailer.add("TrailerAssets", trailerAssetsByCountry);
return trailer.build();
}
}
Obtención de un token de acceso de Azure AD
La clase DevCenterAccessTokenClient
define un método auxiliar que usa los valores tenantId
, clientId
y clientSecret
para crear un token de acceso de Azure AD para usarlo con la API de envío de Microsoft Store.
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.text.MessageFormat;
import javax.json.Json;
import javax.json.JsonReader;
import org.apache.http.client.ClientProtocolException;
import org.apache.http.client.ResponseHandler;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.BasicResponseHandler;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
/**
* A client for getting access tokens to the Dev Center API.
* @author Microsoft
*/
public final class DevCenterAccessTokenClient {
private String tenantId;
private String clientId;
private String clientSecret;
/**
* Creates a new access token client for Dev Center.
* @param tenantId Your tenant ID for the app.
* @param clientId Your client ID for the app.
* @param clientSecret Your client secret string for the app.
*/
public DevCenterAccessTokenClient(String tenantId, String clientId, String clientSecret) {
this.tenantId = tenantId;
this.clientId = clientId;
this.clientSecret = clientSecret;
}
/**
* Gets an access token for the specific resource.
* @param resource The full URI to the resource to be accessed.
* @return An access token for that resource, good for one hour.
*/
public String getAccessToken(String resource) {
// Generate access token. Access token is valid for 1 hour.
String tokenEndpoint = "https://login.microsoftonline.com/{0}/oauth2/token";
HttpPost tokenRequest = new HttpPost(MessageFormat.format(tokenEndpoint, this.tenantId));
String tokenRequestBody = MessageFormat.format("grant_type=client_credentials&client_id={0}&client_secret={1}&resource={2}", this.clientId, this.clientSecret, resource);
tokenRequest.setEntity(new StringEntity(tokenRequestBody, "utf-8"));
tokenRequest.addHeader("Content-Type", "application/x-www-form-urlencoded; charset=utf-8");
CloseableHttpClient httpclient = HttpClients.createDefault();
ResponseHandler<String> responseHandler = new BasicResponseHandler();
try {
String tokenResponse = httpclient.execute(tokenRequest, responseHandler);
JsonReader reader = Json.createReader(new ByteArrayInputStream(tokenResponse.getBytes("UTF-8")));
String accessToken = reader.readObject().getString("access_token");
return accessToken;
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
} catch (ClientProtocolException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
}
Métodos auxiliares para invocar la API de envío y cargar archivos de envío
La clase DevCenterClient
define métodos auxiliares que invocan una variedad de métodos en la API de envío de Microsoft Store y cargan el archivo ZIP que contiene los paquetes, las imágenes de descripciones y los archivos de tráileres para el envío de aplicación.
import java.io.File;
import java.io.IOException;
import java.io.StringWriter;
import java.text.MessageFormat;
import javax.json.Json;
import javax.json.JsonObject;
import javax.json.JsonReader;
import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.StatusLine;
import org.apache.http.client.ClientProtocolException;
import org.apache.http.client.HttpResponseException;
import org.apache.http.client.ResponseHandler;
import org.apache.http.client.methods.HttpDelete;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.methods.HttpPut;
import org.apache.http.client.methods.HttpUriRequest;
import org.apache.http.entity.FileEntity;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
/**
* A client for accessing commands in the Dev Center API.
* @author Microsoft
*/
public final class DevCenterClient implements ResponseHandler<JsonObject> {
private String baseUri;
private String accessToken;
/**
* Creates a new dev center client instance.
* @param baseUri The base URI for the Dev Center API.
* @param accessToken The access token for web call authentication.
*/
public DevCenterClient(String baseUri, String accessToken) {
this.baseUri = baseUri;
this.accessToken = accessToken;
}
/**
* Returns the application JSON object from the Dev Center API.
* @param applicationId The application ID.
* @return A JSON object from Dev Center.
* @throws IOException Thrown when a serialization exception occurs on the read.
* @throws ClientProtocolException Thrown when an HTTP communication error occurs.
*/
public JsonObject getApplicationJsonObject(String applicationId) throws ClientProtocolException, IOException {
String path = MessageFormat.format("/v1.0/my/applications/{0}", applicationId);
return get(path);
}
/**
* Cancels and deletes the in-progress submission from the application.
* @param applicationId The application ID.
* @param submissionId The submission ID.
* @return A JSON object from Dev Center.
* @throws IOException Thrown when a serialization exception occurs on the read.
* @throws ClientProtocolException Thrown when an HTTP communication error occurs.
*/
public JsonObject cancelInProgressSubmission(String applicationId, String submissionId) throws ClientProtocolException, IOException {
String path = MessageFormat.format("/v1.0/my/applications/{0}/submissions/{1}", applicationId, submissionId);
return delete(path);
}
/**
* Creates a new submission in Dev Center.
* @param applicationId The application ID.
* @return The submission JSON object from Dev Center.
* @throws IOException Thrown when a serialization exception occurs on the read.
* @throws ClientProtocolException Thrown when an HTTP communication error occurs.
*/
public JsonObject createSubmission(String applicationId) throws ClientProtocolException, IOException {
String path = MessageFormat.format("/v1.0/my/applications/{0}/submissions", applicationId);
return post(path);
}
/**
* Updates the submission in Dev Center.
* @param applicationId The application ID.
* @param submissionId The submission ID.
* @param submission The submission JSON object.
* @return The updated submission JSON object from Dev Center.
* @throws IOException Thrown when a serialization exception occurs on the read.
* @throws ClientProtocolException Thrown when an HTTP communication error occurs.
*/
public JsonObject updateSubmission(String applicationId, String submissionId, JsonObject submission) throws ClientProtocolException, IOException {
String path = MessageFormat.format("/v1.0/my/applications/{0}/submissions/{1}", applicationId, submissionId);
return put(path, submission);
}
/**
* Gets the submission in Dev Center.
* @param applicationId The application ID.
* @param submissionId The submission ID.
* @return The submission JSON object from Dev Center.
* @throws IOException Thrown when a serialization exception occurs on the read.
* @throws ClientProtocolException Thrown when an HTTP communication error occurs.
*/
public JsonObject getSubmission(String applicationId, String submissionId) throws ClientProtocolException, IOException {
String path = MessageFormat.format("/v1.0/my/applications/{0}/submissions/{1}", applicationId, submissionId);
return get(path);
}
/**
* Commits the submission to Dev Center.
* @param applicationId The application ID.
* @param submissionId The submission ID.
* @throws IOException Thrown when a serialization exception occurs on the read.
* @throws ClientProtocolException Thrown when an HTTP communication error occurs.
*/
public void commitSubmission(String applicationId, String submissionId) throws ClientProtocolException, IOException {
String path = MessageFormat.format("/v1.0/my/applications/{0}/submissions/{1}/commit", applicationId, submissionId);
post(path);
}
/**
* Returns the submission status of this submission.
* @param applicationId The application ID.
* @param submissionId The submission ID.
* @return The status of the submission in Dev Center.
* @throws IOException Thrown when a serialization exception occurs on the read.
* @throws ClientProtocolException Thrown when an HTTP communication error occurs.
*/
public String getSubmissionStatus(String applicationId, String submissionId) throws ClientProtocolException, IOException {
JsonObject response = getSubmission(applicationId, submissionId);
String status = response.getString("status");
if (status == null || status.isEmpty())
{
return "Unknown";
}
return status;
}
/**
* Uploads a ZIP archive containing the binaries, image assets, trailers, and other components to the submission.
* @param applicationId The application ID.
* @param submissionId The submission ID.
* @param zipFilePath The file path to the ZIP file.
* @throws IOException Thrown when a serialization exception occurs on the read.
* @throws ClientProtocolException Thrown when an HTTP communication error occurs.
*/
public void uploadZipFileForSubmission(String applicationId, String submissionId, String zipFilePath) throws ClientProtocolException, IOException {
JsonObject submission = getSubmission(applicationId, submissionId);
String fileUploadUrl = submission.getString("fileUploadUri");
CloseableHttpClient httpclient = HttpClients.createDefault();
File uploadFile = new File(zipFilePath);
HttpPut uploadFileRequest = new HttpPut(fileUploadUrl.replace("+", "%2B")); // Encode '+', otherwise it will be decoded as ' '
uploadFileRequest.addHeader("x-ms-blob-type", "BlockBlob");
uploadFileRequest.setEntity(new FileEntity(uploadFile));
httpclient.execute(uploadFileRequest);
}
private JsonObject get(String path) throws ClientProtocolException, IOException {
HttpGet request = new HttpGet(this.baseUri + path);
return invoke(request);
}
private JsonObject put(String path, JsonObject obj) throws ClientProtocolException, IOException {
HttpPut request = new HttpPut(this.baseUri + path);
request.addHeader("Content-Type", "application/json; charset=utf-8");
request.setEntity(new StringEntity(SerializeJsonObject(obj)));
return invoke(request);
}
private JsonObject post(String path) throws ClientProtocolException, IOException {
return post(path, null);
}
private JsonObject post(String path, JsonObject obj) throws ClientProtocolException, IOException {
HttpPost request = new HttpPost(this.baseUri + path);
request.addHeader("Content-Type", "application/json; charset=utf-8");
if (obj != null)
{
request.setEntity(new StringEntity(SerializeJsonObject(obj)));
}
return invoke(request);
}
private JsonObject delete(String path) throws ClientProtocolException, IOException {
HttpDelete request = new HttpDelete(this.baseUri + path);
return invoke(request);
}
private JsonObject invoke(HttpUriRequest request) throws ClientProtocolException, IOException {
CloseableHttpClient client = HttpClients.createDefault();
request.addHeader("Authorization", "Bearer " + accessToken);
request.addHeader("User-Agent", "Java");
JsonObject response = client.execute(request, this);
return response;
}
public JsonObject handleResponse(HttpResponse response) throws ClientProtocolException, IOException {
StatusLine status = response.getStatusLine();
int statusCode = status.getStatusCode();
String reasonPhrase = status.getReasonPhrase();
HttpEntity entity = response.getEntity();
JsonObject returnValue = null;
if(entity != null && entity.getContentLength() != 0) {
JsonReader reader = Json.createReader(entity.getContent());
returnValue = reader.readObject();
}
if (statusCode < 200 || statusCode > 299) {
if (returnValue != null) {
throw new HttpResponseException(statusCode, returnValue.toString());
}
throw new HttpResponseException(statusCode, reasonPhrase);
}
return returnValue;
}
private static String SerializeJsonObject(JsonObject obj) {
StringWriter writer = new StringWriter();
Json.createWriter(writer).writeObject(obj);
String body = writer.toString();
return body;
}
}