/* Copyright (c) 2008 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
// All Rights Reserved.
package sample.maps;
import com.google.gdata.util.common.base.StringUtil;
import com.google.gdata.util.common.xml.XmlWriter;
import com.google.gdata.client.GoogleService;
import com.google.gdata.client.maps.MapsService;
import com.google.gdata.data.BaseEntry;
import com.google.gdata.data.BaseFeed;
import com.google.gdata.data.ExtensionProfile;
import com.google.gdata.data.PlainTextConstruct;
import com.google.gdata.data.batch.BatchOperationType;
import com.google.gdata.data.batch.BatchUtils;
import com.google.gdata.data.extensions.CustomProperty;
import com.google.gdata.data.extensions.ResourceId;
import com.google.gdata.data.maps.FeatureEntry;
import com.google.gdata.data.maps.FeatureFeed;
import com.google.gdata.data.maps.MapEntry;
import com.google.gdata.data.maps.MapFeed;
import java.io.ByteArrayInputStream;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.io.Writer;
import java.net.URL;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
/**
* Sample client for Maps GData API.
*
* maps VERB FEED FLAG*
* VERB is one of {query, create, read, update, delete, batch, clear}
* FEED is one of {maps, features}
* FLAG (note the non-gnu style) can be
* -chunk CHUNK-SIZE
* -content KML-CONTENT
* -count CHUNK-COUNT
* -fid FEATURE-ID
* -file FILENAME
* -host HOST:PORT
* -id ATOM-ID
* -maxresults MAX-RESULTS
* -mid MAP-ID
* -output OUTPUT-FILENAME
* -password PASSWORD
* -previd PREV_ID
* -preview
* -projection (one of full, public, unlisted, owned, bookmarked)
* -prop NAME VALUE
* -sep SEPARATOR (default ".")
* -startindex START-INDEX
* -title TITLE
* -uid USER-ID (default "default")
* -user USER-EMAIL
* -v VERSION
*/
public class Maps {
private static final HashSet XML_PP = new HashSet(Arrays.asList(
XmlWriter.WriterFlags.PRETTY_PRINT));
private static boolean draft = false;
protected Maps() {
}
/**
* Generic client for Maps API.
*/
public static class ApiClient<E extends BaseEntry, F extends BaseFeed> {
private Class<E> entryClass;
private Class<F> feedClass;
private MapsService maps;
private String base; // URI of base feed
private String uriFormat;
private List<String> params;
/**
* @param entryClass must be supplied, used to direct Atom parser
* @param feedClass must be supplied, used to direct Atom parser
*/
private ApiClient(Class<E> entryClass, Class<F> feedClass, String uriFormat,
List<String> params) {
this.entryClass = entryClass;
this.feedClass = feedClass;
this.uriFormat = uriFormat;
this.params = params;
}
private MapsService authenticate(String base, String u, String p)
throws Exception {
this.base = base;
maps = new MapsService("client");
if (!StringUtil.isEmpty(u) && !StringUtil.isEmpty(p)) {
maps.setUserCredentials(u, p);
}
return maps;
}
private F createFeed(F feed, String... id) throws Exception {
return maps.batch(makeUri(id), feed);
}
private F readFeed(String... id) throws Exception {
return maps.getFeed(makeUri(id), feedClass);
}
private E createEntry(E entry, String... id) throws Exception {
return maps.insert(makeUri(id), entry);
}
private E readEntry(String... id) throws Exception {
return maps.getEntry(makeUri(id), entryClass);
}
private E updateEntry(E entry, String... id) throws Exception {
return maps.update(makeUri(id), entry);
}
/**
* Attempt to delete an entry.
*
* @param id list of id parts
* @return null on success, an error message on failure
*/
private String deleteEntry(String... id) {
try {
maps.delete(makeUri(id));
return null;
} catch (Throwable t) {
return t.getMessage();
}
}
/**
* Construct a URI from a list of id components.
*/
private URL makeUri(String... id) throws Exception {
String uri = base + String.format(uriFormat, id);
boolean first = true;
for (String param : params) {
uri += (first ? "?" : "&") + param;
first = false;
}
System.err.println(uri);
return new URL(uri);
}
/**
* Hack helper to allow callers to provide one less param on query.
*/
private void trimFormat(String tail) {
if (uriFormat.endsWith(tail)) {
uriFormat = uriFormat.substring(0, uriFormat.lastIndexOf(tail));
} else {
System.err.println(uriFormat + " has no " + tail);
}
}
private void addFormat(String tail) {
uriFormat += tail;
}
private E parseAtom(InputStream is) throws Exception {
E entry = entryClass.newInstance();
entry.parseAtom(maps.getExtensionProfile(), is);
return entry;
}
private F parseFeed(InputStream is) throws Exception {
F feed = feedClass.newInstance();
feed.parseAtom(maps.getExtensionProfile(), is);
return feed;
}
public E buildEntry(String title, String content, String summary)
throws Exception {
E entry = entryClass.newInstance();
entry.setTitle(new PlainTextConstruct(title));
if (!StringUtil.isEmpty(content)) {
entry.setContent(new PlainTextConstruct(content));
}
if (!StringUtil.isEmpty(summary)) {
entry.setSummary(new PlainTextConstruct(summary));
}
if (draft) {
entry.setDraft(true);
}
return entry;
}
public E newEntry() throws Exception {
return entryClass.newInstance();
}
public F newFeed() throws Exception {
return feedClass.newInstance();
}
}
/** Pretty-print a feed. */
private static void pp(GoogleService service, BaseFeed feed, Writer w)
throws IOException {
XmlWriter xw = new XmlWriter(w, XML_PP, null);
ExtensionProfile extProfile = service.getExtensionProfile();
feed.generateAtom(xw, extProfile);
xw.flush();
w.write("\n");
w.flush();
}
/** Pretty-print an entry. */
private static void pp(GoogleService service, BaseEntry entry, Writer w)
throws IOException {
XmlWriter xw = new XmlWriter(w, XML_PP, null);
ExtensionProfile extProfile = service.getExtensionProfile();
entry.generateAtom(xw, extProfile);
xw.flush();
w.write("\n");
w.flush();
}
/**
* Create an Entry from an Atom input stream.
*
* @param actor
* @param is
* @param maps
* @param preview if true, print parsed entry to pw
* @param pw
* @return
* @throws Exception
*/
private static BaseEntry parseEntry(ApiClient actor, InputStream is,
MapsService maps, boolean preview, PrintWriter pw) throws Exception {
BaseEntry entry;
entry = actor.parseAtom(is);
if (draft) {
entry.setDraft(true);
}
if (preview) {
pp(maps, entry, pw);
}
return entry;
}
/**
* Create a Feed from an Atom input stream.
*
* @param actor
* @param is
* @param maps
* @param preview if true, print parsed entry to pw
* @param pw
* @return
* @throws Exception
*/
private static BaseFeed parseFeed(ApiClient actor, InputStream is,
MapsService maps, boolean preview, PrintWriter pw) throws Exception {
BaseFeed feed;
feed = actor.parseFeed(is);
if (preview) {
pp(maps, feed, pw);
}
return feed;
}
enum Action {query, create, read, update, delete, batch, clear, bulk, rawpost, revise};
enum Feed {maps, features};
enum Arg {
CHUNK, // bulk chunk size
CONTENT, // kml content
COUNT, // bulk chunk count
DRAFT, // true if a private map
FID, // feature id
FILE, // file name for uploads
FUDGE, // mess with stuff
HOST, // name of API server
ID, // atom id
MAXRESULTS, // limit number of results (query)
MID, // map id
OUTPUT, // output filename
PASSWORD, // for authentication
PREVID, // previd (previous id)
PREVIEW, // show upload data before sending
PROJECTION, // map feed projection
PROP, // custom property
SEP, // uid/mid separator (default ".")
STARTINDEX, // starting index (query)
SUMMARY, // atom:summary
TITLE, // of a new map or feature
UID, // user id ("default" by default)
USER, // for authentication
V // version
};
public static void main(String arg[]) throws Exception {
Action action = enumValueOrDie(Action.class, arg[0]);
Feed feed = enumValueOrDie(Feed.class, arg[1]);
String content = " "; // avoid creating ghost maps
String fid = "";
String host = "maps.google.com";
String id = ""; // client-supplied id
String mid = "";
String password = "";
String previd = null;
String projection = "full";
String sep = "/";
String summary = null;
String title = null;
String uid = "default";
String user = "";
boolean fudge = false;
boolean preview = false;
int chunk = 10;
int count = 10;
int maxResults = 0;
int startIndex = 0;
int version = 2;
InputStream is = System.in;
OutputStream os = System.out;
Map<String, String> props = new HashMap<String, String>();
// Parse command line arguments
for (int i = 2; i < arg.length;) {
String key = arg[i++].substring(1).toUpperCase();
switch (enumValueOrDie(Arg.class, key)) {
case CHUNK:
chunk = Integer.parseInt(arg[i++]);
break;
case CONTENT:
content = arg[i++];
break;
case COUNT:
count = Integer.parseInt(arg[i++]);
break;
case DRAFT:
draft = true;
break;
case FID:
fid = arg[i++];
break;
case FILE:
is = new FileInputStream(arg[i++]);
break;
case FUDGE:
fudge = true;
break;
case HOST:
host = arg[i++];
break;
case ID:
id = arg[i++];
break;
case MID:
mid = arg[i++];
break;
case MAXRESULTS:
maxResults = Integer.parseInt(arg[i++]);
break;
case OUTPUT:
os = new FileOutputStream(arg[i++]);
break;
case PASSWORD:
password = arg[i++];
break;
case PREVID:
previd = arg[i++];
break;
case PROJECTION:
projection = arg[i++];
break;
case PREVIEW:
preview = !preview;
break;
case PROP:
props.put(arg[i], arg[i+1]);
i += 2;
break;
case SEP:
sep = arg[i++];
break;
case STARTINDEX:
startIndex = Integer.parseInt(arg[i++]);
break;
case SUMMARY:
summary = arg[i++];
break;
case TITLE:
title = arg[i++];
break;
case UID:
uid = arg[i++];
break;
case USER:
user = arg[i++];
break;
case V:
version = Integer.parseInt(arg[i++]);
break;
}
}
// Set up API client.
PrintWriter pw = new PrintWriter(os);
String base = "http://" + host + "/maps/feeds/" + feed.toString() + "/";
List<String> params = new ArrayList<String>();
if (startIndex > 0) {
params.add("start-index=" + startIndex);
}
if (maxResults > 0) {
params.add("max-results=" + maxResults);
}
if (null != previd) {
params.add("previd=" + previd);
}
ApiClient client = Feed.maps == feed
? new ApiClient(MapEntry.class, MapFeed.class,
"%s/" + projection + "/%s", params)
: new ApiClient(FeatureEntry.class, FeatureFeed.class,
"%s" + sep + "%s/" + projection + "/%s", params);
MapsService maps = client.authenticate(base, user, password);
if (version < 2) {
maps.setProtocolVersion(MapsService.Versions.V1);
} else if (version == 2){
maps.setProtocolVersion(MapsService.Versions.V2);
}
// Perform action.
BaseEntry entry;
switch (action) {
// For now, just get the whole feed.
case query:
pp(maps, client.readFeed(uid, mid, fid), pw);
break;
// Create an entry from scratch or read from input.
case create:
client.trimFormat("/%s"); // sleazy hack to remove trailing id
entry = title == null
? parseEntry(client, is, maps, preview, pw)
: client.buildEntry(title, content, summary);
if (id.length() > 0) {
entry.setExtension(new ResourceId(id));
}
setProperties(props, entry);
if (preview) {
pp(maps, entry, pw);
}
pp(maps, client.createEntry(entry, uid, mid), pw);
break;
// Read an individual entry.
case read:
pp(maps, client.readEntry(uid, mid, fid), pw);
break;
// Update an existing entry.
case update:
entry = title == null
? parseEntry(client, is, maps, preview, pw)
: client.buildEntry(title, content, summary);
setProperties(props, entry);
if (preview) {
pp(maps, entry, pw);
}
pp(maps, client.updateEntry(entry, uid, mid, fid), pw);
break;
// Delete an existing entry.
case delete:
String err = client.deleteEntry(uid, mid, fid);
if (null != err) {
System.err.println("delete error: " + err);
}
break;
case batch:
client.trimFormat("/%s"); // sleazy hack to remove trailing id
client.addFormat("/batch");
BaseFeed bf = parseFeed(client, is, maps, preview, pw);
pp(maps, client.createFeed(bf, uid, mid), pw);
break;
// Clear out a feed.
case clear:
client.trimFormat("/%s"); // sleazy hack to remove trailing id
bf = client.readFeed(uid, mid, fid);
client.addFormat("/batch");
BaseFeed batch = client.newFeed();
count = 0;
for (Object o : bf.getEntries()) {
String entryId = ((BaseEntry) o).getSelfLink().getHref();
if (count == 0 && fudge) {
entryId += "-xx";
}
BaseEntry delete = client.newEntry();
delete.setId(replaceUserId(uid, entryId));
BatchUtils.setBatchOperationType(delete, BatchOperationType.DELETE);
BatchUtils.setBatchId(delete, Integer.toString(++count));
batch.getEntries().add(delete);
}
if (preview) {
pp(maps, batch, pw);
}
BaseFeed response = maps.batch(client.makeUri(uid, mid, fid), batch);
pp(maps, response, pw);
break;
// Test bulk insert latency.
case bulk:
client.trimFormat("/%s"); // sleazy hack to remove trailing id
String baseXml = "";
if (title == null) {
char buf[] = new char[1024 * 64];
int n = new InputStreamReader(is).read(buf);
baseXml = new String(buf, 0, n);
}
client.addFormat("/batch");
for (int i = 0; i < count; i++) {
batch = client.newFeed();
long t0 = System.currentTimeMillis();
for (int j = 0; j < chunk; j++) {
String entryId = "" + i + "-" + j;
String xml = baseXml.replace("COUNT", entryId);
System.err.println(xml);
entry = title == null
? parseEntry(client, new ByteArrayInputStream(xml.getBytes()),
maps, preview, pw)
: client.buildEntry(title, content, summary);
setProperties(props, entry);
entry.setId(entryId);
BatchUtils.setBatchId(entry, entryId);
BatchUtils.setBatchOperationType(entry, BatchOperationType.INSERT);
batch.getEntries().add(entry);
}
if (preview) {
pp(maps, batch, pw);
}
response = maps.batch(
client.makeUri(uid, mid, fid), batch);
pp(maps, response, pw);
System.err.println("chunk insert time: " +
(System.currentTimeMillis() - t0) + "ms");
}
break;
// Post a file as is, no interpretation.
case rawpost:
break;
// Update a bloc of entries.
case revise:
client.trimFormat("/%s"); // sleazy hack to remove trailing id
baseXml = "";
if (title == null) {
char buf[] = new char[1024 * 64];
int n = new InputStreamReader(is).read(buf);
baseXml = new String(buf, 0, n);
}
bf = client.readFeed(uid, mid, fid);
int n = bf.getEntries().size();
client.addFormat("/batch");
int dex = 0;
count = Math.min(count, n / chunk);
for (int i = 0; i < count; i++) {
long t0 = System.currentTimeMillis();
batch = client.newFeed();
for (int j = 0; j < Math.min(n, chunk); j++) {
Object o = bf.getEntries().get(j + i*chunk);
String entryId = ((BaseEntry) o).getSelfLink().
getHref().replaceAll("\\.", sep);
if (dex == 0 && fudge) {
entryId += "-xx";
}
String xml = baseXml.replace("COUNT", entryId.substring(entryId.lastIndexOf('/')));
BaseEntry update = title == null
? parseEntry(client, new ByteArrayInputStream(xml.getBytes()),
maps, preview, pw)
: client.buildEntry(title, content, summary);
update.setId(replaceUserId(uid, entryId));
setProperties(props, update);
BatchUtils.setBatchOperationType(update, BatchOperationType.UPDATE);
BatchUtils.setBatchId(update, Integer.toString(++dex));
batch.getEntries().add(update);
}
n -= chunk;
if (preview) {
pp(maps, batch, pw);
}
response = maps.batch(
client.makeUri(uid, mid, fid), batch);
pp(maps, response, pw);
System.err.println("chunk update time: " +
(System.currentTimeMillis() - t0) + "ms");
}
break;
}
}
/**
* Returns the enum value of a string, or exits after printing
* the list of known values.
*
* @param ec enum class
* @param key string to turn into an enum
* @return the right enum
*/
private static <T extends Enum<T>> T enumValueOrDie(Class<T> ec, String key) {
try {
return T.valueOf(ec, key);
} catch (IllegalArgumentException e) {
System.err.println("unknown " + ec.getSimpleName() + ": " + key);
System.err.println("allowed: {" +
StringUtil.join(ec.getEnumConstants(), ", ").toLowerCase() + "}");
System.exit(1);
return null;
}
}
/**
* Attempts to replace the user component in an entry id with the given uid.
*/
private static String replaceUserId(String uid, String s) {
String[] parts = s.split("/");
for (int i = 0; i < parts.length; i++) {
if (parts[i].matches("[0-9]+")) {
parts[i] = uid;
break;
}
}
return StringUtil.join(parts, "/");
}
private static void setProperties(Map<String, String> props,
BaseEntry entry) {
List<CustomProperty> cust = (entry instanceof MapEntry)
? ((MapEntry) entry).getCustomProperties()
: ((FeatureEntry) entry).getCustomProperties();
cust.clear();
for (String key : props.keySet()) {
cust.add(new CustomProperty(key, null, null, props.get(key)));
}
}
}