/*
* RapidMiner
*
* Copyright (C) 2001-2011 by Rapid-I and the contributors
*
* Complete list of developers available at our web site:
*
* http://rapid-i.com
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see http://www.gnu.org/licenses/.
*/
package com.rapidminer.repository.remote;
import java.awt.Desktop;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.lang.ref.WeakReference;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.PasswordAuthentication;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.net.URLEncoder;
import java.util.Collection;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.logging.Level;
import javax.swing.Action;
import javax.swing.event.EventListenerList;
import javax.xml.namespace.QName;
import javax.xml.ws.BindingProvider;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.xml.sax.SAXException;
import com.rapid_i.repository.wsimport.EntryResponse;
import com.rapid_i.repository.wsimport.ProcessService;
import com.rapid_i.repository.wsimport.ProcessService_Service;
import com.rapid_i.repository.wsimport.RepositoryService;
import com.rapid_i.repository.wsimport.RepositoryService_Service;
import com.rapidminer.gui.actions.BrowseAction;
import com.rapidminer.gui.tools.PasswordDialog;
import com.rapidminer.gui.tools.SwingTools;
import com.rapidminer.io.Base64;
import com.rapidminer.io.process.XMLTools;
import com.rapidminer.repository.BlobEntry;
import com.rapidminer.repository.Entry;
import com.rapidminer.repository.Folder;
import com.rapidminer.repository.IOObjectEntry;
import com.rapidminer.repository.ProcessEntry;
import com.rapidminer.repository.Repository;
import com.rapidminer.repository.RepositoryConstants;
import com.rapidminer.repository.RepositoryException;
import com.rapidminer.repository.RepositoryListener;
import com.rapidminer.repository.RepositoryManager;
import com.rapidminer.repository.gui.RemoteRepositoryPanel;
import com.rapidminer.repository.gui.RepositoryConfigurationPanel;
import com.rapidminer.tools.GlobalAuthenticator;
import com.rapidminer.tools.I18N;
import com.rapidminer.tools.LogService;
import com.rapidminer.tools.XMLException;
import com.rapidminer.tools.cipher.CipherException;
import com.rapidminer.tools.jdbc.connection.DatabaseConnectionService;
import com.rapidminer.tools.jdbc.connection.FieldConnectionEntry;
/**
* A repository connecting to a RapidAnalytics installation.
*
* @author Simon Fischer
*/
public class RemoteRepository extends RemoteFolder implements Repository {
/** Type of object requested from a server.*/
public static enum EntryStreamType {
METADATA, IOOBJECT, PROCESS, BLOB
}
private URL baseUrl;
private String alias;
private String username;
private char[] password;
private RepositoryService repositoryService;
private ProcessService processService;
private final EventListenerList listeners = new EventListenerList();
private static final Map<URI, WeakReference<RemoteRepository>> ALL_REPOSITORIES = new HashMap<URI, WeakReference<RemoteRepository>>();
private static final Object MAP_LOCK = new Object();
private boolean offline = true;
private boolean isHome;
static {
GlobalAuthenticator.registerServerAuthenticator(new GlobalAuthenticator.URLAuthenticator() {
@Override
public PasswordAuthentication getAuthentication(URL url) {
WeakReference<RemoteRepository> reposRef = null;// = ALL_REPOSITORIES.get(url);
for (Map.Entry<URI, WeakReference<RemoteRepository>> entry : ALL_REPOSITORIES.entrySet()) {
if (url.toString().startsWith(entry.getKey().toString()) || url.toString().replace("127\\.0\\.0\\.1", "localhost").startsWith(entry.getKey().toString())) {
reposRef = entry.getValue();
break;
}
}
if (reposRef == null) {
return null;
}
RemoteRepository repository = reposRef.get();
if (repository != null) {
return repository.getAuthentiaction();
} else {
return null;
}
}
@Override
public String getName() {
return "Repository authenticator";
}
@Override
public String toString() {
return getName();
}
});
}
public RemoteRepository(URL baseUrl, String alias, String username, char[] password, boolean isHome) {
super("/");
setRepository(this);
this.setAlias(alias);
this.baseUrl = baseUrl;
this.setUsername(username);
this.isHome = isHome;
if ((password != null) && (password.length > 0)) {
this.setPassword(password);
} else {
this.setPassword(null);
}
register(this);
}
private static void register(RemoteRepository remoteRepository) {
synchronized (MAP_LOCK) {
try {
ALL_REPOSITORIES.put(remoteRepository.getBaseUrl().toURI(), new WeakReference<RemoteRepository>(remoteRepository));
} catch (URISyntaxException e) {
LogService.getRoot().log(Level.SEVERE, "Could not add repository URI: " + remoteRepository.getBaseUrl().toExternalForm(), e);
}
}
}
public URL getRepositoryServiceBaseUrl() {
try {
return new URL(getBaseUrl(), "RAWS/");
} catch (MalformedURLException e) {
// cannot happen
LogService.getRoot().log(Level.WARNING, "Cannot create Web service url: " + e, e);
return null;
}
}
private URL getRepositoryServiceWSDLUrl() {
try {
return new URL(getBaseUrl(), "RAWS/RepositoryService?wsdl");
} catch (MalformedURLException e) {
// cannot happen
LogService.getRoot().log(Level.WARNING, "Cannot create Web service url: " + e, e);
return null;
}
}
private URL getProcessServiceWSDLUrl() {
try {
return new URL(getBaseUrl(), "RAWS/ProcessService?wsdl");
} catch (MalformedURLException e) {
// cannot happen
LogService.getRoot().log(Level.WARNING, "Cannot create Web service url: " + e, e);
return null;
}
}
@Override
public void addRepositoryListener(RepositoryListener l) {
listeners.add(RepositoryListener.class, l);
}
@Override
public void removeRepositoryListener(RepositoryListener l) {
listeners.remove(RepositoryListener.class, l);
}
@Override
public boolean rename(String newName) {
this.setAlias(newName);
fireEntryRenamed(this);
return true;
}
protected void fireEntryRenamed(Entry entry) {
for (RepositoryListener l : listeners.getListeners(RepositoryListener.class)) {
l.entryRenamed(entry);
}
}
protected void fireEntryAdded(Entry newEntry, Folder parent) {
for (RepositoryListener l : listeners.getListeners(RepositoryListener.class)) {
l.entryAdded(newEntry, parent);
}
}
protected void fireEntryRemoved(Entry removedEntry, Folder parent, int index) {
for (RepositoryListener l : listeners.getListeners(RepositoryListener.class)) {
l.entryRemoved(removedEntry, parent, index);
}
}
protected void fireRefreshed(Folder folder) {
for (RepositoryListener l : listeners.getListeners(RepositoryListener.class)) {
l.folderRefreshed(folder);
}
}
private Map<String, RemoteEntry> cachedEntries = new HashMap<String, RemoteEntry>();
/** Connection entries fetched from server. */
private Collection<FieldConnectionEntry> connectionEntries;
private boolean cachedPasswordUsed = false;
protected void register(RemoteEntry entry) {
cachedEntries.put(entry.getPath(), entry);
}
@Override
public Entry locate(String string) throws RepositoryException {
Entry cached = cachedEntries.get(string);
if (cached != null) {
return cached;
}
Entry firstTry = RepositoryManager.getInstance(null).locate(this, string, true);
if (firstTry != null) {
return firstTry;
}
if (!string.startsWith("/")) {
string = "/" + string;
}
EntryResponse response = getRepositoryService().getEntry(string);
if (response.getStatus() != RepositoryConstants.OK) {
if (response.getStatus() == RepositoryConstants.NO_SUCH_ENTRY) {
return null;
}
throw new RepositoryException(response.getErrorMessage());
}
if (response.getType().equals(Folder.TYPE_NAME)) {
return new RemoteFolder(response, null, this);
} else if (response.getType().equals(ProcessEntry.TYPE_NAME)) {
return new RemoteProcessEntry(response, null, this);
} else if (response.getType().equals(IOObjectEntry.TYPE_NAME)) {
return new RemoteIOObjectEntry(response, null, this);
} else if (response.getType().equals(BlobEntry.TYPE_NAME)) {
return new RemoteBlobEntry(response, null, this);
} else {
throw new RepositoryException("Unknown entry type: " + response.getType());
}
}
@Override
public String getName() {
return getAlias();
}
@Override
public String getState() {
return (offline ? "offline" : (repositoryService != null ? "connected" : "disconnected"));
}
@Override
public String getIconName() {
return I18N.getMessage(I18N.getGUIBundle(), "gui.repository.remote.icon");
}
@Override
public String toString() {
return "<html>" + getAlias() + "<br/><small style=\"color:gray\">(" + getBaseUrl() + ")</small></html>";
}
private PasswordAuthentication getAuthentiaction() {
if (password == null) {
LogService.getRoot().info("Authentication requested for URL: " + getBaseUrl());
PasswordAuthentication passwordAuthentication;
if (cachedPasswordUsed) {
// if we have used a cached password last time, and we enter this method again,
// this is probably because the password was wrong, so rather force dialog than
// using cache again.
passwordAuthentication = PasswordDialog.getPasswordAuthentication(getBaseUrl().toString(), false, false);
this.cachedPasswordUsed = false;
} else {
passwordAuthentication = PasswordDialog.getPasswordAuthentication(getBaseUrl().toString(), false, true);
this.cachedPasswordUsed = true;
}
if (passwordAuthentication != null) {
this.setPassword(passwordAuthentication.getPassword());
this.setUsername(passwordAuthentication.getUserName());
RepositoryManager.getInstance(null).save();
}
return passwordAuthentication;
} else {
return new PasswordAuthentication(getUsername(), password);
}
}
public RepositoryService getRepositoryService() throws RepositoryException {
// if (offline) {
// throw new RepositoryException("Repository "+getName()+" is offline. Connect first.");
// }
installJDBCConnectionEntries();
if (repositoryService == null) {
try {
RepositoryService_Service serviceService = new RepositoryService_Service(getRepositoryServiceWSDLUrl(), new QName("http://service.web.rapidanalytics.de/", "RepositoryService"));
repositoryService = serviceService.getRepositoryServicePort();
setCredentials((BindingProvider)repositoryService);
offline = false;
} catch (Exception e) {
offline = true;
setPassword(null);
repositoryService = null;
throw new RepositoryException("Cannot connect to " + getBaseUrl() + ": " + e, e);
}
}
return repositoryService;
}
private void setCredentials(BindingProvider bp) {
if (password != null) {
bp.getRequestContext().put(BindingProvider.USERNAME_PROPERTY, getUsername());
bp.getRequestContext().put(BindingProvider.PASSWORD_PROPERTY, new String(password));
}
}
public ProcessService getProcessService() throws RepositoryException {
// if (offline) {
// throw new RepositoryException("Repository "+getName()+" is offline. Connect first.");
// }
if (processService == null) {
try {
ProcessService_Service serviceService = new ProcessService_Service(getProcessServiceWSDLUrl(), new QName("http://service.web.rapidanalytics.de/", "ProcessService"));
processService = serviceService.getProcessServicePort();
setCredentials((BindingProvider)processService);
offline = false;
} catch (Exception e) {
offline = true;
setPassword(null);
processService = null;
throw new RepositoryException("Cannot connect to " + getBaseUrl() + ": " + e, e);
}
}
return processService;
}
@Override
public String getDescription() {
return "Remote repository at " + getBaseUrl();
}
@Override
public void refresh() throws RepositoryException {
offline = false;
cachedEntries.clear();
super.refresh();
removeJDBCConnectionEntries();
installJDBCConnectionEntries();
}
/** Returns a connection to a given location in the repository.
* @param preAuthHeader If set, the Authorization: header will be set to basic auth. Otherwise, the {@link GlobalAuthenticator} mechanism
* will be used.
* @param type can be null*/
public HttpURLConnection getResourceHTTPConnection(String location, EntryStreamType type, boolean preAuthHeader) throws IOException {
String split[] = location.split("/");
StringBuilder encoded = new StringBuilder();
encoded.append("RAWS/resources");
for (String fraction : split) {
if (!fraction.isEmpty()) { // only for non empty to prevent double //
encoded.append('/');
encoded.append(URLEncoder.encode(fraction, "UTF-8"));
}
}
if (type == EntryStreamType.METADATA) {
encoded.append("?format=binmeta");
}
return getHTTPConnection(encoded.toString(), preAuthHeader);
}
public HttpURLConnection getHTTPConnection(String pathInfo, boolean preAuthHeader) throws IOException {
final HttpURLConnection conn = (HttpURLConnection) new URL(getBaseUrl(), pathInfo).openConnection();
if (preAuthHeader) {
String userpass = username + ":" + new String(password);
String basicAuth = "Basic " + new String(Base64.encodeBytes(userpass.getBytes()));
conn.setRequestProperty ("Authorization", basicAuth);
}
return conn;
}
@Override
public Element createXML(Document doc) {
Element repositoryElement = doc.createElement("remoteRepository");
Element url = doc.createElement("url");
url.appendChild(doc.createTextNode(this.getBaseUrl().toString()));
repositoryElement.appendChild(url);
Element alias = doc.createElement("alias");
alias.appendChild(doc.createTextNode(this.getAlias()));
repositoryElement.appendChild(alias);
Element user = doc.createElement("user");
user.appendChild(doc.createTextNode(this.getUsername()));
repositoryElement.appendChild(user);
return repositoryElement;
}
public static RemoteRepository fromXML(Element element) throws XMLException {
String url = XMLTools.getTagContents(element, "url", true);
try {
return new RemoteRepository(new URL(url), XMLTools.getTagContents(element, "alias", true), XMLTools.getTagContents(element, "user", true), null, false);
} catch (MalformedURLException e) {
throw new XMLException("Illegal url '" + url + "': " + e, e);
}
}
@Override
public void delete() {
RepositoryManager.getInstance(null).removeRepository(this);
}
public static List<RemoteRepository> getAll() {
List<RemoteRepository> result = new LinkedList<RemoteRepository>();
for (WeakReference<RemoteRepository> ref : ALL_REPOSITORIES.values()) {
RemoteRepository rep = ref.get();
if (ref != null) {
result.add(rep);
}
}
return result;
}
public boolean isConnected() {
return !offline;
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((getAlias() == null) ? 0 : getAlias().hashCode());
int uriModificator = 0;
try {
uriModificator = (getBaseUrl() == null) ? 0 : getBaseUrl().toURI().hashCode();
} catch (URISyntaxException e) {
}
result = prime * result + uriModificator;
result = prime * result + ((getUsername() == null) ? 0 : getUsername().hashCode());
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
RemoteRepository other = (RemoteRepository) obj;
if (getAlias() == null) {
if (other.getAlias() != null)
return false;
} else if (!getAlias().equals(other.getAlias()))
return false;
if (getBaseUrl() == null) {
if (other.getBaseUrl() != null)
return false;
} else
try {
if (!getBaseUrl().toURI().equals(other.getBaseUrl().toURI()))
return false;
} catch (URISyntaxException e) {
// this cannot happen, since we already had have a valid URL: no possible problem when converting to uri
return false;
}
if (getUsername() == null) {
if (other.getUsername() != null)
return false;
} else if (!getUsername().equals(other.getUsername()))
return false;
return true;
}
/** Returns the URI to which a browser can be pointed to browse a given entry. */
public URI getURIForResource(String path) {
try {
return getBaseUrl().toURI().resolve("/RA/faces/restricted/browse.xhtml?location=" + URLEncoder.encode(path, "UTF-8"));
} catch (UnsupportedEncodingException e) {
throw new RuntimeException(e);
} catch (URISyntaxException e) {
throw new RuntimeException(e);
}
}
/** Returns the URI to which a browser can be pointed to access the RA web interface. */
private URI getURIWebInterfaceURI() {
try {
return getBaseUrl().toURI().resolve("RA/faces/restricted/index.xhtml");
} catch (URISyntaxException e) {
throw new RuntimeException(e);
}
}
public void browse(String location) {
try {
Desktop.getDesktop().browse(getURIForResource(location));
} catch (Exception e) {
SwingTools.showSimpleErrorMessage("cannot_open_browser", e);
}
}
public void showLog(int id) {
try {
Desktop.getDesktop().browse(getProcessLogURI(id));
} catch (Exception e) {
SwingTools.showSimpleErrorMessage("cannot_open_browser", e);
}
}
public URI getProcessLogURI(int id) {
try {
return getBaseUrl().toURI().resolve("/RA/processlog?id=" + id);
} catch (URISyntaxException e) {
throw new RuntimeException(e);
}
}
@Override
public Collection<Action> getCustomActions() {
Collection<Action> actions = super.getCustomActions();
actions.add(new BrowseAction("remoterepository.administer", getRepository().getURIWebInterfaceURI()));
return actions;
}
@Override
public boolean shouldSave() {
return !isHome;
}
// JDBC entries provided by server
private Collection<FieldConnectionEntry> fetchJDBCEntries() throws XMLException, CipherException, SAXException, IOException {
URL xmlURL = new URL(getBaseUrl(), "RAWS/jdbc_connections.xml");
Document doc = XMLTools.parse(xmlURL.openStream());
final Collection<FieldConnectionEntry> result = DatabaseConnectionService.parseEntries(doc.getDocumentElement());
for (FieldConnectionEntry entry : result) {
entry.setRepository(getAlias());
}
return result;
}
@Override
public void postInstall() {
}
private void installJDBCConnectionEntries() {
if (this.connectionEntries != null) {
return;
}
try {
this.connectionEntries = fetchJDBCEntries();
for (FieldConnectionEntry entry : connectionEntries) {
DatabaseConnectionService.addConnectionEntry(entry);
}
LogService.getRoot().config("Added " + connectionEntries.size() + " jdbc connections exported by " + getName() + ".");
} catch (Exception e) {
LogService.getRoot().log(Level.WARNING, "Failed to fetch JDBC connection entries from server " + getName() + ".", e);
}
}
private void removeJDBCConnectionEntries() {
if (this.connectionEntries != null) {
for (FieldConnectionEntry entry : connectionEntries) {
DatabaseConnectionService.deleteConnectionEntry(entry);
}
this.connectionEntries = null;
}
}
@Override
public void preRemove() {
}
public URL getBaseUrl() {
return baseUrl;
}
public void setBaseUrl(URL url) {
baseUrl = url;
}
public void setUsername(String username) {
this.username = username;
}
public String getUsername() {
return username;
}
public void setPassword(char[] password) {
this.password = password;
}
@Override
public boolean isConfigurable() {
return true;
}
@Override
public RepositoryConfigurationPanel makeConfigurationPanel() {
return new RemoteRepositoryPanel();
}
public void setAlias(String alias) {
this.alias = alias;
}
public String getAlias() {
return alias;
}
}