// Copyright 2010 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.enterprise.connector.persist;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSortedSet;
import com.google.enterprise.connector.common.AbstractCommandLineApp;
import com.google.enterprise.connector.instantiator.TypeMap;
import com.google.enterprise.connector.manager.Context;
import org.apache.commons.cli.CommandLine;
import org.apache.commons.cli.Option;
import org.apache.commons.cli.Options;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.Collection;
import java.util.Map;
import java.util.TreeSet;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* A utility to migrate connector data from one PersistentStore
* to another.
*
* <pre>
* usage: MigrateStore [-?] [-v] [-c connector_name] [-l] [source_name] [dest_name]
* -?, --help Display this help.
* -v, --version Display version.
* -c, --connector Connector(s) to migrage (default is all connectors).
* -l, --list List available PersistentStores.
* -f, --force Overwrite existing data in destination PersistentStore.
* source_name Name of source PeristentStore (e.g. FilePersistentStore)
* dest_name Name of destination PeristentStore (e.g. JdbcPersistentStore)
* </pre>
*/
public class MigrateStore extends AbstractCommandLineApp {
/** Retrieve the TypeMap from the Spring Context. */
private TypeMap getTypeMap() {
return (TypeMap) Context.getInstance().getRequiredBean(
"TypeMap", TypeMap.class);
}
@Override
public String getName() {
return "MigrateStore";
}
@Override
public String getDescription() {
return "Migrates Connector configurations between Persistent Stores.";
}
@Override
public String getCommandLineSyntax() {
return super.getCommandLineSyntax()
+ "[-l] [-f] [-c connector] [source_name] [dest_name]";
}
@Override
public Options getOptions() {
Options options = super.getOptions();
options.addOption("l", "list", false, "List available PersistentStores.");
options.addOption("f", "force", false,
"Overwrite existing data in destination PersistentStore.");
Option o = new Option("c", "connector_name", true, "Connector to migrate.");
o.setArgName("connector_name");
options.addOption(o);
return options;
}
@Override
protected String getUsageFooter() {
StringBuilder builder = new StringBuilder(NL);
builder.append(getName());
builder.append(" migrates connector configurations from the source ");
builder.append("Persistent Store location to destination Persistent ");
builder.append("Store location. This is useful when upgrading older ");
builder.append("connector installations, when moving connector ");
builder.append("deployments from test to production, or when moving ");
builder.append("from the embedded database to a corporate database.");
builder.append(NL).append(NL);
builder.append("One or more connectors to migrate may be specified using ");
builder.append("-c options. If unspecified, all connectors are migrated.");
builder.append(NL).append(NL);
builder.append("If configuration data for a connector already exists ");
builder.append("in the destination store, it will not be overwritten ");
builder.append("unless forced to do so by using the --force option.");
return builder.toString();
}
@Override
public void run(CommandLine commandLine) throws Exception {
initStandAloneContext(false);
// Since we did not start the Context, we need to init TypeMap
// for PersistentStores to function correctly.
getTypeMap().init();
try {
// If user asks for a list of available PersitentStores,
// print it and exit.
if (commandLine.hasOption("list")) {
listStores();
return;
}
// Get then names of the source and destination PersitentStores.
String sourceName = null;
String destName = null;
String[] args = commandLine.getArgs();
if ((args.length == 1) || (args.length > 2)) {
printUsageAndExit(-1);
}
if (args.length == 2) {
sourceName = args[0];
destName = args[1];
} else {
Collection<String> storeNames = getStoreNames();
sourceName = selectStoreName("source", storeNames);
if (sourceName == null) {
return;
}
storeNames.remove(sourceName);
destName = selectStoreName("destination", storeNames);
if (destName == null) {
return;
}
}
if (sourceName.equals(destName)) {
System.err.println(
"Source and destination PersistentStores must be different.");
return;
}
PersistentStore sourceStore = getPersistentStoreByName(sourceName);
// Determine which connectors to migrate.
Collection<String> connectors = null;
String[] connectorNames = commandLine.getOptionValues('c');
if (connectorNames != null) {
connectors = ImmutableSortedSet.copyOf(connectorNames);
} else if (args.length != 2) {
// If no connectors were specified on the command line, and we had
// to prompt the user for the source and destination stores, then also
// prompt the user for a connector to migrate.
String name = selectConnectorName(getConnectorNames(sourceStore));
if (name != null) {
connectors = ImmutableSortedSet.of(name);
}
}
// Actually perform the migration.
PersistentStore destStore = getPersistentStoreByName(destName);
if (sourceStore != null && destStore != null) {
// Adjust the logging levels so that StoreMigrator messages are logged
// to the Console.
Logger.getLogger(StoreMigrator.class.getName()).setLevel(Level.INFO);
StoreMigrator.migrate(sourceStore, destStore, connectors,
commandLine.hasOption("force"));
StoreMigrator.checkMissing(destStore, connectors);
}
} finally {
shutdown();
}
}
/**
* Prints out a list of available PersistentStores.
*/
private void listStores() {
printVersion();
System.out.println("Available PersistentStores:");
for (String name : getStoreNames()) {
System.out.println(" " + name);
}
System.out.println("");
}
/**
* Returns a storeName selected by the user.
*/
private String selectStoreName(String which, Collection<String> choices) {
return pickMenu("Available PersistentStores:",
"Please select the " + which + " PersistentStore: ",
choices);
}
/**
* Returns the named PersistentStore instance.
*/
private PersistentStore getPersistentStoreByName(String name) {
try {
PersistentStore store = (PersistentStore) Context.getInstance()
.getApplicationContext().getBean(name, PersistentStore.class);
if (store == null) {
System.err.println("No PersistentStore named " + name + " was found.");
}
return store;
} catch (NoSuchBeanDefinitionException e) {
System.err.println("No PersistentStore named " + name + " was found.");
} catch (BeansException e) {
System.err.println("Spring failure - can't instantiate " + name + ": ("
+ e.toString() + ")");
}
return null;
}
/**
* Returns a Collection of the names of configured, but not disabled,
* PersistentStores.
*/
@SuppressWarnings("unchecked")
private Collection<String> getStoreNames() {
TreeSet<String> names = new TreeSet<String>();
Map<String, PersistentStore> stores = (Map<String, PersistentStore>)
Context.getInstance().getApplicationContext()
.getBeansOfType(PersistentStore.class);
for (Map.Entry<String, PersistentStore> entry : stores.entrySet()) {
// Only include PersistentStores that are not disabled.
if (!entry.getValue().isDisabled()) {
names.add(entry.getKey());
}
}
return names;
}
/**
* Returns a connector name to migrate selected by the user.
*/
private String selectConnectorName(Collection<String> connectorNames) {
return pickMenu("Available Connector Instances:",
"Please select Connectors to migrate [All]: ",
connectorNames);
}
/**
* Returns a Collection of the names of configured PersistentStores.
*/
private Collection<String> getConnectorNames(PersistentStore sourceStore) {
ImmutableMap<StoreContext, ConnectorStamps> inventory =
sourceStore.getInventory();
TreeSet<String> names = new TreeSet<String>();
for (StoreContext context : inventory.keySet()) {
names.add(context.getConnectorName());
}
return names;
}
/**
* Prints a menu from a Collection of choices, asks the user to pick one.
*
* @param header Text to display above the list of choices.
* @param prompt Text to display on the prompt line.
* @param choices Available choices.
* @return item chosen from the choices, or null if none was selected.
*/
private String pickMenu(String header, String prompt,
Collection<String> choices) {
try {
String[] items = choices.toArray(new String[0]);
BufferedReader in = new BufferedReader(new InputStreamReader(System.in));
do {
System.out.println("");
System.out.println(header);
for (int i = 0; i < items.length; i++) {
System.out.println(" " + (i + 1) + ") " + items[i]);
}
System.out.print(prompt);
String answer = in.readLine();
if (answer == null || answer.trim().length() == 0) {
return null;
}
int choiceNum;
try {
choiceNum = Integer.parseInt(answer) - 1;
} catch (NumberFormatException nfe) {
choiceNum = -1; // Force chose again.
}
if (choiceNum >= 0 && choiceNum < items.length) {
return items[choiceNum];
}
System.err.println("Invalid choice.\n");
} while (true);
} catch (IOException e) {
System.err.println("Failed to read from input: " + e.getMessage());
}
return null;
}
/**
* Proper main() in case this is called directly.
*/
public static void main(String[] args) throws Exception {
MigrateStore app = new MigrateStore();
app.run(app.parseArgs(args));
System.exit(0);
}
}