/*
*------------------------------------------------------------------------------
* Copyright (C) 2006-2015 University of Dundee. All rights reserved.
*
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License along
* with this program; if not, write to the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*
*------------------------------------------------------------------------------
*/
package org.openmicroscopy.shoola.env.data;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.Map.Entry;
import java.util.concurrent.atomic.AtomicBoolean;
import javax.swing.JDialog;
import javax.swing.JFrame;
import org.openmicroscopy.shoola.env.Agent;
import org.openmicroscopy.shoola.env.Container;
import org.openmicroscopy.shoola.env.Environment;
import org.openmicroscopy.shoola.env.LookupNames;
import org.openmicroscopy.shoola.env.cache.CacheServiceFactory;
import org.openmicroscopy.shoola.env.config.AgentInfo;
import org.openmicroscopy.shoola.env.config.Registry;
import org.openmicroscopy.shoola.env.config.RegistryFactory;
import org.openmicroscopy.shoola.env.data.events.ConnectedEvent;
import org.openmicroscopy.shoola.env.data.events.ReloadRenderingEngine;
import org.openmicroscopy.shoola.env.data.login.LoginService;
import org.openmicroscopy.shoola.env.data.login.UserCredentials;
import omero.gateway.LoginCredentials;
import omero.gateway.SecurityContext;
import omero.gateway.exception.DSAccessException;
import omero.gateway.exception.DSOutOfServiceException;
import org.openmicroscopy.shoola.env.data.views.DataViewsFactory;
import org.openmicroscopy.shoola.env.event.EventBus;
import omero.log.LogMessage;
import omero.log.Logger;
import org.openmicroscopy.shoola.env.rnd.PixelsServicesFactory;
import org.openmicroscopy.shoola.env.rnd.RenderingControl;
import org.openmicroscopy.shoola.env.ui.UserNotifier;
import org.openmicroscopy.shoola.svc.proxy.ProxyUtil;
import org.openmicroscopy.shoola.util.CommonsLangUtils;
import org.openmicroscopy.shoola.util.ui.IconManager;
import org.openmicroscopy.shoola.util.ui.MessageBox;
import org.openmicroscopy.shoola.util.ui.NotificationDialog;
import org.openmicroscopy.shoola.util.ui.ShutDownDialog;
import org.openmicroscopy.shoola.util.ui.UIUtilities;
import omero.gateway.model.ExperimenterData;
import omero.gateway.model.GroupData;
/**
* A factory for the {@link OmeroDataService} and the {@link OmeroImageService}.
*
* @author Jean-Marie Burel
* <a href="mailto:j.burel@dundee.ac.uk">j.burel@dundee.ac.uk</a>
* @author <br>Andrea Falconi
* <a href="mailto:a.falconi@dundee.ac.uk">
* a.falconi@dundee.ac.uk</a>
* @version 2.2
* @since OME2.2
*/
public class DataServicesFactory
{
/** The sole instance. */
private static DataServicesFactory singleton;
/** The dialog indicating that the connection is lost.*/
private JDialog connectionDialog;
/** Flag indicating that the client and server are not compatible.*/
private boolean compatible;
/** Flag indicating that if the upgrade check has been performed or not.*/
private boolean upgradeCheck = false;
/**
* Creates a new instance. This can't be called outside of container
* b/c agents have no references to the singleton container.
* So we can be sure this method is going to create services just once.
*
* @param c Reference to the singleton container. Mustn't be
* <code>null</code>.
* @return See above.
* @throws DSOutOfServiceException
*/
public static DataServicesFactory getInstance(Container c)
throws DSOutOfServiceException
{
if (c == null)
throw new NullPointerException(); //An agent called this method?
if (singleton == null)
singleton = new DataServicesFactory(c);
return singleton;
}
/**
* Reference to the container, to exit the application when the session
* has expired.
*/
private Container container;
/** A reference to the container's registry. */
private static Registry registry;
/** Unified access point to the various OMERO services. */
private static OMEROGateway omeroGateway;
/** The OMERO service adapter. */
private OmeroDataService ds;
/** The image service adapter. */
private OmeroImageService is;
/** The metadata service adapter. */
private OmeroMetadataService ms;
/** The Administration service adapter. */
private AdminService admin;
/** Flag indicating that we try to re-establish the connection.*/
private final AtomicBoolean reconnecting = new AtomicBoolean(false);
/**
* Attempts to create a new instance.
*
* @param c Reference to the container.
* @throws DSOutOfServiceException If the connection can't be established
* or the credentials are invalid.
*/
private DataServicesFactory(Container c)
throws DSOutOfServiceException
{
registry = c.getRegistry();
container = c;
//Check what to do if null.
omeroGateway = new OMEROGateway(this);
//Create the adapters.
ds = new OmeroDataServiceImpl(omeroGateway, registry);
is = new OmeroImageServiceImpl(omeroGateway, registry);
ms = new OmeroMetadataServiceImpl(omeroGateway, registry);
admin = new AdminServiceImpl(omeroGateway, registry);
// pass the adapters on to the registry
RegistryFactory.linkOS(ds, registry);
RegistryFactory.linkMS(ms, registry);
RegistryFactory.linkAdmin(admin, registry);
RegistryFactory.linkIS(is, registry);
RegistryFactory.linkGateway(omeroGateway.getGateway(), registry);
//Initialize the Views Factory.
DataViewsFactory.initialize(c);
}
/**
* Determines the quality of the compression depending on the
* connection speed.
*
* @param connectionSpeed The connection speed.
* @return See above.
*/
private float determineCompression(int connectionSpeed)
{
Float value;
switch (connectionSpeed) {
case UserCredentials.MEDIUM:
case UserCredentials.HIGH:
value = (Float) registry.lookup(
LookupNames.COMPRESSIOM_MEDIUM_QUALITY);
return value.floatValue();
case UserCredentials.LOW:
default:
value = (Float) registry.lookup(
LookupNames.COMPRESSIOM_LOW_QUALITY);
return value.floatValue();
}
}
/**
* Returns the image quality with respect to the
* connection speed
*
* @param connectionSpeed The connection speed.
* @return See above.
*/
private int determineImageQuality(int connectionSpeed)
{
switch (connectionSpeed) {
case UserCredentials.HIGH:
return RenderingControl.UNCOMPRESSED;
case UserCredentials.MEDIUM:
return RenderingControl.MEDIUM;
case UserCredentials.LOW:
default:
return RenderingControl.LOW;
}
}
/**
* Returns <code>true</code> if the server and the client are compatible,
* <code>false</code> otherwise. Return <code>null</code> if an error
* occurred while comparing the versions and the user does not want to
* connect.
*
* @param server The version of the server.
* @param client The version of the client.
* @return See above.
*/
private Boolean checkClientServerCompatibility(String server, String client)
{
if (server == null || client == null) return false;
if (client.startsWith("@")) return true;
if (server.contains("-"))
server = server.split("-")[0];
if (client.contains("-"))
client = client.split("-")[0];
String[] values = server.split("\\.");
String[] valuesClient = client.split("\\.");
if (values.length < 2 || valuesClient.length < 2) return false;
try {
int s1 = Integer.parseInt(values[0]);
int s2 = Integer.parseInt(values[1]);
int c1 = Integer.parseInt(valuesClient[0]);
int c2 = Integer.parseInt(valuesClient[1]);
if (s1 < c1) return false;
if (s2 != c2) return false;
} catch (Exception e) {
//Record error
LogMessage msg = new LogMessage();
msg.print("Client server compatibility");
msg.print(e);
registry.getLogger().debug(this, msg);
//Notify user that it is not possible to parse
String message = "An error occurred while checking " +
"the compatibility between client and server." +
"\nDo you " +
"still want to connect (further errors might occur)?";
JFrame f = new JFrame();
f.setIconImage(IconManager.getOMEImageIcon());
MessageBox box = new MessageBox(f, "Version Check", message);
box.setAlwaysOnTop(true);
if (box.centerMsgBox() == MessageBox.YES_OPTION) {
return true;
}
return null;
}
return true;
}
/**
* Notifies the user that the client and the server are not compatible.
*
* @param clientVersion The version of the client.
* @param serverVersion The version of the server.
* @param hostname The name of the server.
*/
private void notifyIncompatibility(String clientVersion,
String serverVersion, String hostname)
{
UserNotifier un = registry.getUserNotifier();
StringBuffer buffer = new StringBuffer();
buffer.append("The client version ("+clientVersion+") is not " +
"compatible with the server:\n"+hostname);
if (serverVersion != null) {
buffer.append(" version:"+serverVersion);
}
buffer.append(".");
un.notifyInfo("Client Server not compatible", buffer.toString());
}
/**
* Returns the credentials.
*
* @return See above.
*/
UserCredentials getCredentials()
{
return (UserCredentials)
registry.lookup(LookupNames.USER_CREDENTIALS);
}
/**
* Returns the time before each network check.
*
* @return See above.
*/
Integer getElapseTime()
{
return (Integer) registry.lookup(LookupNames.ELAPSE_TIME);
}
/**
* Adds a listener to the dialog and shows the dialog depending on the
* specified value.
*/
private void addListenerAndShow()
{
if (connectionDialog instanceof ShutDownDialog) {
ShutDownDialog d = (ShutDownDialog) connectionDialog;
d.setGateway(omeroGateway.getGateway());
d.setCheckupTime(5);
}
connectionDialog.setModal(false);
connectionDialog.addPropertyChangeListener(new PropertyChangeListener()
{
public void propertyChange(PropertyChangeEvent evt) {
String name = evt.getPropertyName();
if (NotificationDialog.CLOSE_NOTIFICATION_PROPERTY.equals(name))
{
reconnecting.set(false);
connectionDialog = null;
exitApplication(true, true);
} else if (
NotificationDialog.CANCEL_NOTIFICATION_PROPERTY.equals(
name))
{
connectionDialog = null;
reconnecting.set(false);
int index = (Integer) evt.getNewValue();
if (index == ConnectionExceptionHandler.LOST_CONNECTION)
reconnect();
}
}
});
connectionDialog.setModal(true);
UIUtilities.centerAndShow(connectionDialog);
}
/** Attempts to reconnect.*/
private void reconnect()
{
JFrame f = registry.getTaskBar().getFrame();
String message;
Map<SecurityContext, Set<Long>> l =
omeroGateway.getRenderingEngines();
boolean b = omeroGateway.joinSession();
if (b) {
//reactivate the rendering engine. Need to review that
Iterator<Entry<SecurityContext, Set<Long>>> i =
l.entrySet().iterator();
OmeroImageService svc = registry.getImageService();
Long id;
Entry<SecurityContext, Set<Long>> entry;
Map<SecurityContext, List<Long>>
failures = new HashMap<SecurityContext, List<Long>>();
Iterator<Long> j;
SecurityContext ctx;
List<Long> failure;
RenderingControl p;
while (i.hasNext()) {
entry = i.next();
j = entry.getValue().iterator();
ctx = entry.getKey();
while (j.hasNext()) {
id = j.next();
try {
p = PixelsServicesFactory.getRenderingControl(
registry, Long.valueOf(id), false);
if (!p.isShutDown()) {
registry.getLogger().debug(this,
"loading re "+id);
svc.reloadRenderingService(ctx, id);
}
} catch (Exception e) {
failure = failures.get(ctx);
if (failure == null) {
failure = new ArrayList<Long>();
failures.put(ctx, failure);
}
registry.getLogger().debug(this,
"Failed to load re for "+id+" "+e);
failure.add(id);
}
}
}
if (failures.size() > 0) {
registry.getEventBus().post(
new ReloadRenderingEngine(failures));
}
connectionDialog.setVisible(false);
connectionDialog.dispose();
connectionDialog = null;
reconnecting.set(false);
} else {
//connectionDialog.setVisible(false);
message = "A failure occurred while attempting to " +
"reconnect.\nThe application will now exit.";
connectionDialog = new NotificationDialog(f,
"Reconnection Failure", message, null);
addListenerAndShow();
}
}
/**
* Returns the value of the plug-in or <code>-1</code>.
*
* @return See above.
*/
int runAsPlugin()
{
Integer v = (Integer) container.getRegistry().lookup(
LookupNames.PLUGIN);
if (v == null) return -1;
return v.intValue();
}
/**
* Brings up a dialog indicating that the session has expired and
* quits the application.
*
* @param index One of the connection constants defined by the gateway.
* @param exc The exception to register.
*/
public void sessionExpiredExit(int index, Throwable exc)
{
if (reconnecting.get()) return;
reconnecting.set(true);
String message;
if (exc != null) {
LogMessage msg = new LogMessage();
msg.print("Connection Error");
msg.print(exc);
registry.getLogger().debug(this, msg);
}
JFrame f = registry.getTaskBar().getFrame();
switch (index) {
case ConnectionExceptionHandler.DESTROYED_CONNECTION:
message = "The connection has been destroyed." +
"\nThe application will now exit.";
connectionDialog = new NotificationDialog(f,
"Connection Refused", message, null);
addListenerAndShow();
break;
case ConnectionExceptionHandler.NETWORK:
message = "The network is down.\n";
connectionDialog = new ShutDownDialog(f, "Network down",
message, -1);
addListenerAndShow();
break;
case ConnectionExceptionHandler.LOST_CONNECTION:
connectionDialog = new ShutDownDialog(f, "Lost connection",
"Trying to reconnect...", index);
addListenerAndShow();
break;
case ConnectionExceptionHandler.SERVER_OUT_OF_SERVICE:
message = "The server is no longer " +
"running.\nPlease contact your system administrator." +
"\nThe application will now exit.";
connectionDialog = new NotificationDialog(f,
"Connection Refused", message, null);
addListenerAndShow();
}
}
/**
* Returns the {@link OmeroDataService}.
*
* @return See above.
*/
public OmeroDataService getOS() { return ds; }
/**
* Returns the {@link OmeroImageService}.
*
* @return See above.
*/
public OmeroImageService getIS() { return is; }
/**
* Returns the {@link OmeroMetadataService}.
*
* @return See above.
*/
public OmeroMetadataService getMS() { return ms; }
/**
* Returns the {@link AdminService}.
*
* @return See above.
*/
public AdminService getAdmin() { return admin; }
/**
* Returns the {@link LoginService}.
*
* @return See above.
*/
public LoginService getLoginService()
{
return (LoginService) registry.lookup(LookupNames.LOGIN);
}
/**
* Returns the {@link Logger}
*
* @return See above.
*/
Logger getLogger()
{
return (Logger) registry.getLogger();
}
/**
* Attempts to connect to <i>OMERO</i> server.
*
* @param uc The user's credentials for logging onto <i>OMERO</i> server.
* @throws DSOutOfServiceException If the connection can't be established
* or the credentials are invalid.
*/
public void connect(UserCredentials uc)
throws DSOutOfServiceException
{
if (uc == null)
throw new NullPointerException("No user credentials.");
String name = (String)
container.getRegistry().lookup(LookupNames.MASTER);
if (CommonsLangUtils.isBlank(name)) {
name = LookupNames.MASTER_INSIGHT;
}
LoginCredentials cred = new LoginCredentials();
cred.getUser().setUsername(uc.getUserName());
cred.getUser().setPassword(uc.getPassword());
cred.getServer().setHostname( uc.getHostName());
cred.getServer().setPort(uc.getPort());
cred.setApplicationName(name);
cred.setCheckNetwork(true);
cred.setCompression(determineCompression(uc.getSpeedLevel()));
cred.setEncryption(uc.isEncrypted());
ExperimenterData exp = omeroGateway.connect(cred);
//check client server version
compatible = true;
//Register into log file.
Object v = container.getRegistry().lookup(LookupNames.VERSION);
String clientVersion = "";
if (v != null && v instanceof String)
clientVersion = (String) v;
if (uc.getUserName().equals(omeroGateway.getSessionId(exp))) {
container.getRegistry().bind(LookupNames.SESSION_KEY, Boolean.TRUE);
}
//Check if client and server are compatible.
String version = omeroGateway.getServerVersion();
Boolean check = checkClientServerCompatibility(version, clientVersion);
if (check == null) {
compatible = false;
omeroGateway.logout();
return;
}
if (!check.booleanValue()) {
compatible = false;
notifyIncompatibility(clientVersion, version, uc.getHostName());
omeroGateway.logout();
return;
}
//Upgrade check only if client and server are compatible
omeroGateway.isUpgradeRequired(name);
//Post an event to indicate that the user is connected.
EventBus bus = container.getRegistry().getEventBus();
bus.post(new ConnectedEvent());
//Post an event to notify
compatible = true;
//Register into log file.
Map<String, String> info = ProxyUtil.collectOsInfoAndJavaVersion();
LogMessage msg = new LogMessage();
msg.println("Server version: "+version);
msg.println("Client version: "+clientVersion);
Entry<String, String> entry;
Iterator<Entry<String, String>> k = info.entrySet().iterator();
while (k.hasNext()) {
entry = k.next();
msg.println(entry.getKey()+": "+entry.getValue());
}
registry.getLogger().info(this, msg);
registry.bind(LookupNames.CURRENT_USER_DETAILS, exp);
registry.bind(LookupNames.IMAGE_QUALITY_LEVEL,
determineImageQuality(uc.getSpeedLevel()));
try {
// Load the omero client properties from the server
List agents = (List) registry.lookup(LookupNames.AGENTS);
Map<String, String> props = omeroGateway.getOmeroClientProperties(exp.getGroupId());
for (String key : props.keySet()) {
if (registry.lookup(key) == null)
registry.bind(key, props.get(key));
Registry agentReg;
for (Object agent : agents) {
agentReg = ((AgentInfo) agent).getRegistry();
if (agentReg != null && agentReg.lookup(key) == null)
agentReg.bind(key, props.get(key));
}
}
} catch (DSAccessException e1) {
registry.getLogger().warn(this, "Could not load omero client properties from the server");
}
Collection<GroupData> groups;
Set<GroupData> available;
List<ExperimenterData> exps = new ArrayList<ExperimenterData>();
String ldap = null;
try {
GroupData defaultGroup = null;
long gid = exp.getDefaultGroup().getId();
SecurityContext ctx = new SecurityContext(gid);
groups = omeroGateway.getAvailableGroups(ctx, exp);
registry.bind(LookupNames.SYSTEM_ROLES,
omeroGateway.getSystemRoles(ctx));
//Check if the current experimenter is an administrator
Iterator<GroupData> i = groups.iterator();
GroupData g;
available = new HashSet<GroupData>();
while (i.hasNext()) {
g = i.next();
if (gid == g.getId()) defaultGroup = g;
if (!admin.isSecuritySystemGroup(g.getId())) {
available.add(g);
} else {
if (admin.isSecuritySystemGroup(g.getId(),
GroupData.SYSTEM)) {
available.add(g);
uc.setAdministrator(true);
}
}
}
//to be on the safe side.
if (available.size() == 0) {
//group with loaded users.
if (defaultGroup != null) available.add(defaultGroup);
else available.add(exp.getDefaultGroup());
}
registry.bind(LookupNames.USER_GROUP_DETAILS, available);
List<Long> ids = new ArrayList<Long>();
i = available.iterator();
Set set;
Iterator j;
ExperimenterData e;
while (i.hasNext()) {
g = (GroupData) i.next();
set = g.getExperimenters();
j = set.iterator();
while (j.hasNext()) {
e = (ExperimenterData) j.next();
if (!ids.contains(e.getId())) {
ids.add(e.getId());
exps.add(e);
}
}
}
registry.bind(LookupNames.USERS_DETAILS, exps);
registry.bind(LookupNames.USER_ADMINISTRATOR, uc.isAdministrator());
} catch (DSAccessException e) {
throw new DSOutOfServiceException("Cannot retrieve groups", e);
}
//Bind user details to all agents' registry.
List agents = (List) registry.lookup(LookupNames.AGENTS);
Iterator kk = agents.iterator();
AgentInfo agentInfo;
Registry reg;
Boolean b = (Boolean) registry.lookup(LookupNames.BINARY_AVAILABLE);
String url = (String) registry.lookup(LookupNames.HELP_ON_LINE_SEARCH);
while (kk.hasNext()) {
agentInfo = (AgentInfo) kk.next();
if (agentInfo.isActive()) {
reg = agentInfo.getRegistry();
reg.bind(LookupNames.CURRENT_USER_DETAILS, exp);
reg.bind(LookupNames.USER_GROUP_DETAILS, available);
reg.bind(LookupNames.USERS_DETAILS, exps);
reg.bind(LookupNames.USER_ADMINISTRATOR, uc.isAdministrator());
reg.bind(LookupNames.IMAGE_QUALITY_LEVEL,
determineImageQuality(uc.getSpeedLevel()));
reg.bind(LookupNames.BINARY_AVAILABLE, b);
reg.bind(LookupNames.HELP_ON_LINE_SEARCH, url);
}
}
}
/**
* Tells whether the communication channel to <i>OMEDS</i> is currently
* connected.
* This means that we have established a connection and have successfully
* logged in.
*
* @return <code>true</code> if connected, <code>false</code> otherwise.
*/
public boolean isConnected() { return omeroGateway.isConnected(); }
/**
* Returns <code>true</code> if the client and server are compatible,
* <code>false</code> otherwise.
*
* @return See above.
*/
public boolean isCompatible() { return compatible; }
/**
* Shuts down the connection.
*
* @param ctx The security context.
*/
public void shutdown(SecurityContext ctx)
{
//Need to write the current group.
//if (!omeroGateway.isConnected()) return;
if (omeroGateway != null) omeroGateway.logout();
DataServicesFactory.registry.getCacheService().clearAllCaches();
PixelsServicesFactory.shutDownRenderingControls(container.getRegistry());
singleton = null;
omeroGateway = null;
}
/** Shuts the services down and exits the application.
*
* @param forceQuit Pass <code>true</code> to force i.e. do not check if
* the application can terminate,
* <code>false</code> otherwise.
* @param exit Pass <code>true</code> to quit, <code>false</code> to
* only shut down the services.
*/
public void exitApplication(boolean forceQuit, boolean exit)
{
if (!forceQuit) {
List<AgentInfo> agents = (List<AgentInfo>)
registry.lookup(LookupNames.AGENTS);
Iterator<AgentInfo> i = agents.iterator();
AgentInfo agentInfo;
Agent a;
//Agents termination phase.
i = agents.iterator();
List<AgentInfo> notTerminated = new ArrayList<AgentInfo>();
while (i.hasNext()) {
agentInfo = i.next();
if (agentInfo.isActive()) {
a = agentInfo.getAgent();
if (a.canTerminate()) {
a.terminate();
} else notTerminated.add(agentInfo);
}
}
if (notTerminated.size() > 0) {
i = notTerminated.iterator();
StringBuffer buffer = new StringBuffer();
while (i.hasNext()) {
agentInfo = i.next();
buffer.append(agentInfo.getName());
buffer.append("\n");
}
String message = "The following components " +
"could not be closed safely:\n"+buffer.toString()+"\n" +
"Please check.";
String title = "Exit Application";
Environment env = (Environment) registry.lookup(LookupNames.ENV);
if (env != null && env.isRunAsPlugin())
title = "Exit Plugin";
MessageBox box = new MessageBox(
DataServicesFactory.registry.getTaskBar().getFrame(),
title, message,
IconManager.getInstance().getIcon(
IconManager.INFORMATION_MESSAGE_48));
box.setNoText("OK");
box.setYesText("Force Quit");
box.setSize(400, 250);
if (!env.isRunAsPlugin() &&
box.centerMsgBox() == MessageBox.NO_OPTION)
return;
}
}
shutdown(null);
if (exit) {
CacheServiceFactory.shutdown(container);
container.exit();
}
singleton = null;
}
/**
* Remove the security group.
*
* @param ctx The security context to handle.
* @throws Throwable Thrown if the connector cannot be closed.
*/
public void removeGroup(SecurityContext ctx)
throws Exception
{
omeroGateway.removeGroup(ctx);
}
/**
* Checks if the rendering engines
*/
public void checkServicesStatus()
{
PixelsServicesFactory.checkRenderingControls(container.getRegistry());
}
}