// Copyright 2013 Michel Kraemer
//
// 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 de.undercouch.citeproc.tool;
import java.awt.Desktop;
import java.io.File;
import java.io.IOException;
import java.io.PrintWriter;
import java.net.URI;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import de.undercouch.citeproc.ItemDataProvider;
import de.undercouch.citeproc.ListItemDataProvider;
import de.undercouch.citeproc.csl.CSLItemData;
import de.undercouch.citeproc.helper.oauth.AuthenticationStore;
import de.undercouch.citeproc.helper.oauth.FileAuthenticationStore;
import de.undercouch.citeproc.helper.oauth.RequestException;
import de.undercouch.citeproc.helper.oauth.UnauthorizedException;
import de.undercouch.citeproc.helper.tool.CachingRemoteConnector;
import de.undercouch.citeproc.remote.AuthenticatedRemoteConnector;
import de.undercouch.citeproc.remote.RemoteConnector;
import de.undercouch.underline.InputReader;
import de.undercouch.underline.OptionParserException;
/**
* Generates bibliographies and citations from remote sources
* @author Michel Kraemer
*/
public abstract class AbstractRemoteCommand extends AbstractCSLToolCommand {
private boolean sync = false;
private ProviderCommand subcommand;
/**
* Sets the sync flag
* @param sync true if the command should synchronize with the
* remote service before doing anything
*/
public void setSync(boolean sync) {
this.sync = sync;
}
/**
* Sets the subcommand to execute
* @param subcommand the subcommand
*/
public void setSubcommand(ProviderCommand subcommand) {
this.subcommand = subcommand;
if (subcommand instanceof NeedsSynchronization) {
setSync(true);
}
}
@Override
public boolean checkArguments() {
if (subcommand == null) {
error("no subcommand specified");
return false;
}
return super.checkArguments();
}
@Override
public int doRun(String[] remainingArgs, InputReader in, PrintWriter out)
throws OptionParserException, IOException {
ItemDataProvider provider = connect(sync, in);
if (provider == null) {
return 1;
}
subcommand.setProvider(provider);
return subcommand.run(remainingArgs, in, out);
}
/**
* Reads items from the remote server
* @param sync true if synchronization should be forced
* @param in a stream from which user input can be read
* @return an item data provider providing all items from the
* remote server
*/
private ItemDataProvider connect(boolean sync, InputReader in) {
//read app's consumer key and secret
String[] consumer;
try {
consumer = readConsumer();
} catch (Exception e) {
//should never happen
throw new RuntimeException("Could not read OAuth consumer key and secret");
}
//use previously stored authentication
File authStoreFile = new File(CSLToolContext.current().getConfigDir(),
getAuthStoreFileName());
AuthenticationStore authStore;
try {
authStore = new FileAuthenticationStore(authStoreFile);
} catch (IOException e) {
error("could not read user's authentication store: " + authStoreFile.getPath());
return null;
}
//connect to remote server
RemoteConnector dmc = createRemoteConnector(consumer[0], consumer[1]);
dmc = new AuthenticatedRemoteConnector(dmc, authStore);
//enable cache
File cacheFile = new File(CSLToolContext.current().getConfigDir(),
getCacheFileName());
CachingRemoteConnector mc = new CachingRemoteConnector(dmc, cacheFile);
//clear cache if necessary
if (sync) {
mc.clear();
}
List<CSLItemData> itemDataList;
int retries = 1;
while (true) {
try {
//download list of item IDs
boolean cacheempty = false;
if (!mc.hasItemList()) {
System.out.print("Retrieving items ...");
cacheempty = true;
}
List<String> items = mc.getItemIDs();
if (cacheempty) {
System.out.println();
}
//download all items
itemDataList = new ArrayList<>(items.size());
int s = 0;
int printed = 0;
int bulk = mc.getMaxBulkItems();
if (bulk == 1) {
mc.beginTransaction();
}
while (s < items.size()) {
int n = 0;
List<String> itemsToRetrieve = new ArrayList<>(bulk);
while (s < items.size() && n < bulk) {
String did = items.get(s);
if (!mc.containsItemId(did)) {
String msg = String.format("\rSynchronizing (%d/%d) ...",
s + 1, items.size());
System.out.print(msg);
++printed;
++n;
}
itemsToRetrieve.add(did);
++s;
}
Map<String, CSLItemData> itemData = mc.getItems(itemsToRetrieve);
itemDataList.addAll(itemData.values());
if (bulk == 1 && s % 10 == 0) {
mc.commitTransaction();
}
}
if (bulk == 1) {
mc.commitTransaction();
mc.endTransaction();
}
if (printed > 0) {
System.out.println();
}
} catch (UnauthorizedException e) {
if (retries == 0) {
error("failed to authorize.");
return null;
}
--retries;
//app is not authenticated yet
System.out.print("\r"); //overwrite 'Retrieving items' message
if (!authorize(mc, in)) {
return null;
}
continue;
} catch (RequestException e) {
error(e.getMessage());
return null;
} catch (IOException e) {
error("could not get list of items from remote server.");
return null;
}
break;
}
//return provider that contains all items from the server
CSLItemData[] itemDataArr = itemDataList.toArray(new CSLItemData[itemDataList.size()]);
return createItemDataProvider(itemDataArr);
}
/**
* Creates an item data provider that wraps around the given list of items
* @param itemData the items to wrap around
* @return the item data provider
*/
protected ItemDataProvider createItemDataProvider(CSLItemData[] itemData) {
return new ListItemDataProvider(itemData);
}
/**
* Creates the remote connector
* @param consumerKey the OAuth consumer key
* @param consumerSecret the OAuth consumer secret
* @return the remote connector
*/
protected abstract RemoteConnector createRemoteConnector(
String consumerKey, String consumerSecret);
/**
* @return the filename of the authentication store
*/
protected abstract String getAuthStoreFileName();
/**
* @return the filename of the item cache
*/
protected abstract String getCacheFileName();
/**
* Request authorization for the tool from remote server
* @param mc the remote connector
* @param in a stream from which user input can be read
* @return true if authorization was successful
*/
private boolean authorize(RemoteConnector mc, InputReader in) {
//get authorization URL
String authUrl;
try {
authUrl = mc.getAuthorizationURL();
} catch (IOException e) {
error("could not get authorization URL from remote server.");
return false;
}
//ask user to point browser to authorization URL
System.out.println("This tool requires authorization. Please point your "
+ "web browser to the\nfollowing URL and follow the instructions:\n");
System.out.println(authUrl);
System.out.println();
//open authorization tool in browser
if (Desktop.isDesktopSupported()) {
Desktop d = Desktop.getDesktop();
if (d.isSupported(Desktop.Action.BROWSE)) {
try {
d.browse(new URI(authUrl));
} catch (Exception e) {
//ignore. let the user open the browser manually.
}
}
}
//read verification code from console
String verificationCode;
try {
verificationCode = in.readLine("Enter verification code: ");
} catch (IOException e) {
throw new RuntimeException("Could not read from console.");
}
if (verificationCode == null || verificationCode.isEmpty()) {
//user aborted process
return false;
}
//authorize...
try {
System.out.println("Connecting ...");
mc.authorize(verificationCode);
} catch (IOException e) {
error("remote server refused authorization.");
return false;
}
return true;
}
/**
* Reads the OAuth consumer key and consumer secret
* @return the key and secret
* @throws Exception if something goes wrong
*/
protected abstract String[] readConsumer() throws Exception;
}