package org.dcache.cells; import java.io.Serializable; import java.util.concurrent.Executor; import diskCacheV111.util.CacheException; import diskCacheV111.vehicles.Message; import dmg.cells.nucleus.CellAdapter; import dmg.cells.nucleus.CellAddressCore; import dmg.cells.nucleus.CellEndpoint; import dmg.cells.nucleus.CellMessage; import dmg.cells.nucleus.CellMessageReceiver; import dmg.cells.nucleus.Reply; import dmg.cells.nucleus.UOID; import org.dcache.util.Args; import org.dcache.util.Option; import org.dcache.util.OptionParser; /** * Abstract cell implementation providing features needed by many * dCache cells. * * <h2>Automatic dispatch of dCache messages to message handler</h2> * * See org.dcache.util.CellMessageDispatcher for details. * * <h2>Option parsing</h2> * * AbstractCell supports automatic option parsing based on annotations * of fields. A field is annotated with the Option annotation. The * annotation supports the following attributes: * * <dl> * <dt>name</dt> * <dd>The name of the option.</dd> * * <dt>description</dt> * <dd>A one line description of the option.</dd> * * <dt>defaultValue</dt> * <dd>The default value if the option is not specified, * specified as a string.</dd> * * <dt>unit</dt> * <dd>The unit of the value, if any, e.g. seconds.</dd> * * <dt>required</dt> * <dd>Whether this is a mandatory option. Defaults to false.</dd> * * <dt>log</dt> * <dd>Whether to log the value of the option during startup. * Defaults to true, but should be disabled for sensitive * information.</dd> * </dl> * * Options are automatically converted to the type of the field. In * case of non-POD fields, the class must have a one-argument * constructor taking a String. The File class is an example of such a * class. * * By defaults options are logged at the info level. The description * and unit should be formulated in such a way that the a message can * be formed as "<description> set to <value> <unit>". * * In case a required option is missing, an IllegalArgumentException * is thrown during option parsing. * * It is important that fields used for storing options do not have an * initializer. An initializer would overwrite the value retrieved * from the option. Empty Strings will become null. * * Example code: * * <code> * @Option( * name = "maxPinDuration", * description = "Max. lifetime of a pin", * defaultValue = "86400000", // one day * unit = "ms" * ) * protected long _maxPinDuration; * * @see org.dcache.cells.CellMessageDispatcher */ public class AbstractCell extends CellAdapter implements CellMessageReceiver { private static final String MSG_UOID_MISMATCH = "A reply [%s] was generated by a message listener, but the " + "message UOID indicates that another message listener has " + "already replied to the message."; private static final String MSG_ALREADY_FORWARDED = "A result [%s] was generated by a message listener, but the " + "message was already forwarded and thus cannot can be sent."; @Option( name = "monitor", description = "Cell message monitoring", defaultValue = "false" ) protected boolean _isMonitoringEnabled; @Option( name = "cellClass", description = "Cell classification" ) protected String _cellClass; private final OptionParser _optionParser; /** * Helper object used to dispatch messages to message listeners. */ protected final CellMessageDispatcher _messageDispatcher = new CellMessageDispatcher("messageArrived"); /** * Helper object used to dispatch messages to forward to message * listeners. */ protected final CellMessageDispatcher _forwardDispatcher = new CellMessageDispatcher("messageToForward"); protected MessageProcessingMonitor _monitor; /** * Returns the cell type specified as option 'cellType', or * "Generic" if the option was not given. */ private static String getCellType(Args args) { String type = args.getOpt("cellType"); return (type == null) ? "Generic" : type; } public AbstractCell(String cellName, String arguments) { this(cellName, new Args(arguments), null); } public AbstractCell(String cellName, String arguments, Executor executor) { this(cellName, new Args(arguments), executor); } public AbstractCell(String cellName, Args arguments) { this(cellName, getCellType(arguments), arguments, null); } public AbstractCell(String cellName, Args arguments, Executor executor) { this(cellName, getCellType(arguments), arguments, executor); } /** * Constructs an AbstractCell. * * @param cellName the name of the cell * @param cellType the type of the cell * @param arguments the cell arguments */ public AbstractCell(String cellName, String cellType, Args arguments) { this(cellName, cellType, arguments, null); } public AbstractCell(String cellName, String cellType, Args arguments, Executor executor) { super(cellName, cellType, arguments, executor); _optionParser = new OptionParser(arguments); } @Override protected void starting() throws Exception { _optionParser.inject(AbstractCell.this); _monitor = new MessageProcessingMonitor(); _monitor.setCellEndpoint(AbstractCell.this); _monitor.setEnabled(_isMonitoringEnabled); if (_cellClass != null) { getNucleus().setCellClass(_cellClass); } addMessageListener(AbstractCell.this); addCommandListener(_monitor); } /** * Adds a listener for dCache messages. * * @see CellMessageDispatcher#addMessageListener */ public void addMessageListener(CellMessageReceiver o) { _messageDispatcher.addMessageListener(o); _forwardDispatcher.addMessageListener(o); } /** * Removes a listener previously added with addMessageListener. */ public void removeMessageListener(CellMessageReceiver o) { _messageDispatcher.removeMessageListener(o); _forwardDispatcher.removeMessageListener(o); } /** * Delivers message to registered forward listeners. * * A reply is delivered back to the client if any message * listener: * * - Returns a value. * * - Throws a declared exception, IllegalStateException or * IllegalArgumentException. * * dCache vehicles (subclasses of Message) are recognized, and * a reply is only sent if requested by the client. * * For dCache vehicles, errors are reported by sending back the * vehicle with an error code. CacheException and IllegalArgumentException * are recognised and an appropriate error code is used. If the message * is already flagged as having failed, that error is not replaced. * * Return values implementing Reply are recognized and the reply * is delivered by calling the deliver method on the Reply object. * It is the responsibility of the Reply object to decide whether * to forward or return the message. * * If no listener returns a value or throws an exception, then the message is * forwarded to the next destination. */ @Override public void messageToForward(CellMessage envelope) { CellEndpoint endpoint = _monitor.getReplyCellEndpoint(envelope); UOID uoid = envelope.getUOID(); CellAddressCore address = envelope.getSourcePath().getDestinationAddress(); boolean isReply = isReply(envelope); Object result = _forwardDispatcher.call(envelope); if (result != null) { if (!uoid.equals(envelope.getUOID())) { throw new RuntimeException(String.format(MSG_UOID_MISMATCH, result)); } if (!address.equals(envelope.getSourcePath().getDestinationAddress())) { throw new RuntimeException(String.format(MSG_ALREADY_FORWARDED, result)); } Serializable o = envelope.getMessageObject(); if (result instanceof Reply) { Reply reply = (Reply) result; reply.deliver(endpoint, envelope); } else { if (o instanceof Message) { Message msg = (Message) o; /* Don't bother replying if requester isn't interested. */ if (!msg.getReplyRequired() && !isReply) { return; } /* dCache vehicles can transport errors back to the * requester, so detect if this is an error reply. */ if (result instanceof CacheException) { CacheException e = (CacheException)result; msg.setFailedConditionally(e.getRc(), e.getMessage()); result = msg; } else if (result instanceof IllegalArgumentException) { msg.setFailedConditionally(CacheException.INVALID_ARGS, result.toString()); result = msg; } else if (result instanceof Exception) { msg.setFailedConditionally(CacheException.UNEXPECTED_SYSTEM_EXCEPTION, (Exception) result); result = msg; } } if (!isReply) { envelope.revertDirection(); } envelope.setMessageObject((Serializable) result); endpoint.sendMessage(envelope); } } else { endpoint.sendMessage(envelope); } } private boolean isReply(CellMessage envelope) { Object message = envelope.getMessageObject(); return (message instanceof Message) && ((Message) message).isReply(); } /** * Delivers messages to registered message listeners. * * A reply is delivered back to the client if any message * listener: * * - Returns a value * * - Throws a checked exception, IllegalStateException or * IllegalArgumentException. * * dCache vehicles (subclasses of Message) are recognized, and * a reply is only sent if requested by the client. * * For dCache vehicles, errors are reported by sending back the * vehicle with an error code. CacheException and * IllegalArgumentException are recognised and an appropriate * error code is used. * * Return values implementing Reply are recognized and the reply * is delivered by calling the deliver method on the Reply object. */ @Override public void messageArrived(CellMessage envelope) { CellEndpoint endpoint = _monitor.getReplyCellEndpoint(envelope); UOID uoid = envelope.getUOID(); boolean isReply = isReply(envelope); Object result = _messageDispatcher.call(envelope); if (result != null && !isReply) { if (!uoid.equals(envelope.getUOID())) { throw new RuntimeException(String.format(MSG_UOID_MISMATCH, result)); } Serializable o = envelope.getMessageObject(); if (result instanceof Reply) { Reply reply = (Reply)result; reply.deliver(endpoint, envelope); } else { if (o instanceof Message) { Message msg = (Message)o; /* Don't send reply if not requested. Some vehicles * contain a bug in which the message is marked as not * requiring a reply, while what was intended was * asynchronous processing on the server side. Therefore * we have a special test for Reply results. */ if (!msg.getReplyRequired()) { return; } /* dCache vehicles can transport errors back to the * requester, so detect if this is an error reply. */ if (result instanceof CacheException) { CacheException e = (CacheException)result; msg.setFailed(e.getRc(), e.getMessage()); result = msg; } else if (result instanceof IllegalArgumentException) { msg.setFailed(CacheException.INVALID_ARGS, result.toString()); result = msg; } else if (result instanceof Exception) { msg.setFailed(CacheException.UNEXPECTED_SYSTEM_EXCEPTION, (Exception) result); result = msg; } } envelope.revertDirection(); envelope.setMessageObject((Serializable) result); endpoint.sendMessage(envelope); } } } }