/*
* Copyright 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.
*/
package com.google.feedserver.tools;
import com.google.feedserver.client.TypelessFeedServerClient;
import com.google.feedserver.util.CommonsCliHelper;
import com.google.feedserver.util.FeedServerClientException;
import com.google.feedserver.util.FileUtil;
import com.google.gdata.client.GoogleService;
import com.google.gdata.util.AuthenticationException;
import org.apache.commons.lang.StringEscapeUtils;
import java.io.Console;
import java.io.File;
import java.io.IOException;
import java.io.PrintStream;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* Generic Google Feed Server Client Tool. See files in resources/clientTool for
* examples of how to invoke it with various command line arguments and the
* format of the input files.
*
*/
public class FeedServerClientTool {
public static final String OPERATION_GET_FEED = "getFeed";
public static final String OPERATION_GET_ENTRY = "getEntry";
public static final String OPERATION_INSERT = "insert";
public static final String OPERATION_UPDATE = "update";
public static final String OPERATION_DELETE = "delete";
public static final String OPERATION_INSERT_GADGETSPEC = "insertGadgetSpec";
public static final String OPERATION_UPDATE_GADGETSPEC = "updateGadgetSpec";
public static final String ALL_OPERATIONS =
OPERATION_GET_FEED + ", " + OPERATION_GET_ENTRY + ", " + OPERATION_INSERT + ", "
+ OPERATION_UPDATE + OPERATION_DELETE + OPERATION_INSERT_GADGETSPEC + " or "
+ OPERATION_UPDATE_GADGETSPEC;
public static String url_FLAG = null;
public static String url_HELP = "URL to feed or entry";
public static String op_FLAG = null;
public static String op_HELP = "Operation to perform on feed or entry (" + ALL_OPERATIONS + ")";
public static String entryFilePath_FLAG = null;
public static String entryFilePath_HELP = "Path to Atom XML file to insert or update";
public static String username_FLAG = null;
public static String username_HELP =
"Optional user name used for login. " + "Can be entered on console";
public static String password_FLAG = null;
public static String password_HELP =
"Optional password used for login. " + "Can be entered on console";
public static String authnURL_FLAG = null;
public static String authnURL_HELP =
"The URL of the server that will handle authentication and return a token to be used with every request. The format is host:portNo";
public static String authnURLProtocol_FLAG = "http";
public static String authnURLProtocol_HELP =
"Optional. The default protocol assumed is 'http'." + "Explicitly specify if its 'https'";
public static String serviceName_FLAG = null;
public static String serviceName_HELP =
"The name of the service with which the user account is associated with";
public static String gadgetName_FLAG = null;
public static String gadgetName_HELP = "The name of the gadget";
public static String gadgetSpecFile_FLAG = null;
public static String gadgetSpecFile_HELP = "The path of the gadget spec xml file";
public static String gadgetSpecEntityFile_FLAG = null;
public static String gadgetSpecEntityFile_HELP =
"The gadget spec entity file. This will define the feed entry schema used for storing gadget specs";
public static final int TAB_STOP = 2;
protected TypelessFeedServerClient feedServerClient;
protected ThreadLocal<Integer> indentation;
protected FileUtil fileUtil = new FileUtil();
public static void main(String[] args) throws Exception {
FeedServerClientTool tool = new FeedServerClientTool();
tool.run(args);
}
public FeedServerClientTool() {
indentation = new ThreadLocal<Integer>();
indentation.set(0);
}
public FeedServerClientTool(TypelessFeedServerClient feedServerClient) {
// initialize indentation for print result
indentation = new ThreadLocal<Integer>();
indentation.set(0);
this.feedServerClient = feedServerClient;
}
public void run(String[] args) {
try {
// register command line flags
CommonsCliHelper cliHelper = createClientHelper(args, getClass());
checkServiceAndAuthNParams(cliHelper);
// Process the request
processRequest(cliHelper);
} catch(Exception e) {
System.err.println("Error: " + e.getMessage());
}
}
/**
* Creates a client helper for the given command line arguments and the given
* class
*
* @param args The command line arguments
* @param clazz The class that will process the command line arguments
* @return The client helper
*/
@SuppressWarnings("unchecked")
protected CommonsCliHelper createClientHelper(String[] args, Class clazz) {
CommonsCliHelper cliHelper = new CommonsCliHelper();
cliHelper.register(clazz);
cliHelper.parse(args);
return cliHelper;
}
/**
* Processes the request by forwarding the request to appropriate methods
* matching the specified operation
*
* @param cliHelper The client helper for retrieving the required paramater
* values
* @throws FeedServerClientException
* @throws MalformedURLException
* @throws IOException
* @throws AuthenticationException
*/
protected void processRequest(CommonsCliHelper cliHelper) throws FeedServerClientException,
MalformedURLException, IOException, AuthenticationException {
// Check and process the specified request
if (OPERATION_GET_FEED.equals(op_FLAG)) {
getUserCredentials();
printFeed(getFeed(url_FLAG));
} else if (OPERATION_GET_ENTRY.equals(op_FLAG)) {
getUserCredentials();
printEntry(getEntry(url_FLAG));
} else if (OPERATION_INSERT.equals(op_FLAG)) {
getUserCredentials();
printEntry(insert(url_FLAG, readFile(new File(entryFilePath_FLAG))));
} else if (OPERATION_UPDATE.equals(op_FLAG)) {
getUserCredentials();
printEntry(update(url_FLAG, readFile(new File(entryFilePath_FLAG))));
} else if (OPERATION_DELETE.equals(op_FLAG)) {
getUserCredentials();
delete(url_FLAG);
} else if (OPERATION_INSERT_GADGETSPEC.equals(op_FLAG)) {
getUserCredentials();
printEntry(insert(url_FLAG, constructGadgetSpecEntryXml(gadgetSpecEntityFile_FLAG,
gadgetName_FLAG, gadgetSpecFile_FLAG)));
} else if (OPERATION_UPDATE_GADGETSPEC.equals(op_FLAG)) {
getUserCredentials();
printEntry(update(url_FLAG, constructGadgetSpecEntryXml(gadgetSpecEntityFile_FLAG,
gadgetName_FLAG, gadgetSpecFile_FLAG)));
} else {
if (op_FLAG != null) {
System.err.println("Unknown operation. Must use " + ALL_OPERATIONS + ".");
}
cliHelper.usage();
}
}
/**
* Checks that the user has specified the service and authn parameters
*
* @param cliHelper The client helper for retrieving the required paramater
* values
*/
protected void checkServiceAndAuthNParams(CommonsCliHelper cliHelper) {
// Check for the URL to be connected for authenticating and getting the
// authZ token
if (authnURL_FLAG == null) {
System.err.println("Must specify the URL of the server that will handle authentication");
cliHelper.usage();
return;
}
// Check for the URL to be connected for authenticating and getting the
// authZ token
if (serviceName_FLAG == null) {
System.err.println("Must specify the service name that will be used to authenticate users");
cliHelper.usage();
return;
}
// Initialize the feedserver client with the given authN URL and protocol
this.feedServerClient =
new TypelessFeedServerClient(new GoogleService(serviceName_FLAG, FeedServerClientTool.class
.getName(), authnURLProtocol_FLAG, authnURL_FLAG));
}
/**
* Gets user name and password from command line, or if missing, from console
* and sets it on FeedServer client
*
* @throws AuthenticationException
*/
protected void getUserCredentials() throws AuthenticationException {
String username = username_FLAG;
while (username == null || username.trim().isEmpty()) {
getConsole().printf("The username cannot be null or blank");
username = getConsole().readLine("\nUsername: ");
}
String password = password_FLAG;
while (password == null || password.trim().isEmpty()) {
getConsole().printf("The password cannot be null or blank");
password = new String(getConsole().readPassword("\nPassword: "));
}
feedServerClient.setUserCredentials(username, password);
}
protected Console getConsole() {
Console console = System.console();
if (console == null) {
throw new NullPointerException("no console");
} else {
return console;
}
}
/**
* Gets a feed at the given URL
*
* @param url URL of the feed
* @return A feed of entries as a List of Maps
* @throws MalformedURLException
* @throws FeedServerClientException
*/
public List<Map<String, Object>> getFeed(String url) throws MalformedURLException,
FeedServerClientException {
return feedServerClient.getEntries(new URL(url));
}
/**
* Gets an entry at the given URL
*
* @param url URL of the entry
* @return The entity as a Map
* @throws MalformedURLException
* @throws FeedServerClientException
*/
public Map<String, Object> getEntry(String url) throws MalformedURLException,
FeedServerClientException {
return feedServerClient.getEntry(new URL(url));
}
/**
* Inserts a new entry into a feed given a file containing its Atom XML
* representation
*
* @param url URL to the feed
* @param entryFile File containing the Atom XML representation of the new
* entry
* @return The inserted entity as a Map
* @throws IOException
* @throws FeedServerClientException
* @throws MalformedURLException
*/
public Map<String, Object> insert(String url, File entryFile) throws IOException,
FeedServerClientException, MalformedURLException {
return insert(url, readFile(entryFile));
}
/**
* Inserts a new entry into a feed given its Atom XML representation
*
* @param url URL to the feed
* @param entryXml Atom XML representation of the new entry
* @return The inserted entity as a Map
* @throws IOException
* @throws FeedServerClientException
* @throws MalformedURLException
*/
public Map<String, Object> insert(String url, String entryXml) throws IOException,
FeedServerClientException, MalformedURLException {
return insert(url, feedServerClient.getMapFromXml(entryXml));
}
/**
* Inserts a new entry into a feed given the entry as a Map
*
* @param url URL to the feed
* @param entity Entity to insert into feed
* @return The inserted entity as a Map
* @throws IOException
* @throws FeedServerClientException
* @throws MalformedURLException
*/
public Map<String, Object> insert(String url, Map<String, Object> entity) throws IOException,
FeedServerClientException, MalformedURLException {
return feedServerClient.insertEntry(new URL(url), entity);
}
/**
* Updates an entry given the file to its Atom XML representation
*
* @param url URL to the entry
* @param entryFile File containing the Atom XML representation of the new
* entry
* @return Updated entity as a Map
* @throws IOException
* @throws FeedServerClientException
* @throws MalformedURLException
*/
public Map<String, Object> update(String url, File entryFile) throws IOException,
FeedServerClientException, MalformedURLException {
return update(url, readFile(entryFile));
}
/**
* Updates an entry given its Atom XML representation
*
* @param url URL to the entry
* @param entryXml Atom XML representation of the entry
* @return Updated entity as a Map
* @throws IOException
* @throws FeedServerClientException
* @throws MalformedURLException
*/
public Map<String, Object> update(String url, String entryXml) throws IOException,
FeedServerClientException, MalformedURLException {
return update(url, feedServerClient.getMapFromXml(entryXml));
}
/**
* Updates an entry given its entity
*
* @param url URL to entry
* @param entity Entity to update to for the entry
* @return Updated entity as a Map
* @throws IOException
* @throws FeedServerClientException
* @throws MalformedURLException
*/
public Map<String, Object> update(String url, Map<String, Object> entity) throws IOException,
FeedServerClientException, MalformedURLException {
return feedServerClient.updateEntry(new URL(url), entity);
}
/**
* Deletes an entry at the give URL
*
* @param url URL to the entry to delete
* @throws FeedServerClientException
* @throws MalformedURLException
*/
public void delete(String url) throws FeedServerClientException, MalformedURLException {
feedServerClient.deleteEntry(new URL(url));
}
/**
* Prints a feed
*
* @param feed Feed to print as a List of Maps
*/
public void printFeed(List<Map<String, Object>> feed) {
printFeed(feed, System.out);
}
/**
* Prints a feed on a stream
*
* @param feed Feed to print
* @param out Stream to print on
*/
public void printFeed(List<Map<String, Object>> feed, PrintStream out) {
if (feed == null) {
println(out, "Resource not found");
} else {
println(out, "<entities>");
indentMore();
for (Map<String, Object> entry : feed) {
printEntry(entry, out);
}
indentLess();
println(out, "</entities>");
}
}
protected void printList(List<Map<String, Object>> feed) {
printList(feed, System.out);
}
protected void printList(List<Map<String, Object>> feed, PrintStream out) {
for (Map<String, Object> entry : feed) {
printMap(entry, out);
}
}
/**
* Prints an entry
*
* @param entry Entry to print
*/
public void printEntry(Map<String, Object> entry) {
printEntry(entry, System.out);
}
/**
* Prints an entry on a stream
*
* @param entry Entry to print
* @param out Stream to print on
*/
public void printEntry(Map<String, Object> entry, PrintStream out) {
if (entry == null) {
println(out, "Resource not found");
} else {
println(out, "<entity>");
indentMore();
printMap(entry);
indentLess();
println(out, "</entity>");
}
}
/**
* Prints error message on error output.
* @param message Message to print
*/
public void printError(String message) {
System.err.println("Error: " + message);
}
protected void printMap(Map<String, Object> entry) {
printMap(entry, System.out);
}
protected void printMap(Map<String, Object> entity, PrintStream out) {
for (Map.Entry<String, Object> e : entity.entrySet()) {
printProperty(e.getKey(), e.getValue(), out);
}
}
protected void printProperty(Map<String, Object> entity, String name, PrintStream out) {
printProperty(name, entity.get(name), out);
}
@SuppressWarnings("unchecked")
protected void printProperty(String name, Object value, PrintStream out) {
if (value != null && value instanceof Object[]) {
Object[] values = (Object[]) value;
for (int i = 0; i < values.length; i++) {
print(out, "<" + name + (i == 0 ? " repeatable=\"true\"" : "") + ">");
if (values[i] instanceof Map) {
out.println();
indentMore();
printMap((Map<String, Object>) values[i], out);
indentLess();
} else {
out.print(values[i] == null ? "" : StringEscapeUtils.escapeXml(values[i].toString()));
}
println(out, "</" + name + ">");
}
} else if (value != null && value instanceof Map) {
println(out, "<" + name + ">");
indentMore();
printMap((Map<String, Object>) value, out);
indentLess();
println(out, "</" + name + ">");
} else {
print(out, "<" + name + ">");
out.print(value == null ? "" : StringEscapeUtils.escapeXml(value.toString()));
out.println("</" + name + ">");
}
}
protected void indentMore() {
indentation.set(indentation.get() + TAB_STOP);
}
protected void indentLess() {
indentation.set(indentation.get() - TAB_STOP);
}
protected void printIndentation(PrintStream out) {
for (int i = 0; i < indentation.get(); i++) {
out.print(' ');
}
}
protected void print(PrintStream out, String s) {
printIndentation(out);
out.print(s);
}
protected void println(PrintStream out, String s) {
printIndentation(out);
out.println(s);
}
/**
* Helper function that reads contents of specified file into a String.
*
* @param file File to read.
* @return string with file contents.
* @throws IOException if any file operations fail.
*/
protected String readFile(File file) throws IOException {
return resolveEmbeddedFiles(fileUtil.readFileContents(file), file.getParentFile());
}
/**
* The pattern to get the name tag in the feed schema
*/
protected final static Pattern gadgetSpecNameTag = Pattern.compile("<name>.*?</name>");
/**
* The pattern to get the speccontent tag in the feed schema
*/
protected final static Pattern gadgetSpecSpecContentTag =
Pattern.compile("<specContent>.*?</specContent>");
/**
* The file path indicator
*/
protected final static String FILE_PATH_INDICATOR = "@";
/**
* Constructs the gadget spec entry that will be used for creating a new entry
* or updating an existing entry
*
* @param gadgetSpecEntityFile The gadget spec entity file which serves as the
* feed schema for the the gadget entry
* @param gadgetName The gadget name
* @param gadgetSpecFilePath The gadget spec file path
* @return The gadget spec entry xml
* @throws IOException If any exceptions are encountered
*/
protected String constructGadgetSpecEntryXml(String gadgetSpecEntityFile, String gadgetName,
String gadgetSpecFilePath) throws IOException {
while (gadgetName == null || gadgetName.trim().isEmpty()) {
getConsole().printf("The gadget name cannot be null or blank");
gadgetName = getConsole().readLine("\nGadget name: ");
}
while (gadgetSpecFilePath == null || gadgetSpecFilePath.trim().isEmpty()) {
getConsole().printf("The gadget spec file cannot be null or blank");
gadgetName = getConsole().readLine("\nGadget spec file: ");
}
// Get the gadget spec feed schema
File file = new File(gadgetSpecEntityFile);
String gadgetSpecXml = fileUtil.readFileContents(file);
StringBuilder fileContents = new StringBuilder();
int lastStart = 0;
// Add the given gadget name in the feed schema
for (Matcher matcher = gadgetSpecNameTag.matcher(gadgetSpecXml); matcher.find();) {
fileContents.append(gadgetSpecXml.substring(0, matcher.start() + 6));
fileContents.append(gadgetName);
lastStart = matcher.end() - 7;
}
// Add the given gadget file path in the feed schema
for (Matcher matcher = gadgetSpecSpecContentTag.matcher(gadgetSpecXml); matcher.find();) {
fileContents.append(gadgetSpecXml.substring(lastStart, lastStart + 7));
lastStart += 7;
fileContents.append(gadgetSpecXml.substring(lastStart, matcher.start() + 13));
fileContents.append(FILE_PATH_INDICATOR).append(gadgetSpecFilePath);
lastStart = matcher.end() - 14;
}
fileContents.append(gadgetSpecXml.substring((lastStart)));
// Get the final gadget spec entry xml
return resolveEmbeddedFiles(fileContents.toString(), file.getParentFile());
}
/**
* Reads a file and returns its content as a string
*
* @param directory Directory relative to which the file is
* @param filePath Path to file relative to directory
* @return Content of file
* @throws IOException
*/
protected String readFile(File directory, String filePath) throws IOException {
File file = new File(directory, filePath);
return readFile(file);
}
/**
* Pattern for embedded file path
*/
protected final static Pattern embeddedFilePathPattern = Pattern.compile(">@.*?<");
/**
* Replaces all occurrences of ">@embeddedFilePath<" with ">the XML escaped
* content of
* embeddedFilePath<". For example, if file "abc.xml"'s content is "
* <abc>value</abc>", then "<xyz>@abc.xml</xyz>" becomes
* "<xyz><abc>value</abc>".
*
* @param content Input content
* @param directory Directory where embedded files are to be found
* @return Content with all content of embedded files
* @throws IOException
*/
protected String resolveEmbeddedFiles(String content, File directory) throws IOException {
StringBuilder builder = new StringBuilder();
int lastStart = 0;
for (Matcher matcher = embeddedFilePathPattern.matcher(content); matcher.find();) {
// add before embedded file
builder.append(content.substring(lastStart, matcher.start() + 1));
// add embedded file
String group = matcher.group();
String embeddedFilePath = group.substring(2, group.length() - 1);
String embeddedFileContent = readFile(directory, embeddedFilePath);
builder.append(StringEscapeUtils.escapeXml(embeddedFileContent));
lastStart = matcher.end() - 1;
}
builder.append(content.substring(lastStart));
return builder.toString();
}
}