DirectoryAdapter.java
package emissary.server.mvc.adapters;
import emissary.client.EmissaryClient;
import emissary.client.EmissaryResponse;
import emissary.config.ConfigUtil;
import emissary.config.Configurator;
import emissary.core.EmissaryException;
import emissary.directory.DirectoryEntry;
import emissary.directory.DirectoryEntryMap;
import emissary.directory.DirectoryXmlContainer;
import emissary.directory.IRemoteDirectory;
import emissary.directory.KeyManipulator;
import emissary.log.MDCConstants;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.ws.rs.core.MediaType;
import org.apache.hc.client5.http.classic.methods.HttpPost;
import org.apache.hc.client5.http.entity.EntityBuilder;
import org.apache.hc.client5.http.entity.UrlEncodedFormEntity;
import org.apache.hc.core5.http.HttpStatus;
import org.apache.hc.core5.http.NameValuePair;
import org.apache.hc.core5.http.message.BasicClassicHttpResponse;
import org.apache.hc.core5.http.message.BasicNameValuePair;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.slf4j.MDC;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import javax.annotation.Nullable;
/**
* Stuff for adapting the Directory calls to HTTP All the outbound methods supply the TARGET_DIRECTORY parameter that
* matches the machine/port they are going to. This really only required in testing scenarios but is so helpful that it
* seems worth the work.
* <p>
* A few of the outbound methods have no corresponding inbound methods because their answer is supplied by a jsp/worker
* combination without any need to call into the directory on the remote side.
*/
public class DirectoryAdapter extends EmissaryClient {
private static final Logger logger = LoggerFactory.getLogger(DirectoryAdapter.class);
public static final String TARGET_DIRECTORY = "targetDir";
public static final String PROXY_KEY_PARAMETER = "proxy";
public static final String ADD_KEY = "dirAddKey";
public static final String ADD_DESCRIPTION = "dirAddDesc";
public static final String ADD_COST = "dirAddCost";
public static final String ADD_QUALITY = "dirAddQual";
public static final String ADD_PROPAGATION_FLAG = "dirAddPropFlag";
public static final String FAILED_DIRECTORY_NAME = "dirFailName";
public static final String DIRECTORY_NAME = "directoryName";
public static final String ADD_ENTRIES = "dirAddEntries";
public static final String DIRECTORY_KEY = "EMISSARY_DIRECTORY_SERVICES::STUDY";
public static final String FILE_PICKUP_KEY = "INITIAL::INPUT";
// These two parameters will cause each node to only have copies of its own places.
// Greatly speeds up performance when not using the moveTo() functionality.
@SuppressWarnings("NonFinalStaticField")
private static boolean disableAddPlaces = true;
@SuppressWarnings("NonFinalStaticField")
private static boolean filterDirectoryEntryMap = true;
protected static void configure() {
try {
final Configurator c = ConfigUtil.getConfigInfo(DirectoryAdapter.class);
disableAddPlaces = c.findBooleanEntry("DISABLE_ADD_PLACES", true);
filterDirectoryEntryMap = c.findBooleanEntry("FILTER_DIRECTORY_ENTRY_MAP", true);
} catch (IOException e) {
logger.info("Failed to find or read DirectoryAdapter config. Using default values.");
logger.debug(e.toString());
}
}
/**
* Handle the packaging and sending of an addPlaces call to a remote directory. Sends multiple keys on the same place
* with the same cost/quality and description if the description, cost and quality lists are only size 1. Uses a
* distinct description/cost/quality for each key when there are enough values
*
* @param parentDirectory the url portion of the parent directory location
* @param entryList the list of directory entries to add
* @param propagating true if going downstream
* @return status of operation
*/
public EmissaryResponse outboundAddPlaces(final String parentDirectory, final List<DirectoryEntry> entryList, final boolean propagating) {
if (disableAddPlaces) {
BasicClassicHttpResponse response = new BasicClassicHttpResponse(HttpStatus.SC_OK, "Not accepting remote add places");
response.setEntity(EntityBuilder.create().setText("").setContentEncoding(MediaType.TEXT_PLAIN).build());
return new EmissaryResponse(response);
} else {
final String parentDirectoryUrl = KeyManipulator.getServiceHostUrl(parentDirectory);
final HttpPost method = createHttpPost(parentDirectoryUrl, context, "/RegisterPlace.action");
final String parentLoc = KeyManipulator.getServiceLocation(parentDirectory);
// Separate it out into lists
final List<String> keyList = new ArrayList<>();
final List<String> descList = new ArrayList<>();
final List<Integer> costList = new ArrayList<>();
final List<Integer> qualityList = new ArrayList<>();
for (final DirectoryEntry d : entryList) {
keyList.add(d.getKey());
descList.add(d.getDescription());
costList.add(d.getCost());
qualityList.add(d.getQuality());
}
final List<NameValuePair> nvps = new ArrayList<>();
nvps.add(new BasicNameValuePair(TARGET_DIRECTORY, parentLoc));
for (int count = 0; count < keyList.size(); count++) {
nvps.add(new BasicNameValuePair(ADD_KEY + count, keyList.get(count)));
// possibly use the single desc/cost/qual for each key
if (descList.size() > count) {
String desc = descList.get(count);
if (desc == null) {
desc = "No description provided";
}
nvps.add(new BasicNameValuePair(ADD_DESCRIPTION + count, desc));
}
if (costList.size() > count) {
nvps.add(new BasicNameValuePair(ADD_COST + count, costList.get(count).toString()));
}
if (qualityList.size() > count) {
nvps.add(new BasicNameValuePair(ADD_QUALITY + count, qualityList.get(count).toString()));
}
}
nvps.add(new BasicNameValuePair(ADD_PROPAGATION_FLAG, Boolean.toString(propagating)));
method.setEntity(new UrlEncodedFormEntity(nvps, StandardCharsets.UTF_8));
return send(method);
}
}
/**
* Handle the packaging and sending of an removePlaces call to a remote directory
*
* @param directory the url portion of the remote directory location
* @param key list of keys to remove (four-tuples)
* @param propagating true if doing down stream
* @return status of operation
*/
public EmissaryResponse outboundRemovePlaces(final String directory, final List<String> key, final boolean propagating) {
final String directoryUrl = KeyManipulator.getServiceHostUrl(directory);
final HttpPost method = createHttpPost(directoryUrl, context, "/DeregisterPlace.action");
final String parentLoc = KeyManipulator.getServiceLocation(directory);
final List<NameValuePair> nvps = new ArrayList<>();
nvps.add(new BasicNameValuePair(TARGET_DIRECTORY, parentLoc));
int count = 0;
for (String k : key) {
nvps.add(new BasicNameValuePair(ADD_KEY + count++, k));
}
nvps.add(new BasicNameValuePair(ADD_PROPAGATION_FLAG, Boolean.toString(propagating)));
method.setEntity(new UrlEncodedFormEntity(nvps, StandardCharsets.UTF_8));
return send(method);
}
/**
* Handle the packaging and sending of an addPlaces call to a remote directory
*
* @param directory the url portion of the destination directory
* @param failKey key of the directory that failed
* @param permanent true from normal deregistration
* @return status of operation
*/
public EmissaryResponse outboundFailDirectory(final String directory, final String failKey, final boolean permanent) {
final String directoryUrl = KeyManipulator.getServiceHostUrl(directory);
final HttpPost method = createHttpPost(directoryUrl, context, "/FailDirectory.action");
final String parentLoc = KeyManipulator.getServiceLocation(directory);
final List<NameValuePair> nvps = new ArrayList<>();
nvps.add(new BasicNameValuePair(TARGET_DIRECTORY, parentLoc));
nvps.add(new BasicNameValuePair(FAILED_DIRECTORY_NAME, failKey));
nvps.add(new BasicNameValuePair(ADD_PROPAGATION_FLAG, Boolean.toString(permanent)));
method.setEntity(new UrlEncodedFormEntity(nvps, StandardCharsets.UTF_8));
return send(method);
}
/**
* Process the failDirectory call coming remotely over HTTP request params onto the specified (local) directory place.
*
* @param req the inbound request object
*/
public boolean inboundFailDirectory(final HttpServletRequest req) {
final String dir = RequestUtil.getParameter(req, TARGET_DIRECTORY);
final IRemoteDirectory localDirectory = getLocalDirectory(dir);
if (localDirectory == null) {
throw new IllegalArgumentException("No local directory found using name " + dir);
}
final String remoteDir = RequestUtil.getParameter(req, FAILED_DIRECTORY_NAME);
int count;
MDC.put(MDCConstants.SERVICE_LOCATION, KeyManipulator.getServiceLocation(localDirectory.getKey()));
try {
count = localDirectory.irdFailDirectory(remoteDir, RequestUtil.getBooleanParam(req, ADD_PROPAGATION_FLAG));
} finally {
MDC.remove(MDCConstants.SERVICE_LOCATION);
}
logger.debug("Modified {} entries from {} due to failure of remote {}", count, dir, remoteDir);
return true;
}
/**
* Request the XML directory entry markup from a remote directory peer and turn the response XML into a Map of
* String,DirectoryEntryList for return
*
* @param key the key of the remote directory to request the zone transfer from
* @param myKey the key of the local requesting the zone or null if none
* @return DirectoryEntryList map from the remote side
* @throws EmissaryException if remote returns an error
*/
public DirectoryEntryMap outboundZoneTransfer(final String key, final String myKey) throws EmissaryException {
return zoneTransfer(key, myKey, "/TransferDirectory.action");
}
/**
* Request the XML directory entry markup from a remote directory peer and turn the response XML into a
* DirectoryEntryMap for return. Register the caller as a peer of the destination as part of the transfer.
*
* @param key the key of the remote directory to request the zone transfer from
* @param peerKey the key of the peer requesting the zone or null if none
* @return DirectoryEntryList map from the remote side
* @throws EmissaryException if remote returns an error
*/
public DirectoryEntryMap outboundRegisterPeer(final String key, final String peerKey) throws EmissaryException {
return zoneTransfer(key, peerKey, "/RegisterPeer.action");
}
/**
* Request the XML directory entry markup from a remote directory peer and turn the response XML into a Map of
* String,DirectoryEntryList for return.
*
* @param key the key of the remote directory to request the zone transfer from
* @param myKey the key of the local dir requesting the zone or null if none
* @param action the action to use in the request
* @return DirectoryEntryList map from the remote side
* @throws EmissaryException if remote returns an error
*/
private DirectoryEntryMap zoneTransfer(final String key, @Nullable final String myKey, final String action) throws EmissaryException {
final HttpPost method = createHttpPost(KeyManipulator.getServiceHostUrl(key), context, action);
final String parentLoc = KeyManipulator.getServiceLocation(key);
final List<NameValuePair> nvps = new ArrayList<>();
nvps.add(new BasicNameValuePair(TARGET_DIRECTORY, parentLoc));
if (myKey != null) {
nvps.add(new BasicNameValuePair(DIRECTORY_NAME, myKey));
}
method.setEntity(new UrlEncodedFormEntity(nvps, StandardCharsets.UTF_8));
DirectoryEntryMap map = null;
EmissaryResponse ws = null;
try {
ws = send(method);
// TODO Consider putting this method in the response
if (ws.getStatus() != HttpStatus.SC_OK) {
logger.debug("Unable to contact remote directory for zone transfer: {}", ws.getContentString());
} else {
map = DirectoryXmlContainer.buildEntryListMap(ws.getContentString());
}
} catch (Exception ex) {
logger.debug("Unable to contact remote directory for " + "zone transfer " + key, ex);
}
if (map == null) {
String errMsg = "Unable to perform zone transfer to " + key + ": received map is null";
if (ws != null) {
errMsg += ", isError=" + ws.getStatus() + ", msgBody=" + ws.getContentString();
}
throw new EmissaryException(errMsg);
}
// This ensures each node only has knowledge of all DirectoryPlace and FilePickupPlace entries
if (filterDirectoryEntryMap) {
return filterDirectoryEntryMap(map);
} else {
return map;
}
}
private static DirectoryEntryMap filterDirectoryEntryMap(DirectoryEntryMap map) {
DirectoryEntryMap filtered = new DirectoryEntryMap();
if (map.containsKey(DIRECTORY_KEY)) {
filtered.put(DIRECTORY_KEY, map.get(DIRECTORY_KEY));
}
if (map.containsKey(FILE_PICKUP_KEY)) {
filtered.put(FILE_PICKUP_KEY, map.get(FILE_PICKUP_KEY));
}
return filtered;
}
/**
* Look up the local directory using one of two methods. The easier method almost always works, the case where it
* doesn't in when there are multiple configured Emissary nodes on the same local JVM through a single jetty with
* multiple Listeners. This is a testing scenario, but it is helpful to keep supporting it, so we have good test
* coverage.
*
* @param name name of the local directory or null for default
*/
private static IRemoteDirectory getLocalDirectory(final String name) {
return new IRemoteDirectory.Lookup().getLocalDirectory(name);
}
}