677 lines
20 KiB
Java
677 lines
20 KiB
Java
/* 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)));
|
|
}
|
|
}
|
|
}
|