Java tutorial
/* $Id: LivelinkConnector.java 996524 2010-09-13 13:38:01Z kwright $ */ /** * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF licenses this file to You 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 org.apache.manifoldcf.crawler.connectors.livelink; import org.apache.manifoldcf.core.interfaces.*; import org.apache.manifoldcf.agents.interfaces.*; import org.apache.manifoldcf.crawler.interfaces.*; import org.apache.manifoldcf.crawler.system.Logging; import org.apache.manifoldcf.crawler.system.ManifoldCF; import org.apache.manifoldcf.connectorcommon.interfaces.*; import org.apache.manifoldcf.connectorcommon.common.XThreadInputStream; import org.apache.manifoldcf.connectorcommon.common.XThreadOutputStream; import org.apache.manifoldcf.connectorcommon.common.InterruptibleSocketFactory; import org.apache.manifoldcf.core.common.DateParser; import org.apache.manifoldcf.livelink.*; import java.io.*; import java.util.*; import java.net.*; import java.util.concurrent.TimeUnit; import com.opentext.api.*; import org.apache.http.conn.HttpClientConnectionManager; import org.apache.http.client.HttpClient; import org.apache.http.impl.conn.PoolingHttpClientConnectionManager; import org.apache.http.impl.client.HttpClientBuilder; import org.apache.http.protocol.HttpRequestExecutor; import org.apache.http.impl.client.HttpClients; import org.apache.http.client.config.RequestConfig; import org.apache.http.client.CredentialsProvider; import org.apache.http.impl.client.BasicCredentialsProvider; import org.apache.http.config.SocketConfig; import org.apache.http.client.HttpClient; import org.apache.http.client.methods.HttpGet; import org.apache.http.client.methods.HttpPost; import org.apache.http.NameValuePair; import org.apache.http.conn.ssl.SSLConnectionSocketFactory; import org.apache.http.conn.ssl.BrowserCompatHostnameVerifier; import org.apache.http.HttpEntity; import org.apache.http.HttpResponse; import org.apache.http.auth.AuthScope; import org.apache.http.auth.NTCredentials; import org.apache.http.client.methods.HttpGet; import org.apache.http.client.methods.HttpRequestBase; import org.apache.http.impl.client.DefaultRedirectStrategy; import org.apache.http.util.EntityUtils; import org.apache.http.HttpStatus; import org.apache.http.HttpHost; import org.apache.http.Header; import org.apache.http.message.BasicHeader; import org.apache.http.message.BasicNameValuePair; import org.apache.http.protocol.HTTP; import org.apache.http.client.entity.UrlEncodedFormEntity; import org.apache.http.client.HttpRequestRetryHandler; import org.apache.http.protocol.HttpContext; import org.apache.http.conn.ConnectTimeoutException; import org.apache.http.client.RedirectException; import org.apache.http.client.CircularRedirectException; import org.apache.http.NoHttpResponseException; import org.apache.http.HttpException; /** This is the Livelink implementation of the IRepositoryConnectr interface. * The original Volant code forced there to be one livelink session per JVM, with * lots of buggy synchronization present to try to enforce this. This implementation * is multi-session. However, since it is possible that the Volant restriction was * indeed needed, I have attempted to structure things to allow me to turn on * single-session if needed. * * For livelink, the document identifiers are the object identifiers. * */ public class LivelinkConnector extends org.apache.manifoldcf.crawler.connectors.BaseRepositoryConnector { public static final String _rcsid = "@(#)$Id: LivelinkConnector.java 996524 2010-09-13 13:38:01Z kwright $"; // Activities we will report on private final static String ACTIVITY_SEED = "find documents"; private final static String ACTIVITY_FETCH = "fetch document"; /** Deny access token for default authority */ private final static String defaultAuthorityDenyToken = GLOBAL_DENY_TOKEN; // A couple of very important points. // First, the canonical document identifier has the following form: // <D|F>[<volume_id>:]<object_id> // Second, the only LEGAL objects for a document identifier to describe // are folders, documents, and volume objects. Project objects are NOT // allowed; they must be mapped to the appropriate volume object before // being returned to the crawler. // Metadata names for general metadata fields protected final static String GENERAL_NAME_FIELD = "general_name"; protected final static String GENERAL_DESCRIPTION_FIELD = "general_description"; protected final static String GENERAL_CREATIONDATE_FIELD = "general_creationdate"; protected final static String GENERAL_MODIFYDATE_FIELD = "general_modifydate"; protected final static String GENERAL_OWNER = "general_owner"; protected final static String GENERAL_CREATOR = "general_creator"; protected final static String GENERAL_MODIFIER = "general_modifier"; protected final static String GENERAL_PARENTID = "general_parentid"; // Signal that we have set up connection parameters properly private boolean hasSessionParameters = false; // Signal that we have set up a connection properly private boolean hasConnected = false; // Session expiration time private long expirationTime = -1L; // Idle session expiration interval private final static long expirationInterval = 300000L; // Data required for maintaining livelink connection private LAPI_DOCUMENTS LLDocs = null; private LAPI_ATTRIBUTES LLAttributes = null; private LAPI_USERS LLUsers = null; private LLSERVER llServer = null; private int LLENTWK_VOL; private int LLENTWK_ID; private int LLCATWK_VOL; private int LLCATWK_ID; // Parameter values we need private String serverProtocol = null; private String serverName = null; private int serverPort = -1; private String serverUsername = null; private String serverPassword = null; private String serverHTTPCgi = null; private String serverHTTPNTLMDomain = null; private String serverHTTPNTLMUsername = null; private String serverHTTPNTLMPassword = null; private IKeystoreManager serverHTTPSKeystore = null; private String ingestProtocol = null; private String ingestPort = null; private String ingestCgiPath = null; private String viewProtocol = null; private String viewServerName = null; private String viewPort = null; private String viewCgiPath = null; private String ingestNtlmDomain = null; private String ingestNtlmUsername = null; private String ingestNtlmPassword = null; // SSL support for ingestion private IKeystoreManager ingestKeystoreManager = null; // Connection management private HttpClientConnectionManager connectionManager = null; private HttpClient httpClient = null; // Base path for viewing private String viewBasePath = null; // Ingestion port number private int ingestPortNumber = -1; // Activities list private static final String[] activitiesList = new String[] { ACTIVITY_SEED, ACTIVITY_FETCH }; // Retry count. This is so we can try to install some measure of sanity into situations where LAPI gets confused communicating to the server. // So, for some kinds of errors, we just retry for a while hoping it will go away. private static final int FAILURE_RETRY_COUNT = 10; // Current host name private static String currentHost = null; private static java.net.InetAddress currentAddr = null; static { // Find the current host name try { currentAddr = java.net.InetAddress.getLocalHost(); // Get hostname currentHost = currentAddr.getHostName(); } catch (UnknownHostException e) { } } /** Constructor. */ public LivelinkConnector() { } /** Tell the world what model this connector uses for getDocumentIdentifiers(). * This must return a model value as specified above. *@return the model type value. */ @Override public int getConnectorModel() { // Livelink is a chained hierarchy model return MODEL_CHAINED_ADD_CHANGE; } /** Connect. The configuration parameters are included. *@param configParams are the configuration parameters for this connection. */ @Override public void connect(ConfigParams configParams) { super.connect(configParams); // This is required by getBins() serverName = params.getParameter(LiveLinkParameters.serverName); } protected class GetSessionThread extends Thread { protected Throwable exception = null; public GetSessionThread() { super(); setDaemon(true); } public void run() { try { // Create the session llServer = new LLSERVER(!serverProtocol.equals("internal"), serverProtocol.equals("https"), serverName, serverPort, serverUsername, serverPassword, serverHTTPCgi, serverHTTPNTLMDomain, serverHTTPNTLMUsername, serverHTTPNTLMPassword, serverHTTPSKeystore); LLDocs = new LAPI_DOCUMENTS(llServer.getLLSession()); LLAttributes = new LAPI_ATTRIBUTES(llServer.getLLSession()); LLUsers = new LAPI_USERS(llServer.getLLSession()); if (Logging.connectors.isDebugEnabled()) { String passwordExists = (serverPassword != null && serverPassword.length() > 0) ? "password exists" : ""; Logging.connectors.debug("Livelink: Livelink Session: Server='" + serverName + "'; port='" + serverPort + "'; user name='" + serverUsername + "'; " + passwordExists); } LLValue entinfo = new LLValue().setAssoc(); int status; status = LLDocs.AccessEnterpriseWS(entinfo); if (status == 0) { LLENTWK_ID = entinfo.toInteger("ID"); LLENTWK_VOL = entinfo.toInteger("VolumeID"); } else throw new ManifoldCFException("Error accessing enterprise workspace: " + status); entinfo = new LLValue().setAssoc(); status = LLDocs.AccessCategoryWS(entinfo); if (status == 0) { LLCATWK_ID = entinfo.toInteger("ID"); LLCATWK_VOL = entinfo.toInteger("VolumeID"); } else throw new ManifoldCFException("Error accessing category workspace: " + status); } catch (Throwable e) { this.exception = e; } } public void finishUp() throws ManifoldCFException, InterruptedException { join(); Throwable thr = exception; if (thr != null) { if (thr instanceof RuntimeException) throw (RuntimeException) thr; else if (thr instanceof ManifoldCFException) throw (ManifoldCFException) thr; else if (thr instanceof Error) throw (Error) thr; else throw new RuntimeException( "Unrecognized exception type: " + thr.getClass().getName() + ": " + thr.getMessage(), thr); } } } /** Get the bin name string for a document identifier. The bin name describes the queue to which the * document will be assigned for throttling purposes. Throttling controls the rate at which items in a * given queue are fetched; it does not say anything about the overall fetch rate, which may operate on * multiple queues or bins. * For example, if you implement a web crawler, a good choice of bin name would be the server name, since * that is likely to correspond to a real resource that will need real throttle protection. *@param documentIdentifier is the document identifier. *@return the bin name. */ @Override public String[] getBinNames(String documentIdentifier) { // This should return server name return new String[] { serverName }; } protected HttpHost getHost() { return new HttpHost(llServer.getHost(), ingestPortNumber, ingestProtocol); } protected void getSessionParameters() throws ManifoldCFException { if (hasSessionParameters == false) { // Do the initial setup part (what used to be part of connect() itself) // Get the parameters ingestProtocol = params.getParameter(LiveLinkParameters.ingestProtocol); ingestPort = params.getParameter(LiveLinkParameters.ingestPort); ingestCgiPath = params.getParameter(LiveLinkParameters.ingestCgiPath); viewProtocol = params.getParameter(LiveLinkParameters.viewProtocol); viewServerName = params.getParameter(LiveLinkParameters.viewServerName); viewPort = params.getParameter(LiveLinkParameters.viewPort); viewCgiPath = params.getParameter(LiveLinkParameters.viewCgiPath); ingestNtlmDomain = params.getParameter(LiveLinkParameters.ingestNtlmDomain); ingestNtlmUsername = params.getParameter(LiveLinkParameters.ingestNtlmUsername); ingestNtlmPassword = params.getObfuscatedParameter(LiveLinkParameters.ingestNtlmPassword); serverProtocol = params.getParameter(LiveLinkParameters.serverProtocol); String serverPortString = params.getParameter(LiveLinkParameters.serverPort); serverUsername = params.getParameter(LiveLinkParameters.serverUsername); serverPassword = params.getObfuscatedParameter(LiveLinkParameters.serverPassword); serverHTTPCgi = params.getParameter(LiveLinkParameters.serverHTTPCgiPath); serverHTTPNTLMDomain = params.getParameter(LiveLinkParameters.serverHTTPNTLMDomain); serverHTTPNTLMUsername = params.getParameter(LiveLinkParameters.serverHTTPNTLMUsername); serverHTTPNTLMPassword = params.getObfuscatedParameter(LiveLinkParameters.serverHTTPNTLMPassword); if (ingestProtocol == null || ingestProtocol.length() == 0) ingestProtocol = null; if (viewProtocol == null || viewProtocol.length() == 0) { if (ingestProtocol == null) viewProtocol = "http"; else viewProtocol = ingestProtocol; } if (ingestPort == null || ingestPort.length() == 0) { if (ingestProtocol != null) { if (!ingestProtocol.equals("https")) ingestPort = "80"; else ingestPort = "443"; } else ingestPort = null; } if (viewPort == null || viewPort.length() == 0) { if (ingestProtocol == null || !viewProtocol.equals(ingestProtocol)) { if (!viewProtocol.equals("https")) viewPort = "80"; else viewPort = "443"; } else viewPort = ingestPort; } if (ingestPort != null) { try { ingestPortNumber = Integer.parseInt(ingestPort); } catch (NumberFormatException e) { throw new ManifoldCFException("Bad ingest port: " + e.getMessage(), e); } } String viewPortString; try { int portNumber = Integer.parseInt(viewPort); viewPortString = ":" + Integer.toString(portNumber); if (!viewProtocol.equals("https")) { if (portNumber == 80) viewPortString = ""; } else { if (portNumber == 443) viewPortString = ""; } } catch (NumberFormatException e) { throw new ManifoldCFException("Bad view port: " + e.getMessage(), e); } if (viewCgiPath == null || viewCgiPath.length() == 0) viewCgiPath = ingestCgiPath; if (ingestNtlmDomain != null && ingestNtlmDomain.length() == 0) ingestNtlmDomain = null; if (ingestNtlmDomain == null) { ingestNtlmUsername = null; ingestNtlmPassword = null; } else { if (ingestNtlmUsername == null || ingestNtlmUsername.length() == 0) { ingestNtlmUsername = serverUsername; if (ingestNtlmPassword == null || ingestNtlmPassword.length() == 0) ingestNtlmPassword = serverPassword; } else { if (ingestNtlmPassword == null) ingestNtlmPassword = ""; } } // Set up ingest ssl if indicated String ingestKeystoreData = params.getParameter(LiveLinkParameters.ingestKeystore); if (ingestKeystoreData != null) ingestKeystoreManager = KeystoreManagerFactory.make("", ingestKeystoreData); // Server parameter processing if (serverProtocol == null || serverProtocol.length() == 0) serverProtocol = "internal"; if (serverPortString == null) serverPort = 2099; else serverPort = new Integer(serverPortString).intValue(); if (serverHTTPNTLMDomain != null && serverHTTPNTLMDomain.length() == 0) serverHTTPNTLMDomain = null; if (serverHTTPNTLMUsername == null || serverHTTPNTLMUsername.length() == 0) { serverHTTPNTLMUsername = null; serverHTTPNTLMPassword = null; } // Set up server ssl if indicated String serverHTTPSKeystoreData = params.getParameter(LiveLinkParameters.serverHTTPSKeystore); if (serverHTTPSKeystoreData != null) serverHTTPSKeystore = KeystoreManagerFactory.make("", serverHTTPSKeystoreData); // View parameters if (viewServerName == null || viewServerName.length() == 0) viewServerName = serverName; viewBasePath = viewProtocol + "://" + viewServerName + viewPortString + viewCgiPath; hasSessionParameters = true; } } protected void getSession() throws ManifoldCFException, ServiceInterruption { getSessionParameters(); if (hasConnected == false) { int socketTimeout = 900000; int connectionTimeout = 300000; // Set up connection manager connectionManager = new PoolingHttpClientConnectionManager(); CredentialsProvider credentialsProvider = new BasicCredentialsProvider(); // Set up ingest ssl if indicated SSLConnectionSocketFactory myFactory = null; if (ingestKeystoreManager != null) { myFactory = new SSLConnectionSocketFactory( new InterruptibleSocketFactory(ingestKeystoreManager.getSecureSocketFactory(), connectionTimeout), new BrowserCompatHostnameVerifier()); } // Set up authentication to use if (ingestNtlmDomain != null) { credentialsProvider.setCredentials(AuthScope.ANY, new NTCredentials(ingestNtlmUsername, ingestNtlmPassword, currentHost, ingestNtlmDomain)); } HttpClientBuilder builder = HttpClients.custom().setConnectionManager(connectionManager) .setMaxConnTotal(1).disableAutomaticRetries() .setDefaultRequestConfig(RequestConfig.custom().setCircularRedirectsAllowed(true) .setSocketTimeout(socketTimeout).setStaleConnectionCheckEnabled(true) .setExpectContinueEnabled(true).setConnectTimeout(connectionTimeout) .setConnectionRequestTimeout(socketTimeout).build()) .setDefaultSocketConfig( SocketConfig.custom().setTcpNoDelay(true).setSoTimeout(socketTimeout).build()) .setDefaultCredentialsProvider(credentialsProvider) .setRequestExecutor(new HttpRequestExecutor(socketTimeout)) .setRedirectStrategy(new DefaultRedirectStrategy()); if (myFactory != null) builder.setSSLSocketFactory(myFactory); httpClient = builder.build(); // System.out.println("Connection server object = "+llServer.toString()); // Establish the actual connection int sanityRetryCount = FAILURE_RETRY_COUNT; while (true) { GetSessionThread t = new GetSessionThread(); try { t.start(); t.finishUp(); hasConnected = true; break; } catch (InterruptedException e) { t.interrupt(); throw new ManifoldCFException("Interrupted: " + e.getMessage(), e, ManifoldCFException.INTERRUPTED); } catch (RuntimeException e2) { sanityRetryCount = handleLivelinkRuntimeException(e2, sanityRetryCount, true); } } } expirationTime = System.currentTimeMillis() + expirationInterval; } // All methods below this line will ONLY be called if a connect() call succeeded // on this instance! protected static int executeMethodViaThread(HttpClient client, HttpRequestBase executeMethod) throws InterruptedException, HttpException, IOException { ExecuteMethodThread t = new ExecuteMethodThread(client, executeMethod); t.start(); try { return t.getResponseCode(); } catch (InterruptedException e) { t.interrupt(); throw e; } finally { t.abort(); t.finishUp(); } } /** Check status of connection. */ @Override public String check() throws ManifoldCFException { try { // Destroy saved session setup and repeat it hasConnected = false; getSession(); // Now, set up trial of ingestion connection if (ingestProtocol != null) { String contextMsg = "for document access"; String ingestHttpAddress = ingestCgiPath; HttpClient client = getInitializedClient(contextMsg); HttpGet method = new HttpGet(getHost().toURI() + ingestHttpAddress); method.setHeader(new BasicHeader("Accept", "*/*")); try { int statusCode = executeMethodViaThread(client, method); switch (statusCode) { case 502: return "Fetch test had transient 502 error response"; case HttpStatus.SC_UNAUTHORIZED: return "Fetch test returned UNAUTHORIZED (401) response; check the security credentials and configuration"; case HttpStatus.SC_OK: return super.check(); default: return "Fetch test returned an unexpected response code of " + Integer.toString(statusCode); } } catch (InterruptedException e) { throw new ManifoldCFException("Interrupted: " + e.getMessage(), e, ManifoldCFException.INTERRUPTED); } catch (java.net.SocketTimeoutException e) { return "Fetch test timed out reading from the Livelink HTTP Server: " + e.getMessage(); } catch (java.net.SocketException e) { return "Fetch test received a socket error reading from Livelink HTTP Server: " + e.getMessage(); } catch (javax.net.ssl.SSLHandshakeException e) { return "Fetch test was unable to set up a SSL connection to Livelink HTTP Server: " + e.getMessage(); } catch (ConnectTimeoutException e) { return "Fetch test connection timed out reading from Livelink HTTP Server: " + e.getMessage(); } catch (InterruptedIOException e) { throw new ManifoldCFException("Interrupted: " + e.getMessage(), e, ManifoldCFException.INTERRUPTED); } catch (HttpException e) { return "Fetch test had an HTTP exception: " + e.getMessage(); } catch (IOException e) { return "Fetch test had an IO failure: " + e.getMessage(); } } else return super.check(); } catch (ServiceInterruption e) { return "Transient error: " + e.getMessage(); } catch (ManifoldCFException e) { if (e.getErrorCode() == ManifoldCFException.INTERRUPTED) throw e; return "Error: " + e.getMessage(); } } /** This method is periodically called for all connectors that are connected but not * in active use. */ @Override public void poll() throws ManifoldCFException { if (!hasConnected) return; long currentTime = System.currentTimeMillis(); if (currentTime >= expirationTime) { hasConnected = false; expirationTime = -1L; // Shutdown livelink connection if (llServer != null) { llServer.disconnect(); llServer = null; } // Shutdown pool if (connectionManager != null) { connectionManager.shutdown(); connectionManager = null; } } } /** This method is called to assess whether to count this connector instance should * actually be counted as being connected. *@return true if the connector instance is actually connected. */ @Override public boolean isConnected() { return hasConnected; } /** Close the connection. Call this before discarding the repository connector. */ @Override public void disconnect() throws ManifoldCFException { hasSessionParameters = false; hasConnected = false; expirationTime = -1L; if (llServer != null) { llServer.disconnect(); llServer = null; } LLDocs = null; LLAttributes = null; ingestKeystoreManager = null; ingestPortNumber = -1; serverProtocol = null; serverName = null; serverPort = -1; serverUsername = null; serverPassword = null; serverHTTPCgi = null; serverHTTPNTLMDomain = null; serverHTTPNTLMUsername = null; serverHTTPNTLMPassword = null; serverHTTPSKeystore = null; ingestPort = null; ingestProtocol = null; ingestCgiPath = null; viewPort = null; viewServerName = null; viewProtocol = null; viewCgiPath = null; viewBasePath = null; ingestNtlmDomain = null; ingestNtlmUsername = null; ingestNtlmPassword = null; if (connectionManager != null) { connectionManager.shutdown(); connectionManager = null; } super.disconnect(); } /** List the activities we might report on. */ @Override public String[] getActivitiesList() { return activitiesList; } /** Convert a document identifier to a relative URI to read data from. This is not the search URI; that's constructed * by a different method. *@param documentIdentifier is the document identifier. *@return the relative document uri. */ protected String convertToIngestURI(String documentIdentifier) throws ManifoldCFException { // The document identifier is the string form of the object ID for this connector. if (!documentIdentifier.startsWith("D")) return null; int colonPosition = documentIdentifier.indexOf(":", 1); if (colonPosition == -1) return ingestCgiPath + "?func=ll&objID=" + documentIdentifier.substring(1) + "&objAction=download"; else return ingestCgiPath + "?func=ll&objID=" + documentIdentifier.substring(colonPosition + 1) + "&objAction=download"; } /** Convert a document identifier to a URI to view. The URI is the URI that will be the unique key from * the search index, and will be presented to the user as part of the search results. It must therefore * be a unique way of describing the document. *@param documentIdentifier is the document identifier. *@return the document uri. */ protected String convertToViewURI(String documentIdentifier) throws ManifoldCFException { // The document identifier is the string form of the object ID for this connector. if (!documentIdentifier.startsWith("D")) return null; int colonPosition = documentIdentifier.indexOf(":", 1); if (colonPosition == -1) return viewBasePath + "?func=ll&objID=" + documentIdentifier.substring(1) + "&objAction=download"; else return viewBasePath + "?func=ll&objID=" + documentIdentifier.substring(colonPosition + 1) + "&objAction=download"; } /** Request arbitrary connector information. * This method is called directly from the API in order to allow API users to perform any one of several connector-specific * queries. *@param output is the response object, to be filled in by this method. *@param command is the command, which is taken directly from the API request. *@return true if the resource is found, false if not. In either case, output may be filled in. */ @Override public boolean requestInfo(Configuration output, String command) throws ManifoldCFException { if (command.equals("workspaces")) { try { String[] workspaces = getWorkspaceNames(); int i = 0; while (i < workspaces.length) { String workspace = workspaces[i++]; ConfigurationNode node = new ConfigurationNode("workspace"); node.setValue(workspace); output.addChild(output.getChildCount(), node); } } catch (ServiceInterruption e) { ManifoldCF.createServiceInterruptionNode(output, e); } catch (ManifoldCFException e) { ManifoldCF.createErrorNode(output, e); } } else if (command.startsWith("folders/")) { String path = command.substring("folders/".length()); try { String[] folders = getChildFolderNames(path); int i = 0; while (i < folders.length) { String folder = folders[i++]; ConfigurationNode node = new ConfigurationNode("folder"); node.setValue(folder); output.addChild(output.getChildCount(), node); } } catch (ServiceInterruption e) { ManifoldCF.createServiceInterruptionNode(output, e); } catch (ManifoldCFException e) { ManifoldCF.createErrorNode(output, e); } } else if (command.startsWith("categories/")) { String path = command.substring("categories/".length()); try { String[] categories = getChildCategoryNames(path); int i = 0; while (i < categories.length) { String category = categories[i++]; ConfigurationNode node = new ConfigurationNode("category"); node.setValue(category); output.addChild(output.getChildCount(), node); } } catch (ServiceInterruption e) { ManifoldCF.createServiceInterruptionNode(output, e); } catch (ManifoldCFException e) { ManifoldCF.createErrorNode(output, e); } } else if (command.startsWith("categoryattributes/")) { String path = command.substring("categoryattributes/".length()); try { String[] attributes = getCategoryAttributes(path); int i = 0; while (i < attributes.length) { String attribute = attributes[i++]; ConfigurationNode node = new ConfigurationNode("attribute"); node.setValue(attribute); output.addChild(output.getChildCount(), node); } } catch (ServiceInterruption e) { ManifoldCF.createServiceInterruptionNode(output, e); } catch (ManifoldCFException e) { ManifoldCF.createErrorNode(output, e); } } else return super.requestInfo(output, command); return true; } /** Queue "seed" documents. Seed documents are the starting places for crawling activity. Documents * are seeded when this method calls appropriate methods in the passed in ISeedingActivity object. * * This method can choose to find repository changes that happen only during the specified time interval. * The seeds recorded by this method will be viewed by the framework based on what the * getConnectorModel() method returns. * * It is not a big problem if the connector chooses to create more seeds than are * strictly necessary; it is merely a question of overall work required. * * The end time and seeding version string passed to this method may be interpreted for greatest efficiency. * For continuous crawling jobs, this method will * be called once, when the job starts, and at various periodic intervals as the job executes. * * When a job's specification is changed, the framework automatically resets the seeding version string to null. The * seeding version string may also be set to null on each job run, depending on the connector model returned by * getConnectorModel(). * * Note that it is always ok to send MORE documents rather than less to this method. * The connector will be connected before this method can be called. *@param activities is the interface this method should use to perform whatever framework actions are desired. *@param spec is a document specification (that comes from the job). *@param seedTime is the end of the time range of documents to consider, exclusive. *@param lastSeedVersionString is the last seeding version string for this job, or null if the job has no previous seeding version string. *@param jobMode is an integer describing how the job is being run, whether continuous or once-only. *@return an updated seeding version string, to be stored with the job. */ @Override public String addSeedDocuments(ISeedingActivity activities, Specification spec, String lastSeedVersion, long seedTime, int jobMode) throws ManifoldCFException, ServiceInterruption { getSession(); LivelinkContext llc = new LivelinkContext(); // First, grab the root LLValue ObjectInformation rootValue = llc.getObjectInformation(LLENTWK_VOL, LLENTWK_ID); if (!rootValue.exists()) { // If we get here, it HAS to be a bad network/transient problem. Logging.connectors .warn("Livelink: Could not look up root workspace object during seeding! Retrying -"); throw new ServiceInterruption("Service interruption during seeding", new ManifoldCFException("Could not looking root workspace object during seeding"), System.currentTimeMillis() + 60000L, System.currentTimeMillis() + 600000L, -1, true); } // Walk the specification for the "startpoint" types. Amalgamate these into a list of strings. // Presume that all roots are startpoint nodes boolean doUserWorkspaces = false; for (int i = 0; i < spec.getChildCount(); i++) { SpecificationNode n = spec.getChild(i); if (n.getType().equals("startpoint")) { // The id returned is simply the node path, which can't be messed up long beginTime = System.currentTimeMillis(); String path = n.getAttributeValue("path"); VolumeAndId vaf = rootValue.getPathId(path); if (vaf != null) { activities.recordActivity(new Long(beginTime), ACTIVITY_SEED, null, path, "OK", null, null); String newID = "F" + new Integer(vaf.getVolumeID()).toString() + ":" + new Integer(vaf.getPathId()).toString(); activities.addSeedDocument(newID); if (Logging.connectors.isDebugEnabled()) Logging.connectors.debug("Livelink: Seed = '" + newID + "'"); } else { activities.recordActivity(new Long(beginTime), ACTIVITY_SEED, null, path, "NOT FOUND", null, null); } } else if (n.getType().equals("userworkspace")) { String value = n.getAttributeValue("value"); if (value != null && value.equals("true")) doUserWorkspaces = true; else if (value != null && value.equals("false")) doUserWorkspaces = false; } if (doUserWorkspaces) { // Do ListUsers and enumerate the values. int sanityRetryCount = FAILURE_RETRY_COUNT; while (true) { ListUsersThread t = new ListUsersThread(); try { t.start(); LLValue childrenDocs; try { childrenDocs = t.finishUp(); } catch (ManifoldCFException e) { sanityRetryCount = assessRetry(sanityRetryCount, e); continue; } int size = 0; if (childrenDocs.isRecord()) size = 1; if (childrenDocs.isTable()) size = childrenDocs.size(); // Do the scan for (int j = 0; j < size; j++) { int childID = childrenDocs.toInteger(j, "ID"); // Skip admin user if (childID == 1000 || childID == 1001) continue; if (Logging.connectors.isDebugEnabled()) Logging.connectors.debug("Livelink: Found a user: ID=" + Integer.toString(childID)); activities.addSeedDocument("F0:" + Integer.toString(childID)); } break; } catch (InterruptedException e) { t.interrupt(); throw new ManifoldCFException("Interrupted: " + e.getMessage(), e, ManifoldCFException.INTERRUPTED); } catch (RuntimeException e) { sanityRetryCount = handleLivelinkRuntimeException(e, sanityRetryCount, true); continue; } } } } return ""; } /** Process a set of documents. * This is the method that should cause each document to be fetched, processed, and the results either added * to the queue of documents for the current job, and/or entered into the incremental ingestion manager. * The document specification allows this class to filter what is done based on the job. * The connector will be connected before this method can be called. *@param documentIdentifiers is the set of document identifiers to process. *@param statuses are the currently-stored document versions for each document in the set of document identifiers * passed in above. *@param activities is the interface this method should use to queue up new document references * and ingest documents. *@param jobMode is an integer describing how the job is being run, whether continuous or once-only. *@param usesDefaultAuthority will be true only if the authority in use for these documents is the default one. */ @Override public void processDocuments(String[] documentIdentifiers, IExistingVersions statuses, Specification spec, IProcessActivity activities, int jobMode, boolean usesDefaultAuthority) throws ManifoldCFException, ServiceInterruption { // Initialize a "livelink context", to minimize the number of objects we have to fetch LivelinkContext llc = new LivelinkContext(); // Initialize the table of catid's. // Keeping this around will allow us to benefit from batching of documents. MetadataDescription desc = new MetadataDescription(llc); // First, process the spec to get the string we tack on SystemMetadataDescription sDesc = new SystemMetadataDescription(llc, spec); // Read the forced acls. A null return indicates that security is disabled!!! // A zero-length return indicates that the native acls should be used. // All of this is germane to how we ingest the document, so we need to note it in // the version string completely. String[] acls = sDesc.getAcls(); // Sort it, in case it is needed. if (acls != null) java.util.Arrays.sort(acls); // Prepare the specified metadata String metadataString = null; String[] specifiedMetadataAttributes = null; CategoryPathAccumulator catAccum = null; if (!sDesc.includeAllMetadata()) { StringBuilder sb = new StringBuilder(); specifiedMetadataAttributes = sDesc.getMetadataAttributes(); // Sort! java.util.Arrays.sort(specifiedMetadataAttributes); // Build the metadata string piece now packList(sb, specifiedMetadataAttributes, '+'); metadataString = sb.toString(); } else catAccum = new CategoryPathAccumulator(llc); // Calculate the part of the version string that comes from path name and mapping. // This starts with = since ; is used by another optional component (the forced acls) String pathNameAttributeVersion; StringBuilder sb2 = new StringBuilder(); if (sDesc.getPathAttributeName() != null) sb2.append("=").append(sDesc.getPathAttributeName()).append(":").append(sDesc.getPathSeparator()) .append(":").append(sDesc.getMatchMapString()); pathNameAttributeVersion = sb2.toString(); // Since the identifier indicates it is a directory, then queue up all the current children which pass the filter. String filterString = sDesc.getFilterString(); for (String documentIdentifier : documentIdentifiers) { // Since each livelink access is time-consuming, be sure that we abort if the job has gone inactive activities.checkJobStillActive(); // Read the document or folder metadata, which includes the ModifyDate String docID = documentIdentifier; boolean isFolder = docID.startsWith("F"); int colonPos = docID.indexOf(":", 1); int objID; int vol; if (colonPos == -1) { objID = new Integer(docID.substring(1)).intValue(); vol = LLENTWK_VOL; } else { objID = new Integer(docID.substring(colonPos + 1)).intValue(); vol = new Integer(docID.substring(1, colonPos)).intValue(); } getSession(); ObjectInformation value = llc.getObjectInformation(vol, objID); if (!value.exists()) { if (Logging.connectors.isDebugEnabled()) Logging.connectors.debug("Livelink: Object " + Integer.toString(vol) + ":" + Integer.toString(objID) + " has no information - deleting"); activities.deleteDocument(documentIdentifier); continue; } // Make sure we have permission to see the object's contents int permissions = value.getPermissions().intValue(); if ((permissions & LAPI_DOCUMENTS.PERM_SEECONTENTS) == 0) { if (Logging.connectors.isDebugEnabled()) Logging.connectors.debug("Livelink: Crawl user cannot see contents of object " + Integer.toString(vol) + ":" + Integer.toString(objID) + " - deleting"); activities.deleteDocument(documentIdentifier); continue; } Date dt = value.getModifyDate(); // The rights don't change when the object changes, so we have to include those too. int[] rights = getObjectRights(vol, objID); if (rights == null) { if (Logging.connectors.isDebugEnabled()) Logging.connectors.debug("Livelink: Could not get rights for object " + Integer.toString(vol) + ":" + Integer.toString(objID) + " - deleting"); activities.deleteDocument(documentIdentifier); continue; } // We were able to get rights, so object still exists. // Changed folder versioning for MCF 2.0 if (isFolder) { // === Livelink folder === // I'm still not sure if Livelink folder modified dates are one-level or hierarchical. // The code below assumes one-level only, so we always scan folders and there's no versioning if (Logging.connectors.isDebugEnabled()) Logging.connectors.debug( "Livelink: Processing folder " + Integer.toString(vol) + ":" + Integer.toString(objID)); int sanityRetryCount = FAILURE_RETRY_COUNT; while (true) { ListObjectsThread t = new ListObjectsThread(vol, objID, filterString); try { t.start(); LLValue childrenDocs; try { childrenDocs = t.finishUp(); } catch (ManifoldCFException e) { sanityRetryCount = assessRetry(sanityRetryCount, e); continue; } int size = 0; if (childrenDocs.isRecord()) size = 1; if (childrenDocs.isTable()) size = childrenDocs.size(); // System.out.println("Total child count = "+Integer.toString(size)); // Do the scan for (int j = 0; j < size; j++) { int childID = childrenDocs.toInteger(j, "ID"); if (Logging.connectors.isDebugEnabled()) Logging.connectors .debug("Livelink: Found a child of folder " + Integer.toString(vol) + ":" + Integer.toString(objID) + " : ID=" + Integer.toString(childID)); int subtype = childrenDocs.toInteger(j, "SubType"); boolean childIsFolder = (subtype == LAPI_DOCUMENTS.FOLDERSUBTYPE || subtype == LAPI_DOCUMENTS.PROJECTSUBTYPE || subtype == LAPI_DOCUMENTS.COMPOUNDDOCUMENTSUBTYPE); // If it's a folder, we just let it through for now if (!childIsFolder && checkInclude( childrenDocs.toString(j, "Name") + "." + childrenDocs.toString(j, "FileType"), spec) == false) { if (Logging.connectors.isDebugEnabled()) Logging.connectors.debug("Livelink: Child identifier " + Integer.toString(childID) + " was excluded by inclusion criteria"); continue; } if (childIsFolder) { if (Logging.connectors.isDebugEnabled()) Logging.connectors.debug("Livelink: Child identifier " + Integer.toString(childID) + " is a folder, project, or compound document; adding a reference"); if (subtype == LAPI_DOCUMENTS.PROJECTSUBTYPE) { // If we pick up a project object, we need to describe the volume object (which // will be the root of all documents beneath) activities.addDocumentReference("F" + new Integer(childID).toString() + ":" + new Integer(-childID).toString()); } else activities.addDocumentReference("F" + new Integer(vol).toString() + ":" + new Integer(childID).toString()); } else { if (Logging.connectors.isDebugEnabled()) Logging.connectors .debug("Livelink: Child identifier " + Integer.toString(childID) + " is a simple document; adding a reference"); activities.addDocumentReference( "D" + new Integer(vol).toString() + ":" + new Integer(childID).toString()); } } break; } catch (InterruptedException e) { t.interrupt(); throw new ManifoldCFException("Interrupted: " + e.getMessage(), e, ManifoldCFException.INTERRUPTED); } catch (RuntimeException e) { sanityRetryCount = handleLivelinkRuntimeException(e, sanityRetryCount, true); continue; } } if (Logging.connectors.isDebugEnabled()) Logging.connectors.debug("Livelink: Done processing folder " + Integer.toString(vol) + ":" + Integer.toString(objID)); } else { // === Livelink document === // The version string includes the following: // 1) The modify date for the document // 2) The rights for the document, ordered (which can change without changing the ModifyDate field) // 3) The requested metadata fields (category and attribute, ordered) for the document // // The document identifiers are object id's. StringBuilder sb = new StringBuilder(); String[] categoryPaths; if (sDesc.includeAllMetadata()) { // Find all the metadata associated with this object, and then // find the set of category pathnames that correspond to it. int[] catIDs = getObjectCategoryIDs(vol, objID); categoryPaths = catAccum.getCategoryPathsAttributeNames(catIDs); // Sort! java.util.Arrays.sort(categoryPaths); // Build the metadata string piece now packList(sb, categoryPaths, '+'); } else { categoryPaths = specifiedMetadataAttributes; sb.append(metadataString); } String[] actualAcls; String[] denyAcls; String denyAcl; if (acls != null && acls.length == 0) { // No forced acls. Read the actual acls from livelink, as a set of rights. // We need also to add in support for the special rights objects. These are: // -1: RIGHT_WORLD // -2: RIGHT_SYSTEM // -3: RIGHT_OWNER // -4: RIGHT_GROUP // // RIGHT_WORLD means guest access. // RIGHT_SYSTEM is "Public Access". // RIGHT_OWNER is access by the owner of the object. // RIGHT_GROUP is access by a member of the base group containing the owner // // These objects are returned by the GetObjectRights() call made above, and NOT // returned by LLUser.ListObjects(). We have to figure out how to map these to // things that are // the equivalent of acls. actualAcls = lookupTokens(rights, value); java.util.Arrays.sort(actualAcls); // If security is on, no deny acl is needed for the local authority, since the repository does not support "deny". But this was added // to be really really really sure. denyAcl = defaultAuthorityDenyToken; } else if (acls != null && acls.length > 0) { // Forced acls actualAcls = acls; denyAcl = defaultAuthorityDenyToken; } else { // Security is OFF actualAcls = acls; denyAcl = null; } // Now encode the acls. If null, we write a special value. if (actualAcls == null) { sb.append('-'); denyAcls = null; } else { sb.append('+'); packList(sb, actualAcls, '+'); // This was added on 4/21/2008 to support forced acls working with the global default authority. pack(sb, denyAcl, '+'); denyAcls = new String[] { denyAcl }; } // The date does not need to be parseable sb.append(new Long(dt.getTime()).toString()); // PathNameAttributeVersion comes completely from the spec, so we don't // have to worry about it changing. No need, therefore, to parse it during // processDocuments. sb.append("=").append(pathNameAttributeVersion); // Tack on ingestCgiPath, to insulate us against changes to the repository connection setup. Added 9/7/07. sb.append("_").append(viewBasePath); String versionString = sb.toString(); if (Logging.connectors.isDebugEnabled()) Logging.connectors.debug("Livelink: Successfully calculated version string for document " + Integer.toString(vol) + ":" + Integer.toString(objID) + " : '" + versionString + "'"); if (!activities.checkDocumentNeedsReindexing(documentIdentifier, versionString)) continue; // Index the document if (Logging.connectors.isDebugEnabled()) Logging.connectors.debug("Livelink: Processing document " + Integer.toString(vol) + ":" + Integer.toString(objID)); if (!checkIngest(llc, objID, spec)) { if (Logging.connectors.isDebugEnabled()) Logging.connectors.debug("Livelink: Decided not to ingest document " + Integer.toString(vol) + ":" + Integer.toString(objID) + " - Did not match ingestion criteria"); activities.noDocument(documentIdentifier, versionString); continue; } if (Logging.connectors.isDebugEnabled()) Logging.connectors.debug("Livelink: Decided to ingest document " + Integer.toString(vol) + ":" + Integer.toString(objID)); // Grab the access tokens for this file from the version string, inside ingest method. ingestFromLiveLink(llc, documentIdentifier, versionString, actualAcls, denyAcls, categoryPaths, activities, desc, sDesc); if (Logging.connectors.isDebugEnabled()) Logging.connectors.debug("Livelink: Done processing document " + Integer.toString(vol) + ":" + Integer.toString(objID)); } } } protected class ListObjectsThread extends Thread { protected final int vol; protected final int objID; protected final String filterString; protected Throwable exception = null; protected LLValue rval = null; public ListObjectsThread(int vol, int objID, String filterString) { super(); setDaemon(true); this.vol = vol; this.objID = objID; this.filterString = filterString; } public void run() { try { LLValue childrenDocs = new LLValue(); int status = LLDocs.ListObjects(vol, objID, null, filterString, LAPI_DOCUMENTS.PERM_SEECONTENTS, childrenDocs); if (status != 0) { throw new ManifoldCFException("Error retrieving contents of folder " + Integer.toString(vol) + ":" + Integer.toString(objID) + " : Status=" + Integer.toString(status) + " (" + llServer.getErrors() + ")"); } rval = childrenDocs; } catch (Throwable e) { this.exception = e; } } public LLValue finishUp() throws ManifoldCFException, InterruptedException { join(); Throwable thr = exception; if (thr != null) { if (thr instanceof RuntimeException) throw (RuntimeException) thr; else if (thr instanceof ManifoldCFException) throw (ManifoldCFException) thr; else if (thr instanceof Error) throw (Error) thr; else throw new RuntimeException( "Unrecognized exception type: " + thr.getClass().getName() + ": " + thr.getMessage(), thr); } return rval; } } /** Get the maximum number of documents to amalgamate together into one batch, for this connector. *@return the maximum number. 0 indicates "unlimited". */ @Override public int getMaxDocumentRequest() { // Intrinsically, Livelink doesn't batch well. Multiple chunks have no advantage over one-at-a-time requests, // since apparently the Livelink API does not support multiples. HOWEVER - when metadata is considered, // it becomes worthwhile, because we will be able to do what is needed to look up the correct CATID node // only once per n requests! So it's a tradeoff between the advantage gained by threading, and the // savings gained by CATID lookup. // Note that at Shell, the fact that the network hiccups a lot makes it better to choose a smaller value. return 6; } // UI support methods. // // These support methods come in two varieties. The first bunch is involved in setting up connection configuration information. The second bunch // is involved in presenting and editing document specification information for a job. The two kinds of methods are accordingly treated differently, // in that the first bunch cannot assume that the current connector object is connected, while the second bunch can. That is why the first bunch // receives a thread context argument for all UI methods, while the second bunch does not need one (since it has already been applied via the connect() // method, above). /** Output the configuration header section. * This method is called in the head section of the connector's configuration page. Its purpose is to add the required tabs to the list, and to output any * javascript methods that might be needed by the configuration editing HTML. *@param threadContext is the local thread context. *@param out is the output to which any HTML should be sent. *@param parameters are the configuration parameters, as they currently exist, for this connection being configured. *@param tabsArray is an array of tab names. Add to this array any tab names that are specific to the connector. */ @Override public void outputConfigurationHeader(IThreadContext threadContext, IHTTPOutput out, Locale locale, ConfigParams parameters, List<String> tabsArray) throws ManifoldCFException, IOException { tabsArray.add(Messages.getString(locale, "LivelinkConnector.Server")); tabsArray.add(Messages.getString(locale, "LivelinkConnector.DocumentAccess")); tabsArray.add(Messages.getString(locale, "LivelinkConnector.DocumentView")); out.print("<script type=\"text/javascript\">\n" + "<!--\n" + "function ServerDeleteCertificate(aliasName)\n" + "{\n" + " editconnection.serverkeystorealias.value = aliasName;\n" + " editconnection.serverconfigop.value = \"Delete\";\n" + " postForm();\n" + "}\n" + "\n" + "function ServerAddCertificate()\n" + "{\n" + " if (editconnection.servercertificate.value == \"\")\n" + " {\n" + " alert(\"" + Messages.getBodyJavascriptString(locale, "LivelinkConnector.ChooseACertificateFile") + "\");\n" + " editconnection.servercertificate.focus();\n" + " }\n" + " else\n" + " {\n" + " editconnection.serverconfigop.value = \"Add\";\n" + " postForm();\n" + " }\n" + "}\n" + "\n" + "function IngestDeleteCertificate(aliasName)\n" + "{\n" + " editconnection.ingestkeystorealias.value = aliasName;\n" + " editconnection.ingestconfigop.value = \"Delete\";\n" + " postForm();\n" + "}\n" + "\n" + "function IngestAddCertificate()\n" + "{\n" + " if (editconnection.ingestcertificate.value == \"\")\n" + " {\n" + " alert(\"" + Messages.getBodyJavascriptString(locale, "LivelinkConnector.ChooseACertificateFile") + "\");\n" + " editconnection.ingestcertificate.focus();\n" + " }\n" + " else\n" + " {\n" + " editconnection.ingestconfigop.value = \"Add\";\n" + " postForm();\n" + " }\n" + "}\n" + "\n" + "function checkConfig()\n" + "{\n" + " if (editconnection.serverport.value != \"\" && !isInteger(editconnection.serverport.value))\n" + " {\n" + " alert(\"" + Messages.getBodyJavascriptString(locale, "LivelinkConnector.AValidNumberIsRequired") + "\");\n" + " editconnection.serverport.focus();\n" + " return false;\n" + " }\n" + " if (editconnection.ingestport.value != \"\" && !isInteger(editconnection.ingestport.value))\n" + " {\n" + " alert(\"" + Messages.getBodyJavascriptString(locale, "LivelinkConnector.AValidNumberOrBlankIsRequired") + "\");\n" + " editconnection.ingestport.focus();\n" + " return false;\n" + " }\n" + " if (editconnection.viewport.value != \"\" && !isInteger(editconnection.viewport.value))\n" + " {\n" + " alert(\"" + Messages.getBodyJavascriptString(locale, "LivelinkConnector.AValidNumberOrBlankIsRequired") + "\");\n" + " editconnection.viewport.focus();\n" + " return false;\n" + " }\n" + " return true;\n" + "}\n" + "\n" + "function checkConfigForSave()\n" + "{\n" + " if (editconnection.servername.value == \"\")\n" + " {\n" + " alert(\"" + Messages.getBodyJavascriptString(locale, "LivelinkConnector.EnterALivelinkServerName") + "\");\n" + " SelectTab(\"" + Messages.getBodyJavascriptString(locale, "LivelinkConnector.Server") + "\");\n" + " editconnection.servername.focus();\n" + " return false;\n" + " }\n" + " if (editconnection.serverport.value == \"\")\n" + " {\n" + " alert(\"" + Messages.getBodyJavascriptString(locale, "LivelinkConnector.AServerPortNumberIsRequired") + "\");\n" + " SelectTab(\"" + Messages.getBodyJavascriptString(locale, "LivelinkConnector.Server") + "\");\n" + " editconnection.serverport.focus();\n" + " return false;\n" + " }\n" + " if (editconnection.serverhttpcgipath.value == \"\")\n" + " {\n" + " alert(\"" + Messages.getBodyJavascriptString(locale, "LivelinkConnector.EnterTheServerCgiPathToLivelink") + "\");\n" + " SelectTab(\"" + Messages.getBodyJavascriptString(locale, "LivelinkConnector.Server") + "\");\n" + " editconnection.serverhttpcgipath.focus();\n" + " return false;\n" + " }\n" + " if (editconnection.serverhttpcgipath.value.substring(0,1) != \"/\")\n" + " {\n" + " alert(\"" + Messages.getBodyJavascriptString(locale, "LivelinkConnector.TheServerCgiPathMustBeginWithACharacter") + "\");\n" + " SelectTab(\"" + Messages.getBodyJavascriptString(locale, "LivelinkConnector.Server") + "\");\n" + " editconnection.serverhttpcgipath.focus();\n" + " return false;\n" + " }\n" + " if (editconnection.viewprotocol.value == \"\" && editconnection.ingestprotocol.value == \"\")\n" + " {\n" + " alert(\"" + Messages.getBodyJavascriptString(locale, "LivelinkConnector.SelectAViewProtocol") + "\");\n" + " SelectTab(\"" + Messages.getBodyJavascriptString(locale, "LivelinkConnector.DocumentView") + "\");\n" + " editconnection.viewprotocol.focus();\n" + " return false;\n" + " }\n" + " if (editconnection.viewcgipath.value == \"\" && editconnection.ingestcgipath.value == \"\")\n" + " {\n" + " alert(\"" + Messages.getBodyJavascriptString(locale, "LivelinkConnector.EnterTheViewCgiPathToLivelink") + "\");\n" + " SelectTab(\"" + Messages.getBodyJavascriptString(locale, "LivelinkConnector.DocumentView") + "\");\n" + " editconnection.viewcgipath.focus();\n" + " return false;\n" + " }\n" + " if (editconnection.ingestcgipath.value != \"\" && editconnection.ingestcgipath.value.substring(0,1) != \"/\")\n" + " {\n" + " alert(\"" + Messages.getBodyJavascriptString(locale, "LivelinkConnector.TheIngestCgiPathMustBeBlankOrBeginWithACharacter") + "\");\n" + " SelectTab(\"" + Messages.getBodyJavascriptString(locale, "LivelinkConnector.DocumentAccess") + "\");\n" + " editconnection.ingestcgipath.focus();\n" + " return false;\n" + " }\n" + " if (editconnection.viewcgipath.value != \"\" && editconnection.viewcgipath.value.substring(0,1) != \"/\")\n" + " {\n" + " alert(\"" + Messages.getBodyJavascriptString(locale, "LivelinkConnector.TheViewCgiPathMustBeBlankOrBeginWithACharacter") + "\");\n" + " SelectTab(\"" + Messages.getBodyJavascriptString(locale, "LivelinkConnector.DocumentView") + "\");\n" + " editconnection.viewcgipath.focus();\n" + " return false;\n" + " }\n" + " return true;\n" + "}\n" + "\n" + "//-->\n" + "</script>\n"); } /** Output the configuration body section. * This method is called in the body section of the connector's configuration page. Its purpose is to present the required form elements for editing. * The coder can presume that the HTML that is output from this configuration will be within appropriate <html>, <body>, and <form> tags. The name of the * form is "editconnection". *@param threadContext is the local thread context. *@param out is the output to which any HTML should be sent. *@param parameters are the configuration parameters, as they currently exist, for this connection being configured. *@param tabName is the current tab name. */ @Override public void outputConfigurationBody(IThreadContext threadContext, IHTTPOutput out, Locale locale, ConfigParams parameters, String tabName) throws ManifoldCFException, IOException { // LAPI parameters String serverProtocol = parameters.getParameter(LiveLinkParameters.serverProtocol); if (serverProtocol == null) serverProtocol = "internal"; String serverName = parameters.getParameter(LiveLinkParameters.serverName); if (serverName == null) serverName = "localhost"; String serverPort = parameters.getParameter(LiveLinkParameters.serverPort); if (serverPort == null) serverPort = "2099"; String serverUserName = parameters.getParameter(LiveLinkParameters.serverUsername); if (serverUserName == null) serverUserName = ""; String serverPassword = parameters.getObfuscatedParameter(LiveLinkParameters.serverPassword); if (serverPassword == null) serverPassword = ""; else serverPassword = out.mapPasswordToKey(serverPassword); String serverHTTPCgiPath = parameters.getParameter(LiveLinkParameters.serverHTTPCgiPath); if (serverHTTPCgiPath == null) serverHTTPCgiPath = "/livelink/livelink.exe"; String serverHTTPNTLMDomain = parameters.getParameter(LiveLinkParameters.serverHTTPNTLMDomain); if (serverHTTPNTLMDomain == null) serverHTTPNTLMDomain = ""; String serverHTTPNTLMUserName = parameters.getParameter(LiveLinkParameters.serverHTTPNTLMUsername); if (serverHTTPNTLMUserName == null) serverHTTPNTLMUserName = ""; String serverHTTPNTLMPassword = parameters .getObfuscatedParameter(LiveLinkParameters.serverHTTPNTLMPassword); if (serverHTTPNTLMPassword == null) serverHTTPNTLMPassword = ""; else serverHTTPNTLMPassword = out.mapPasswordToKey(serverHTTPNTLMPassword); String serverHTTPSKeystore = parameters.getParameter(LiveLinkParameters.serverHTTPSKeystore); IKeystoreManager localServerHTTPSKeystore; if (serverHTTPSKeystore == null) localServerHTTPSKeystore = KeystoreManagerFactory.make(""); else localServerHTTPSKeystore = KeystoreManagerFactory.make("", serverHTTPSKeystore); // Document access parameters String ingestProtocol = parameters.getParameter(LiveLinkParameters.ingestProtocol); if (ingestProtocol == null) ingestProtocol = ""; String ingestPort = parameters.getParameter(LiveLinkParameters.ingestPort); if (ingestPort == null) ingestPort = ""; String ingestCgiPath = parameters.getParameter(LiveLinkParameters.ingestCgiPath); if (ingestCgiPath == null) ingestCgiPath = ""; String ingestNtlmUsername = parameters.getParameter(LiveLinkParameters.ingestNtlmUsername); if (ingestNtlmUsername == null) ingestNtlmUsername = ""; String ingestNtlmPassword = parameters.getObfuscatedParameter(LiveLinkParameters.ingestNtlmPassword); if (ingestNtlmPassword == null) ingestNtlmPassword = ""; else ingestNtlmPassword = out.mapPasswordToKey(ingestNtlmPassword); String ingestNtlmDomain = parameters.getParameter(LiveLinkParameters.ingestNtlmDomain); if (ingestNtlmDomain == null) ingestNtlmDomain = ""; String ingestKeystore = parameters.getParameter(LiveLinkParameters.ingestKeystore); IKeystoreManager localIngestKeystore; if (ingestKeystore == null) localIngestKeystore = KeystoreManagerFactory.make(""); else localIngestKeystore = KeystoreManagerFactory.make("", ingestKeystore); // Document view parameters String viewProtocol = parameters.getParameter(LiveLinkParameters.viewProtocol); if (viewProtocol == null) viewProtocol = "http"; String viewServerName = parameters.getParameter(LiveLinkParameters.viewServerName); if (viewServerName == null) viewServerName = ""; String viewPort = parameters.getParameter(LiveLinkParameters.viewPort); if (viewPort == null) viewPort = ""; String viewCgiPath = parameters.getParameter(LiveLinkParameters.viewCgiPath); if (viewCgiPath == null) viewCgiPath = "/livelink/livelink.exe"; // The "Server" tab // Always pass the whole keystore as a hidden. out.print("<input name=\"serverconfigop\" type=\"hidden\" value=\"Continue\"/>\n"); if (serverHTTPSKeystore != null) { out.print("<input type=\"hidden\" name=\"serverhttpskeystoredata\" value=\"" + org.apache.manifoldcf.ui.util.Encoder.attributeEscape(serverHTTPSKeystore) + "\"/>\n"); } if (tabName.equals(Messages.getString(locale, "LivelinkConnector.Server"))) { out.print("<table class=\"displaytable\">\n" + " <tr><td class=\"separator\" colspan=\"2\"><hr/></td></tr>\n" + " <tr>\n" + " <td class=\"description\">" + Messages.getBodyString(locale, "LivelinkConnector.ServerProtocol") + "</td>\n" + " <td class=\"value\">\n" + " <select name=\"serverprotocol\" size=\"2\">\n" + " <option value=\"internal\" " + ((serverProtocol.equals("internal")) ? "selected=\"selected\"" : "") + ">" + Messages.getBodyString(locale, "LivelinkConnector.internal") + "</option>\n" + " <option value=\"http\" " + ((serverProtocol.equals("http")) ? "selected=\"selected\"" : "") + ">http</option>\n" + " <option value=\"https\" " + ((serverProtocol.equals("https")) ? "selected=\"selected\"" : "") + ">https</option>\n" + " </select>\n" + " </td>\n" + " </tr>\n" + " <tr>\n" + " <td class=\"description\"><nobr>" + Messages.getBodyString(locale, "LivelinkConnector.ServerName") + "</nobr></td>\n" + " <td class=\"value\"><input type=\"text\" size=\"64\" name=\"servername\" value=\"" + org.apache.manifoldcf.ui.util.Encoder.attributeEscape(serverName) + "\"/></td>\n" + " </tr>\n" + " <tr>\n" + " <td class=\"description\"><nobr>" + Messages.getBodyString(locale, "LivelinkConnector.ServerPort") + "</nobr></td>\n" + " <td class=\"value\"><input type=\"text\" size=\"5\" name=\"serverport\" value=\"" + serverPort + "\"/></td>\n" + " </tr>\n" + " <tr>\n" + " <td class=\"description\"><nobr>" + Messages.getBodyString(locale, "LivelinkConnector.ServerUserName") + "</nobr></td>\n" + " <td class=\"value\"><input type=\"text\" size=\"32\" name=\"serverusername\" value=\"" + org.apache.manifoldcf.ui.util.Encoder.attributeEscape(serverUserName) + "\"/></td>\n" + " </tr>\n" + " <tr>\n" + " <td class=\"description\"><nobr>" + Messages.getBodyString(locale, "LivelinkConnector.ServerPassword") + "</nobr></td>\n" + " <td class=\"value\"><input type=\"password\" size=\"32\" name=\"serverpassword\" value=\"" + org.apache.manifoldcf.ui.util.Encoder.attributeEscape(serverPassword) + "\"/></td>\n" + " </tr>\n" + " <tr><td class=\"separator\" colspan=\"2\"><hr/></td></tr>\n" + " <tr>\n" + " <td class=\"description\"><nobr>" + Messages.getBodyString(locale, "LivelinkConnector.ServerHTTPCGIPath") + "</nobr></td>\n" + " <td class=\"value\"><input type=\"text\" size=\"32\" name=\"serverhttpcgipath\" value=\"" + org.apache.manifoldcf.ui.util.Encoder.attributeEscape(serverHTTPCgiPath) + "\"/></td>\n" + " </tr>\n" + " <tr>\n" + " <td class=\"description\"><nobr>" + Messages.getBodyString(locale, "LivelinkConnector.ServerHTTPNTLMDomain") + "</nobr><br/><nobr>" + Messages.getBodyString(locale, "LivelinkConnector.SetIfNTLMAuthDesired") + "</nobr></td>\n" + " <td class=\"value\">\n" + " <input type=\"text\" size=\"32\" name=\"serverhttpntlmdomain\" value=\"" + org.apache.manifoldcf.ui.util.Encoder.attributeEscape(serverHTTPNTLMDomain) + "\"/>\n" + " </td>\n" + " </tr>\n" + " <tr>\n" + " <td class=\"description\"><nobr>" + Messages.getBodyString(locale, "LivelinkConnector.ServerHTTPNTLMUserName") + "</nobr></td>\n" + " <td class=\"value\">\n" + " <input type=\"text\" size=\"32\" name=\"serverhttpntlmusername\" value=\"" + org.apache.manifoldcf.ui.util.Encoder.attributeEscape(serverHTTPNTLMUserName) + "\"/>\n" + " </td>\n" + " </tr>\n" + " <tr>\n" + " <td class=\"description\"><nobr>" + Messages.getBodyString(locale, "LivelinkConnector.ServerHTTPNTLMPassword") + "</nobr></td>\n" + " <td class=\"value\">\n" + " <input type=\"password\" size=\"32\" name=\"serverhttpntlmpassword\" value=\"" + org.apache.manifoldcf.ui.util.Encoder.attributeEscape(serverHTTPNTLMPassword) + "\"/>\n" + " </td>\n" + " </tr>\n" + " <tr><td class=\"separator\" colspan=\"2\"><hr/></td></tr>\n"); out.print(" <tr>\n" + " <td class=\"description\"><nobr>" + Messages.getBodyString(locale, "LivelinkConnector.ServerSSLCertificateList") + "</nobr></td>\n" + " <td class=\"value\">\n" + " <input type=\"hidden\" name=\"serverkeystorealias\" value=\"\"/>\n" + " <table class=\"displaytable\">\n"); // List the individual certificates in the store, with a delete button for each String[] contents = localServerHTTPSKeystore.getContents(); if (contents.length == 0) { out.print(" <tr><td class=\"message\" colspan=\"2\"><nobr>" + Messages.getBodyString(locale, "LivelinkConnector.NoCertificatesPresent") + "</nobr></td></tr>\n"); } else { int i = 0; while (i < contents.length) { String alias = contents[i]; String description = localServerHTTPSKeystore.getDescription(alias); if (description.length() > 128) description = description.substring(0, 125) + "..."; out.print(" <tr>\n" + " <td class=\"value\"><input type=\"button\" onclick='Javascript:ServerDeleteCertificate(\"" + org.apache.manifoldcf.ui.util.Encoder.attributeJavascriptEscape(alias) + "\")' alt=\"" + Messages.getAttributeString(locale, "LivelinkConnector.DeleteCert") + org.apache.manifoldcf.ui.util.Encoder.attributeEscape(alias) + "\" value=\"" + Messages.getAttributeString(locale, "LivelinkConnector.Delete") + "\"/></td>\n" + " <td>" + org.apache.manifoldcf.ui.util.Encoder.bodyEscape(description) + "</td>\n" + " </tr>\n"); i++; } } out.print(" </table>\n" + " <input type=\"button\" onclick='Javascript:ServerAddCertificate()' alt=\"" + Messages.getAttributeString(locale, "LivelinkConnector.AddCert") + "\" value=\"" + Messages.getAttributeString(locale, "LivelinkConnector.Add") + "\"/> \n" + " " + Messages.getBodyString(locale, "LivelinkConnector.Certificate") + "<input name=\"servercertificate\" size=\"50\" type=\"file\"/>\n" + " </td>\n" + " </tr>\n"); out.print("</table>\n"); } else { // Hiddens for Server tab out.print("<input type=\"hidden\" name=\"serverprotocol\" value=\"" + serverProtocol + "\"/>\n" + "<input type=\"hidden\" name=\"servername\" value=\"" + org.apache.manifoldcf.ui.util.Encoder.attributeEscape(serverName) + "\"/>\n" + "<input type=\"hidden\" name=\"serverport\" value=\"" + serverPort + "\"/>\n" + "<input type=\"hidden\" name=\"serverusername\" value=\"" + org.apache.manifoldcf.ui.util.Encoder.attributeEscape(serverUserName) + "\"/>\n" + "<input type=\"hidden\" name=\"serverpassword\" value=\"" + org.apache.manifoldcf.ui.util.Encoder.attributeEscape(serverPassword) + "\"/>\n" + "<input type=\"hidden\" name=\"serverhttpcgipath\" value=\"" + org.apache.manifoldcf.ui.util.Encoder.attributeEscape(serverHTTPCgiPath) + "\"/>\n" + "<input type=\"hidden\" name=\"serverhttpntlmdomain\" value=\"" + org.apache.manifoldcf.ui.util.Encoder.attributeEscape(serverHTTPNTLMDomain) + "\"/>\n" + "<input type=\"hidden\" name=\"serverhttpntlmusername\" value=\"" + org.apache.manifoldcf.ui.util.Encoder.attributeEscape(serverHTTPNTLMUserName) + "\"/>\n" + "<input type=\"hidden\" name=\"serverhttpntlmpassword\" value=\"" + org.apache.manifoldcf.ui.util.Encoder.attributeEscape(serverHTTPNTLMPassword) + "\"/>\n"); } // The "Document Access" tab // Always pass the whole keystore as a hidden. out.print("<input name=\"ingestconfigop\" type=\"hidden\" value=\"Continue\"/>\n"); if (ingestKeystore != null) { out.print("<input type=\"hidden\" name=\"ingestkeystoredata\" value=\"" + org.apache.manifoldcf.ui.util.Encoder.attributeEscape(ingestKeystore) + "\"/>\n"); } if (tabName.equals(Messages.getString(locale, "LivelinkConnector.DocumentAccess"))) { out.print("<table class=\"displaytable\">\n" + " <tr><td class=\"separator\" colspan=\"2\"><hr/></td></tr>\n" + " <tr>\n" + " <td class=\"description\">" + Messages.getBodyString(locale, "LivelinkConnector.DocumentFetchProtocol") + "</td>\n" + " <td class=\"value\">\n" + " <select name=\"ingestprotocol\" size=\"3\">\n" + " <option value=\"\" " + ((ingestProtocol.equals("")) ? "selected=\"selected\"" : "") + ">" + Messages.getBodyString(locale, "LivelinkConnector.UseLAPI") + "</option>\n" + " <option value=\"http\" " + ((ingestProtocol.equals("http")) ? "selected=\"selected\"" : "") + ">http</option>\n" + " <option value=\"https\" " + ((ingestProtocol.equals("https")) ? "selected=\"selected\"" : "") + ">https</option>\n" + " </select>\n" + " </td>\n" + " </tr>\n" + " <tr>\n" + " <td class=\"description\"><nobr>" + Messages.getBodyString(locale, "LivelinkConnector.DocumentFetchPort") + "</nobr></td>\n" + " <td class=\"value\"><input type=\"text\" size=\"5\" name=\"ingestport\" value=\"" + ingestPort + "\"/></td>\n" + " </tr>\n" + " <tr><td class=\"separator\" colspan=\"2\"><hr/></td></tr>\n" + " <tr>\n" + " <td class=\"description\"><nobr>" + Messages.getBodyString(locale, "LivelinkConnector.DocumentFetchCGIPath") + "</nobr></td>\n" + " <td class=\"value\"><input type=\"text\" size=\"32\" name=\"ingestcgipath\" value=\"" + org.apache.manifoldcf.ui.util.Encoder.attributeEscape(ingestCgiPath) + "\"/></td>\n" + " </tr>\n" + " <tr>\n" + " <td class=\"description\"><nobr>" + Messages.getBodyString(locale, "LivelinkConnector.DocumentFetchNTLMDomain") + "</nobr><br/><nobr>" + Messages.getBodyString(locale, "LivelinkConnector.SetIfNTLMAuthDesired") + "</nobr></td>\n" + " <td class=\"value\">\n" + " <input type=\"text\" size=\"32\" name=\"ingestntlmdomain\" value=\"" + org.apache.manifoldcf.ui.util.Encoder.attributeEscape(ingestNtlmDomain) + "\"/>\n" + " </td>\n" + " </tr>\n" + " <tr>\n" + " <td class=\"description\"><nobr>" + Messages.getBodyString(locale, "LivelinkConnector.DocumentFetchNTLMUserName") + "</nobr><br/><nobr>" + Messages.getBodyString(locale, "LivelinkConnector.SetIfDifferentFromServerUserName") + "</nobr></td>\n" + " <td class=\"value\">\n" + " <input type=\"text\" size=\"32\" name=\"ingestntlmusername\" value=\"" + org.apache.manifoldcf.ui.util.Encoder.attributeEscape(ingestNtlmUsername) + "\"/>\n" + " </td>\n" + " </tr>\n" + " <tr>\n" + " <td class=\"description\"><nobr>" + Messages.getBodyString(locale, "LivelinkConnector.DocumentFetchNTLMPassword") + "</nobr><br/><nobr>" + Messages.getBodyString(locale, "LivelinkConnector.SetIfDifferentFromServerPassword") + "</nobr></td>\n" + " <td class=\"value\">\n" + " <input type=\"password\" size=\"32\" name=\"ingestntlmpassword\" value=\"" + org.apache.manifoldcf.ui.util.Encoder.attributeEscape(ingestNtlmPassword) + "\"/>\n" + " </td>\n" + " </tr>\n" + " <tr><td class=\"separator\" colspan=\"2\"><hr/></td></tr>\n"); out.print(" <tr>\n" + " <td class=\"description\"><nobr>" + Messages.getBodyString(locale, "LivelinkConnector.DocumentFetchSSLCertificateList") + "</nobr></td>\n" + " <td class=\"value\">\n" + " <input type=\"hidden\" name=\"ingestkeystorealias\" value=\"\"/>\n" + " <table class=\"displaytable\">\n"); // List the individual certificates in the store, with a delete button for each String[] contents = localIngestKeystore.getContents(); if (contents.length == 0) { out.print(" <tr><td class=\"message\" colspan=\"2\"><nobr>" + Messages.getBodyString(locale, "LivelinkConnector.NoCertificatesPresent") + "</nobr></td></tr>\n"); } else { int i = 0; while (i < contents.length) { String alias = contents[i]; String description = localIngestKeystore.getDescription(alias); if (description.length() > 128) description = description.substring(0, 125) + "..."; out.print(" <tr>\n" + " <td class=\"value\"><input type=\"button\" onclick='Javascript:IngestDeleteCertificate(\"" + org.apache.manifoldcf.ui.util.Encoder.attributeJavascriptEscape(alias) + "\")' alt=\"" + Messages.getAttributeString(locale, "LivelinkConnector.DeleteCert") + org.apache.manifoldcf.ui.util.Encoder.attributeEscape(alias) + "\" value=\"" + Messages.getAttributeString(locale, "LivelinkConnector.Delete") + "\"/></td>\n" + " <td>" + org.apache.manifoldcf.ui.util.Encoder.bodyEscape(description) + "</td>\n" + " </tr>\n"); i++; } } out.print(" </table>\n" + " <input type=\"button\" onclick='Javascript:IngestAddCertificate()' alt=\"" + Messages.getAttributeString(locale, "LivelinkConnector.AddCert") + "\" value=\"" + Messages.getAttributeString(locale, "LivelinkConnector.Add") + "\"/> \n" + " " + Messages.getBodyString(locale, "LivelinkConnector.Certificate") + "<input name=\"ingestcertificate\" size=\"50\" type=\"file\"/>\n" + " </td>\n" + " </tr>\n"); out.print("</table>\n"); } else { // Hiddens for Document Access tab out.print("<input type=\"hidden\" name=\"ingestprotocol\" value=\"" + ingestProtocol + "\"/>\n" + "<input type=\"hidden\" name=\"ingestport\" value=\"" + ingestPort + "\"/>\n" + "<input type=\"hidden\" name=\"ingestcgipath\" value=\"" + org.apache.manifoldcf.ui.util.Encoder.attributeEscape(ingestCgiPath) + "\"/>\n" + "<input type=\"hidden\" name=\"ingestntlmusername\" value=\"" + org.apache.manifoldcf.ui.util.Encoder.attributeEscape(ingestNtlmUsername) + "\"/>\n" + "<input type=\"hidden\" name=\"ingestntlmpassword\" value=\"" + org.apache.manifoldcf.ui.util.Encoder.attributeEscape(ingestNtlmPassword) + "\"/>\n" + "<input type=\"hidden\" name=\"ingestntlmdomain\" value=\"" + org.apache.manifoldcf.ui.util.Encoder.attributeEscape(ingestNtlmDomain) + "\"/>\n"); } // Document View tab if (tabName.equals(Messages.getString(locale, "LivelinkConnector.DocumentView"))) { out.print("<table class=\"displaytable\">\n" + " <tr><td class=\"separator\" colspan=\"2\"><hr/></td></tr>\n" + " <tr>\n" + " <td class=\"description\">" + Messages.getBodyString(locale, "LivelinkConnector.DocumentViewProtocol") + "</td>\n" + " <td class=\"value\">\n" + " <select name=\"viewprotocol\" size=\"3\">\n" + " <option value=\"\" " + ((viewProtocol.equals("")) ? "selected=\"selected\"" : "") + ">" + Messages.getBodyString(locale, "LivelinkConnector.SameAsFetchProtocol") + "</option>\n" + " <option value=\"http\" " + ((viewProtocol.equals("http")) ? "selected=\"selected\"" : "") + ">http</option>\n" + " <option value=\"https\" " + ((viewProtocol.equals("https")) ? "selected=\"selected\"" : "") + ">https</option>\n" + " </select>\n" + " </td>\n" + " </tr>\n" + " <tr>\n" + " <td class=\"description\"><nobr>" + Messages.getBodyString(locale, "LivelinkConnector.DocumentViewServerName") + "</nobr><br/><nobr>" + Messages.getBodyString(locale, "LivelinkConnector.BlankSameAsFetchServer") + "</nobr></td>\n" + " <td class=\"value\"><input type=\"text\" size=\"64\" name=\"viewservername\" value=\"" + org.apache.manifoldcf.ui.util.Encoder.attributeEscape(viewServerName) + "\"/></td>\n" + " </tr>\n" + " <tr>\n" + " <td class=\"description\"><nobr>" + Messages.getBodyString(locale, "LivelinkConnector.DocumentViewPort") + "</nobr><br/><nobr>(blank = same as fetch port)</nobr></td>\n" + " <td class=\"value\"><input type=\"text\" size=\"5\" name=\"viewport\" value=\"" + viewPort + "\"/></td>\n" + " </tr>\n" + " <tr>\n" + " <td class=\"description\"><nobr>" + Messages.getBodyString(locale, "LivelinkConnector.DocumentViewCGIPath") + "</nobr><br/><nobr>" + Messages.getBodyString(locale, "LivelinkConnector.BlankSameAsFetchServer") + "</nobr></td>\n" + " <td class=\"value\"><input type=\"text\" size=\"32\" name=\"viewcgipath\" value=\"" + org.apache.manifoldcf.ui.util.Encoder.attributeEscape(viewCgiPath) + "\"/></td>\n" + " </tr>\n" + "</table>\n"); } else { // Hiddens for Document View tab out.print("<input type=\"hidden\" name=\"viewprotocol\" value=\"" + org.apache.manifoldcf.ui.util.Encoder.attributeEscape(viewProtocol) + "\"/>\n" + "<input type=\"hidden\" name=\"viewservername\" value=\"" + org.apache.manifoldcf.ui.util.Encoder.attributeEscape(viewServerName) + "\"/>\n" + "<input type=\"hidden\" name=\"viewport\" value=\"" + viewPort + "\"/>\n" + "<input type=\"hidden\" name=\"viewcgipath\" value=\"" + org.apache.manifoldcf.ui.util.Encoder.attributeEscape(viewCgiPath) + "\"/>\n"); } } /** Process a configuration post. * This method is called at the start of the connector's configuration page, whenever there is a possibility that form data for a connection has been * posted. Its purpose is to gather form information and modify the configuration parameters accordingly. * The name of the posted form is "editconnection". *@param threadContext is the local thread context. *@param variableContext is the set of variables available from the post, including binary file post information. *@param parameters are the configuration parameters, as they currently exist, for this connection being configured. *@return null if all is well, or a string error message if there is an error that should prevent saving of the connection (and cause a redirection to an error page). */ @Override public String processConfigurationPost(IThreadContext threadContext, IPostParameters variableContext, Locale locale, ConfigParams parameters) throws ManifoldCFException { // View parameters String viewProtocol = variableContext.getParameter("viewprotocol"); if (viewProtocol != null) parameters.setParameter(LiveLinkParameters.viewProtocol, viewProtocol); String viewServerName = variableContext.getParameter("viewservername"); if (viewServerName != null) parameters.setParameter(LiveLinkParameters.viewServerName, viewServerName); String viewPort = variableContext.getParameter("viewport"); if (viewPort != null) parameters.setParameter(LiveLinkParameters.viewPort, viewPort); String viewCgiPath = variableContext.getParameter("viewcgipath"); if (viewCgiPath != null) parameters.setParameter(LiveLinkParameters.viewCgiPath, viewCgiPath); // Server parameters String serverProtocol = variableContext.getParameter("serverprotocol"); if (serverProtocol != null) parameters.setParameter(LiveLinkParameters.serverProtocol, serverProtocol); String serverName = variableContext.getParameter("servername"); if (serverName != null) parameters.setParameter(LiveLinkParameters.serverName, serverName); String serverPort = variableContext.getParameter("serverport"); if (serverPort != null) parameters.setParameter(LiveLinkParameters.serverPort, serverPort); String serverUserName = variableContext.getParameter("serverusername"); if (serverUserName != null) parameters.setParameter(LiveLinkParameters.serverUsername, serverUserName); String serverPassword = variableContext.getParameter("serverpassword"); if (serverPassword != null) parameters.setObfuscatedParameter(LiveLinkParameters.serverPassword, variableContext.mapKeyToPassword(serverPassword)); String serverHTTPCgiPath = variableContext.getParameter("serverhttpcgipath"); if (serverHTTPCgiPath != null) parameters.setParameter(LiveLinkParameters.serverHTTPCgiPath, serverHTTPCgiPath); String serverHTTPNTLMDomain = variableContext.getParameter("serverhttpntlmdomain"); if (serverHTTPNTLMDomain != null) parameters.setParameter(LiveLinkParameters.serverHTTPNTLMDomain, serverHTTPNTLMDomain); String serverHTTPNTLMUserName = variableContext.getParameter("serverhttpntlmusername"); if (serverHTTPNTLMUserName != null) parameters.setParameter(LiveLinkParameters.serverHTTPNTLMUsername, serverHTTPNTLMUserName); String serverHTTPNTLMPassword = variableContext.getParameter("serverhttpntlmpassword"); if (serverHTTPNTLMPassword != null) parameters.setObfuscatedParameter(LiveLinkParameters.serverHTTPNTLMPassword, variableContext.mapKeyToPassword(serverHTTPNTLMPassword)); String serverHTTPSKeystoreValue = variableContext.getParameter("serverhttpskeystoredata"); if (serverHTTPSKeystoreValue != null) parameters.setParameter(LiveLinkParameters.serverHTTPSKeystore, serverHTTPSKeystoreValue); String serverConfigOp = variableContext.getParameter("serverconfigop"); if (serverConfigOp != null) { if (serverConfigOp.equals("Delete")) { String alias = variableContext.getParameter("serverkeystorealias"); serverHTTPSKeystoreValue = parameters.getParameter(LiveLinkParameters.serverHTTPSKeystore); IKeystoreManager mgr; if (serverHTTPSKeystoreValue != null) mgr = KeystoreManagerFactory.make("", serverHTTPSKeystoreValue); else mgr = KeystoreManagerFactory.make(""); mgr.remove(alias); parameters.setParameter(LiveLinkParameters.serverHTTPSKeystore, mgr.getString()); } else if (serverConfigOp.equals("Add")) { String alias = IDFactory.make(threadContext); byte[] certificateValue = variableContext.getBinaryBytes("servercertificate"); serverHTTPSKeystoreValue = parameters.getParameter(LiveLinkParameters.serverHTTPSKeystore); IKeystoreManager mgr; if (serverHTTPSKeystoreValue != null) mgr = KeystoreManagerFactory.make("", serverHTTPSKeystoreValue); else mgr = KeystoreManagerFactory.make(""); java.io.InputStream is = new java.io.ByteArrayInputStream(certificateValue); String certError = null; try { mgr.importCertificate(alias, is); } catch (Throwable e) { certError = e.getMessage(); } finally { try { is.close(); } catch (IOException e) { // Eat this exception } } if (certError != null) { return "Illegal certificate: " + certError; } parameters.setParameter(LiveLinkParameters.serverHTTPSKeystore, mgr.getString()); } } // Ingest parameters String ingestProtocol = variableContext.getParameter("ingestprotocol"); if (ingestProtocol != null) parameters.setParameter(LiveLinkParameters.ingestProtocol, ingestProtocol); String ingestPort = variableContext.getParameter("ingestport"); if (ingestPort != null) parameters.setParameter(LiveLinkParameters.ingestPort, ingestPort); String ingestCgiPath = variableContext.getParameter("ingestcgipath"); if (ingestCgiPath != null) parameters.setParameter(LiveLinkParameters.ingestCgiPath, ingestCgiPath); String ingestNtlmDomain = variableContext.getParameter("ingestntlmdomain"); if (ingestNtlmDomain != null) parameters.setParameter(LiveLinkParameters.ingestNtlmDomain, ingestNtlmDomain); String ingestNtlmUsername = variableContext.getParameter("ingestntlmusername"); if (ingestNtlmUsername != null) parameters.setParameter(LiveLinkParameters.ingestNtlmUsername, ingestNtlmUsername); String ingestNtlmPassword = variableContext.getParameter("ingestntlmpassword"); if (ingestNtlmPassword != null) parameters.setObfuscatedParameter(LiveLinkParameters.ingestNtlmPassword, variableContext.mapKeyToPassword(ingestNtlmPassword)); String ingestKeystoreValue = variableContext.getParameter("ingestkeystoredata"); if (ingestKeystoreValue != null) parameters.setParameter(LiveLinkParameters.ingestKeystore, ingestKeystoreValue); String ingestConfigOp = variableContext.getParameter("ingestconfigop"); if (ingestConfigOp != null) { if (ingestConfigOp.equals("Delete")) { String alias = variableContext.getParameter("ingestkeystorealias"); ingestKeystoreValue = parameters.getParameter(LiveLinkParameters.ingestKeystore); IKeystoreManager mgr; if (ingestKeystoreValue != null) mgr = KeystoreManagerFactory.make("", ingestKeystoreValue); else mgr = KeystoreManagerFactory.make(""); mgr.remove(alias); parameters.setParameter(LiveLinkParameters.ingestKeystore, mgr.getString()); } else if (ingestConfigOp.equals("Add")) { String alias = IDFactory.make(threadContext); byte[] certificateValue = variableContext.getBinaryBytes("ingestcertificate"); ingestKeystoreValue = parameters.getParameter(LiveLinkParameters.ingestKeystore); IKeystoreManager mgr; if (ingestKeystoreValue != null) mgr = KeystoreManagerFactory.make("", ingestKeystoreValue); else mgr = KeystoreManagerFactory.make(""); java.io.InputStream is = new java.io.ByteArrayInputStream(certificateValue); String certError = null; try { mgr.importCertificate(alias, is); } catch (Throwable e) { certError = e.getMessage(); } finally { try { is.close(); } catch (IOException e) { // Eat this exception } } if (certError != null) { return "Illegal certificate: " + certError; } parameters.setParameter(LiveLinkParameters.ingestKeystore, mgr.getString()); } } return null; } /** View configuration. * This method is called in the body section of the connector's view configuration page. Its purpose is to present the connection information to the user. * The coder can presume that the HTML that is output from this configuration will be within appropriate <html> and <body> tags. *@param threadContext is the local thread context. *@param out is the output to which any HTML should be sent. *@param parameters are the configuration parameters, as they currently exist, for this connection being configured. */ @Override public void viewConfiguration(IThreadContext threadContext, IHTTPOutput out, Locale locale, ConfigParams parameters) throws ManifoldCFException, IOException { out.print("<table class=\"displaytable\">\n" + " <tr>\n" + " <td class=\"description\" colspan=\"1\"><nobr>" + Messages.getBodyString(locale, "LivelinkConnector.Parameters") + "</nobr></td>\n" + " <td class=\"value\" colspan=\"3\">\n"); Iterator iter = parameters.listParameters(); while (iter.hasNext()) { String param = (String) iter.next(); String value = parameters.getParameter(param); if (param.length() >= "password".length() && param.substring(param.length() - "password".length()).equalsIgnoreCase("password")) { out.print(" <nobr>" + org.apache.manifoldcf.ui.util.Encoder.bodyEscape(param) + "=********</nobr><br/>\n"); } else if (param.length() >= "keystore".length() && param.substring(param.length() - "keystore".length()).equalsIgnoreCase("keystore") || param.length() > "truststore".length() && param .substring(param.length() - "truststore".length()).equalsIgnoreCase("truststore")) { IKeystoreManager kmanager = KeystoreManagerFactory.make("", value); out.print(" <nobr>" + org.apache.manifoldcf.ui.util.Encoder.bodyEscape(param) + "=<" + Integer.toString(kmanager.getContents().length) + Messages.getBodyString(locale, "LivelinkConnector.certificates") + "></nobr><br/>\n"); } else { out.print(" <nobr>" + org.apache.manifoldcf.ui.util.Encoder.bodyEscape(param) + "=" + org.apache.manifoldcf.ui.util.Encoder.bodyEscape(value) + "</nobr><br/>\n"); } } out.print(" </td>\n" + " </tr>\n" + "</table>\n"); } /** Output the specification header section. * This method is called in the head section of a job page which has selected a repository connection of the * current type. Its purpose is to add the required tabs to the list, and to output any javascript methods * that might be needed by the job editing HTML. * The connector will be connected before this method can be called. *@param out is the output to which any HTML should be sent. *@param locale is the locale the output is preferred to be in. *@param ds is the current document specification for this job. *@param connectionSequenceNumber is the unique number of this connection within the job. *@param tabsArray is an array of tab names. Add to this array any tab names that are specific to the connector. */ @Override public void outputSpecificationHeader(IHTTPOutput out, Locale locale, Specification ds, int connectionSequenceNumber, List<String> tabsArray) throws ManifoldCFException, IOException { tabsArray.add(Messages.getString(locale, "LivelinkConnector.Paths")); tabsArray.add(Messages.getString(locale, "LivelinkConnector.Filters")); tabsArray.add(Messages.getString(locale, "LivelinkConnector.Security")); tabsArray.add(Messages.getString(locale, "LivelinkConnector.Metadata")); String seqPrefix = "s" + connectionSequenceNumber + "_"; out.print("<script type=\"text/javascript\">\n" + "<!--\n" + "\n" + "function " + seqPrefix + "SpecOp(n, opValue, anchorvalue)\n" + "{\n" + " eval(\"editjob.\"+n+\".value = \\\"\"+opValue+\"\\\"\");\n" + " postFormSetAnchor(anchorvalue);\n" + "}\n" + "\n" + "function " + seqPrefix + "SpecAddToPath(anchorvalue)\n" + "{\n" + " if (editjob." + seqPrefix + "pathaddon.value == \"\")\n" + " {\n" + " alert(\"" + Messages.getBodyJavascriptString(locale, "LivelinkConnector.SelectAFolderFirst") + "\");\n" + " editjob." + seqPrefix + "pathaddon.focus();\n" + " return;\n" + " }\n" + "\n" + " " + seqPrefix + "SpecOp(\"" + seqPrefix + "pathop\",\"AddToPath\",anchorvalue);\n" + "}\n" + "\n" + "function " + seqPrefix + "SpecAddFilespec(anchorvalue)\n" + "{\n" + " if (editjob." + seqPrefix + "specfile.value == \"\")\n" + " {\n" + " alert(\"" + Messages.getBodyJavascriptString(locale, "LivelinkConnector.TypeInAFileSpecification") + "\");\n" + " editjob." + seqPrefix + "specfile.focus();\n" + " return;\n" + " }\n" + " " + seqPrefix + "SpecOp(\"" + seqPrefix + "fileop\",\"Add\",anchorvalue);\n" + "}\n" + "\n" + "function " + seqPrefix + "SpecAddToken(anchorvalue)\n" + "{\n" + " if (editjob." + seqPrefix + "spectoken.value == \"\")\n" + " {\n" + " alert(\"" + Messages.getBodyJavascriptString(locale, "LivelinkConnector.TypeInAnAccessToken") + "\");\n" + " editjob." + seqPrefix + "spectoken.focus();\n" + " return;\n" + " }\n" + " " + seqPrefix + "SpecOp(\"" + seqPrefix + "accessop\",\"Add\",anchorvalue);\n" + "}\n" + "\n" + "function " + seqPrefix + "SpecAddToMetadata(anchorvalue)\n" + "{\n" + " if (editjob." + seqPrefix + "metadataaddon.value == \"\")\n" + " {\n" + " alert(\"" + Messages.getBodyJavascriptString(locale, "LivelinkConnector.SelectAFolderFirst") + "\");\n" + " editjob." + seqPrefix + "metadataaddon.focus();\n" + " return;\n" + " }\n" + " " + seqPrefix + "SpecOp(\"" + seqPrefix + "metadataop\",\"AddToPath\",anchorvalue);\n" + "}\n" + "\n" + "function " + seqPrefix + "SpecSetWorkspace(anchorvalue)\n" + "{\n" + " if (editjob." + seqPrefix + "metadataaddon.value == \"\")\n" + " {\n" + " alert(\"" + Messages.getBodyJavascriptString(locale, "LivelinkConnector.SelectAWorkspaceFirst") + "\");\n" + " editjob." + seqPrefix + "metadataaddon.focus();\n" + " return;\n" + " }\n" + " " + seqPrefix + "SpecOp(\"" + seqPrefix + "metadataop\",\"SetWorkspace\",anchorvalue);\n" + "}\n" + "\n" + "function " + seqPrefix + "SpecAddCategory(anchorvalue)\n" + "{\n" + " if (editjob." + seqPrefix + "categoryaddon.value == \"\")\n" + " {\n" + " alert(\"" + Messages.getBodyJavascriptString(locale, "LivelinkConnector.SelectACategoryFirst") + "\");\n" + " editjob." + seqPrefix + "categoryaddon.focus();\n" + " return;\n" + " }\n" + " " + seqPrefix + "SpecOp(\"" + seqPrefix + "metadataop\",\"AddCategory\",anchorvalue);\n" + "}\n" + "\n" + "function " + seqPrefix + "SpecAddMetadata(anchorvalue)\n" + "{\n" + " if (editjob." + seqPrefix + "attributeselect.value == \"\" && editjob." + seqPrefix + "attributeall.value == \"\")\n" + " {\n" + " alert(\"" + Messages.getBodyJavascriptString(locale, "LivelinkConnector.SelectAtLeastOneAttributeFirst") + "\");\n" + " editjob." + seqPrefix + "attributeselect.focus();\n" + " return;\n" + " }\n" + " " + seqPrefix + "SpecOp(\"" + seqPrefix + "metadataop\",\"Add\",anchorvalue);\n" + "}\n" + "\n" + "function " + seqPrefix + "SpecAddMapping(anchorvalue)\n" + "{\n" + " if (editjob." + seqPrefix + "specmatch.value == \"\")\n" + " {\n" + " alert(\"" + Messages.getBodyJavascriptString(locale, "LivelinkConnector.MatchStringCannotBeEmpty") + "\");\n" + " editjob." + seqPrefix + "specmatch.focus();\n" + " return;\n" + " }\n" + " if (!isRegularExpression(editjob." + seqPrefix + "specmatch.value))\n" + " {\n" + " alert(\"" + Messages.getBodyJavascriptString(locale, "LivelinkConnector.MatchStringMustBeValidRegularExpression") + "\");\n" + " editjob." + seqPrefix + "specmatch.focus();\n" + " return;\n" + " }\n" + " " + seqPrefix + "SpecOp(\"" + seqPrefix + "specmappingop\",\"Add\",anchorvalue);\n" + "}\n" + "//-->\n" + "</script>\n"); } /** Output the specification body section. * This method is called in the body section of a job page which has selected a repository connection of the * current type. Its purpose is to present the required form elements for editing. * The coder can presume that the HTML that is output from this configuration will be within appropriate * <html>, <body>, and <form> tags. The name of the form is always "editjob". * The connector will be connected before this method can be called. *@param out is the output to which any HTML should be sent. *@param locale is the locale the output is preferred to be in. *@param ds is the current document specification for this job. *@param connectionSequenceNumber is the unique number of this connection within the job. *@param actualSequenceNumber is the connection within the job that has currently been selected. *@param tabName is the current tab name. (actualSequenceNumber, tabName) form a unique tuple within * the job. */ @Override public void outputSpecificationBody(IHTTPOutput out, Locale locale, Specification ds, int connectionSequenceNumber, int actualSequenceNumber, String tabName) throws ManifoldCFException, IOException { String seqPrefix = "s" + connectionSequenceNumber + "_"; int i; int k; // Paths tab boolean userWorkspaces = false; i = 0; while (i < ds.getChildCount()) { SpecificationNode sn = ds.getChild(i++); if (sn.getType().equals("userworkspace")) { String value = sn.getAttributeValue("value"); if (value != null && value.equals("true")) userWorkspaces = true; } } if (tabName.equals(Messages.getString(locale, "LivelinkConnector.Paths")) && connectionSequenceNumber == actualSequenceNumber) { out.print("<table class=\"displaytable\">\n" + " <tr><td class=\"separator\" colspan=\"2\"><hr/></td></tr>\n" + " <tr>\n" + " <td class=\"description\">\n" + " <nobr>" + Messages.getBodyString(locale, "LivelinkConnector.CrawlUserWorkspaces") + "</nobr>\n" + " </td>\n" + " <td class=\"value\">\n" + " <input type=\"checkbox\" name=\"" + seqPrefix + "userworkspace\" value=\"true\"" + (userWorkspaces ? " checked=\"true\"" : "") + "/>\n" + " <input type=\"hidden\" name=\"" + seqPrefix + "userworkspace_present\" value=\"true\"/>\n" + " </td>\n" + " </tr>\n" + " <tr><td class=\"separator\" colspan=\"2\"><hr/></td></tr>\n"); // Now, loop through paths i = 0; k = 0; while (i < ds.getChildCount()) { SpecificationNode sn = ds.getChild(i++); if (sn.getType().equals("startpoint")) { String pathDescription = "_" + Integer.toString(k); String pathOpName = seqPrefix + "pathop" + pathDescription; out.print( " <tr>\n" + " <td class=\"description\">\n" + " <input type=\"hidden\" name=\"" + seqPrefix + "specpath" + pathDescription + "\" value=\"" + org.apache.manifoldcf.ui.util.Encoder .attributeEscape(sn.getAttributeValue("path")) + "\"/>\n" + " <input type=\"hidden\" name=\"" + pathOpName + "\" value=\"\"/>\n" + " <a name=\"" + seqPrefix + "path_" + Integer.toString(k) + "\">\n" + " <input type=\"button\" value=\"Delete\" onClick='Javascript:" + seqPrefix + "SpecOp(\"" + pathOpName + "\",\"Delete\",\"" + seqPrefix + "path_" + Integer.toString(k) + "\")' alt=\"" + Messages.getAttributeString(locale, "LivelinkConnector.DeletePath") + Integer.toString(k) + "\"/>\n" + " </a>\n" + " </td>\n" + " <td class=\"value\">\n" + " " + ((sn.getAttributeValue("path").length() == 0) ? "(root)" : org.apache.manifoldcf.ui.util.Encoder .bodyEscape(sn.getAttributeValue("path"))) + "\n" + " </td>\n" + " </tr>\n"); k++; } } if (k == 0) { out.print(" <tr>\n" + " <td class=\"message\" colspan=\"2\">" + Messages.getBodyString(locale, "LivelinkConnector.NoStartingPointsDefined") + "</td>\n" + " </tr>\n"); } out.print(" <tr><td class=\"lightseparator\" colspan=\"2\"><hr/></td></tr>\n" + " <tr>\n" + " <td class=\"description\">\n" + " <input type=\"hidden\" name=\"" + seqPrefix + "pathcount\" value=\"" + Integer.toString(k) + "\"/>\n"); String pathSoFar = (String) currentContext.get(seqPrefix + "specpath"); if (pathSoFar == null) pathSoFar = ""; // Grab next folder/project list try { String[] childList; childList = getChildFolderNames(pathSoFar); if (childList == null) { // Illegal path - set it back pathSoFar = ""; childList = getChildFolderNames(""); if (childList == null) throw new ManifoldCFException("Can't find any children for root folder"); } out.print(" <input type=\"hidden\" name=\"" + seqPrefix + "specpath\" value=\"" + org.apache.manifoldcf.ui.util.Encoder.attributeEscape(pathSoFar) + "\"/>\n" + " <input type=\"hidden\" name=\"" + seqPrefix + "pathop\" value=\"\"/>\n" + " <a name=\"" + seqPrefix + "path_" + Integer.toString(k) + "\">\n" + " <input type=\"button\" value=\"Add\" onClick='Javascript:" + seqPrefix + "SpecOp(\"" + seqPrefix + "pathop\",\"Add\",\"" + seqPrefix + "path_" + Integer.toString(k + 1) + "\")' alt=\"" + Messages.getAttributeString(locale, "LivelinkConnector.AddPath") + "\"/>\n" + " </a> \n" + " </td>\n" + " <td class=\"value\">\n" + " " + ((pathSoFar.length() == 0) ? "(root)" : org.apache.manifoldcf.ui.util.Encoder.bodyEscape(pathSoFar)) + "\n"); if (pathSoFar.length() > 0) { out.print(" <input type=\"button\" value=\"-\" onClick='Javascript:" + seqPrefix + "SpecOp(\"" + seqPrefix + "pathop\",\"Up\",\"" + seqPrefix + "path_" + Integer.toString(k) + "\")' alt=\"Back up path\"/>\n"); } if (childList.length > 0) { out.print(" <input type=\"button\" value=\"+\" onClick='Javascript:" + seqPrefix + "SpecAddToPath(\"" + seqPrefix + "path_" + Integer.toString(k) + "\")' alt=\"" + Messages.getAttributeString(locale, "LivelinkConnector.AddToPath") + "\"/> \n" + " <select multiple=\"false\" name=\"" + seqPrefix + "pathaddon\" size=\"2\">\n" + " <option value=\"\" selected=\"selected\">" + Messages.getBodyString(locale, "LivelinkConnector.PickAFolder") + "</option>\n"); int j = 0; while (j < childList.length) { out.print(" <option value=\"" + org.apache.manifoldcf.ui.util.Encoder.attributeEscape(childList[j]) + "\">" + org.apache.manifoldcf.ui.util.Encoder.bodyEscape(childList[j]) + "</option>\n"); j++; } out.print(" </select>\n"); } } catch (ServiceInterruption e) { //e.printStackTrace(); out.println(org.apache.manifoldcf.ui.util.Encoder.bodyEscape(e.getMessage())); } catch (ManifoldCFException e) { //e.printStackTrace(); out.println(org.apache.manifoldcf.ui.util.Encoder.bodyEscape(e.getMessage())); } out.print(" </td>\n" + " </tr>\n" + "</table>\n"); } else { // Now, loop through paths i = 0; k = 0; while (i < ds.getChildCount()) { SpecificationNode sn = ds.getChild(i++); if (sn.getType().equals("startpoint")) { String pathDescription = "_" + Integer.toString(k); out.print("<input type=\"hidden\" name=\"" + seqPrefix + "specpath" + pathDescription + "\" value=\"" + org.apache.manifoldcf.ui.util.Encoder.attributeEscape(sn.getAttributeValue("path")) + "\"/>\n"); k++; } } out.print("<input type=\"hidden\" name=\"" + seqPrefix + "pathcount\" value=\"" + Integer.toString(k) + "\"/>\n" + "<input type=\"hidden\" name=\"" + seqPrefix + "userworkspace\" value=\"" + (userWorkspaces ? "true" : "false") + "\"/>\n" + "<input type=\"hidden\" name=\"" + seqPrefix + "userworkspace_present\" value=\"true\"/>\n"); } // Filter tab if (tabName.equals(Messages.getString(locale, "LivelinkConnector.Filters")) && connectionSequenceNumber == actualSequenceNumber) { out.print("<table class=\"displaytable\">\n" + " <tr><td class=\"separator\" colspan=\"2\"><hr/></td></tr>\n"); // Next, go through include/exclude filespecs i = 0; k = 0; while (i < ds.getChildCount()) { SpecificationNode sn = ds.getChild(i++); if (sn.getType().equals("include") || sn.getType().equals("exclude")) { String fileSpecDescription = "_" + Integer.toString(k); String fileOpName = seqPrefix + "fileop" + fileSpecDescription; String filespec = sn.getAttributeValue("filespec"); out.print(" <tr>\n" + " <td class=\"description\">\n" + " <input type=\"hidden\" name=\"" + seqPrefix + "specfiletype" + fileSpecDescription + "\" value=\"" + sn.getType() + "\"/>\n" + " <input type=\"hidden\" name=\"" + fileOpName + "\" value=\"\"/>\n" + " <a name=\"" + seqPrefix + "filespec_" + Integer.toString(k) + "\">\n" + " <input type=\"button\" value=\"Delete\" onClick='Javascript:" + seqPrefix + "SpecOp(\"" + fileOpName + "\",\"Delete\",\"" + seqPrefix + "filespec_" + Integer.toString(k) + "\")' alt=\"" + Messages.getAttributeString(locale, "LivelinkConnector.DeleteFilespec") + Integer.toString(k) + "\"/>\n" + " </a>\n" + " </td>\n" + " <td class=\"value\">\n" + " " + (sn.getType().equals("include") ? "Include:" : "") + "\n" + " " + (sn.getType().equals("exclude") ? "Exclude:" : "") + "\n" + " <input type=\"hidden\" name=\"" + seqPrefix + "specfile" + fileSpecDescription + "\" value=\"" + org.apache.manifoldcf.ui.util.Encoder.attributeEscape(filespec) + "\"/>\n" + " " + org.apache.manifoldcf.ui.util.Encoder.bodyEscape(filespec) + "\n" + " </td>\n" + " </tr>\n"); k++; } } if (k == 0) { out.print(" <tr>\n" + " <td class=\"message\" colspan=\"2\">" + Messages.getBodyString(locale, "LivelinkConnector.NoIncludeExcludeFilesDefined") + "</td>\n" + " </tr>\n"); } out.print(" <tr><td class=\"lightseparator\" colspan=\"2\"><hr/></td></tr>\n" + " <tr>\n" + " <td class=\"description\">\n" + " <input type=\"hidden\" name=\"" + seqPrefix + "filecount\" value=\"" + Integer.toString(k) + "\"/>\n" + " <input type=\"hidden\" name=\"" + seqPrefix + "fileop\" value=\"\"/>\n" + " <a name=\"" + seqPrefix + "filespec_" + Integer.toString(k) + "\">\n" + " <input type=\"button\" value=\"Add\" onClick='Javascript:" + seqPrefix + "SpecAddFilespec(\"" + seqPrefix + "filespec_" + Integer.toString(k + 1) + "\")' alt=\"" + Messages.getAttributeString(locale, "LivelinkConnector.AddFileSpecification") + "\"/>\n" + " </a> \n" + " </td>\n" + " <td class=\"value\">\n" + " <select name=\"" + seqPrefix + "specfiletype\" size=\"1\">\n" + " <option value=\"include\" selected=\"selected\">" + Messages.getBodyString(locale, "LivelinkConnector.Include") + "</option>\n" + " <option value=\"exclude\">" + Messages.getBodyString(locale, "LivelinkConnector.Exclude") + "</option>\n" + " </select> \n" + " <input type=\"text\" size=\"30\" name=\"" + seqPrefix + "specfile\" value=\"\"/>\n" + " </td>\n" + " </tr>\n" + "</table>\n"); } else { // Next, go through include/exclude filespecs i = 0; k = 0; while (i < ds.getChildCount()) { SpecificationNode sn = ds.getChild(i++); if (sn.getType().equals("include") || sn.getType().equals("exclude")) { String fileSpecDescription = "_" + Integer.toString(k); String filespec = sn.getAttributeValue("filespec"); out.print("<input type=\"hidden\" name=\"" + seqPrefix + "specfiletype" + fileSpecDescription + "\" value=\"" + sn.getType() + "\"/>\n" + "<input type=\"hidden\" name=\"" + seqPrefix + "specfile" + fileSpecDescription + "\" value=\"" + org.apache.manifoldcf.ui.util.Encoder.attributeEscape(filespec) + "\"/>\n"); k++; } } out.print("<input type=\"hidden\" name=\"" + seqPrefix + "filecount\" value=\"" + Integer.toString(k) + "\"/>\n"); } // Security tab // Find whether security is on or off i = 0; boolean securityOn = true; while (i < ds.getChildCount()) { SpecificationNode sn = ds.getChild(i++); if (sn.getType().equals("security")) { String securityValue = sn.getAttributeValue("value"); if (securityValue.equals("off")) securityOn = false; else if (securityValue.equals("on")) securityOn = true; } } if (tabName.equals(Messages.getString(locale, "LivelinkConnector.Security")) && connectionSequenceNumber == actualSequenceNumber) { out.print("<table class=\"displaytable\">\n" + " <tr><td class=\"separator\" colspan=\"2\"><hr/></td></tr>\n" + " <tr>\n" + " <td class=\"description\"><nobr>" + Messages.getBodyString(locale, "LivelinkConnector.SecurityColon") + "</nobr></td>\n" + " <td class=\"value\">\n" + " <input type=\"radio\" name=\"" + seqPrefix + "specsecurity\" value=\"on\" " + (securityOn ? "checked=\"true\"" : "") + " />" + Messages.getBodyString(locale, "LivelinkConnector.Enabled") + "\n" + " <input type=\"radio\" name=\"" + seqPrefix + "specsecurity\" value=\"off\" " + ((securityOn == false) ? "checked=\"true\"" : "") + " />" + Messages.getBodyString(locale, "LivelinkConnector.Disabled") + "\n" + " </td>\n" + " </tr>\n" + " <tr><td class=\"separator\" colspan=\"2\"><hr/></td></tr>\n"); // Go through forced ACL i = 0; k = 0; while (i < ds.getChildCount()) { SpecificationNode sn = ds.getChild(i++); if (sn.getType().equals("access")) { String accessDescription = "_" + Integer.toString(k); String accessOpName = seqPrefix + "accessop" + accessDescription; String token = sn.getAttributeValue("token"); out.print(" <tr>\n" + " <td class=\"description\">\n" + " <input type=\"hidden\" name=\"" + accessOpName + "\" value=\"\"/>\n" + " <input type=\"hidden\" name=\"" + seqPrefix + "spectoken" + accessDescription + "\" value=\"" + org.apache.manifoldcf.ui.util.Encoder.attributeEscape(token) + "\"/>\n" + " <a name=\"" + seqPrefix + "token_" + Integer.toString(k) + "\">\n" + " <input type=\"button\" value=\"Delete\" onClick='Javascript:" + seqPrefix + "SpecOp(\"" + accessOpName + "\",\"Delete\",\"" + seqPrefix + "token_" + Integer.toString(k) + "\")' alt=\"" + Messages.getAttributeString(locale, "LivelinkConnector.DeleteToken") + Integer.toString(k) + "\"/>\n" + " </a> \n" + " </td>\n" + " <td class=\"value\">\n" + " " + org.apache.manifoldcf.ui.util.Encoder.bodyEscape(token) + "\n" + " </td>\n" + " </tr>\n"); k++; } } if (k == 0) { out.print(" <tr>\n" + " <td class=\"message\" colspan=\"2\">" + Messages.getBodyString(locale, "LivelinkConnector.NoAccessTokensPresent") + "</td>\n" + " </tr>\n"); } out.print(" <tr><td class=\"lightseparator\" colspan=\"2\"><hr/></td></tr>\n" + " <tr>\n" + " <td class=\"description\">\n" + " <input type=\"hidden\" name=\"" + seqPrefix + "tokencount\" value=\"" + Integer.toString(k) + "\"/>\n" + " <input type=\"hidden\" name=\"" + seqPrefix + "accessop\" value=\"\"/>\n" + " <a name=\"" + seqPrefix + "token_" + Integer.toString(k) + "\">\n" + " <input type=\"button\" value=\"Add\" onClick='Javascript:" + seqPrefix + "SpecAddToken(\"" + seqPrefix + "token_" + Integer.toString(k + 1) + "\")' alt=\"" + Messages.getAttributeString(locale, "LivelinkConnector.AddAccessToken") + "\"/>\n" + " </a> \n" + " </td>\n" + " <td class=\"value\">\n" + " <input type=\"text\" size=\"30\" name=\"" + seqPrefix + "spectoken\" value=\"\"/>\n" + " </td>\n" + " </tr>\n" + "</table>\n"); } else { out.print("<input type=\"hidden\" name=\"" + seqPrefix + "specsecurity\" value=\"" + (securityOn ? "on" : "off") + "\"/>\n"); // Finally, go through forced ACL i = 0; k = 0; while (i < ds.getChildCount()) { SpecificationNode sn = ds.getChild(i++); if (sn.getType().equals("access")) { String accessDescription = "_" + Integer.toString(k); String token = sn.getAttributeValue("token"); out.print("<input type=\"hidden\" name=\"" + seqPrefix + "spectoken" + accessDescription + "\" value=\"" + org.apache.manifoldcf.ui.util.Encoder.attributeEscape(token) + "\"/>\n"); k++; } } out.print("<input type=\"hidden\" name=\"" + seqPrefix + "tokencount\" value=\"" + Integer.toString(k) + "\"/>\n"); } // Metadata tab // Find the path-value metadata attribute name i = 0; String pathNameAttribute = ""; String pathNameSeparator = "/"; while (i < ds.getChildCount()) { SpecificationNode sn = ds.getChild(i++); if (sn.getType().equals("pathnameattribute")) { pathNameAttribute = sn.getAttributeValue("value"); if (sn.getAttributeValue("separator") != null) pathNameSeparator = sn.getAttributeValue("separator"); } } // Find the path-value mapping data i = 0; MatchMap matchMap = new MatchMap(); while (i < ds.getChildCount()) { SpecificationNode sn = ds.getChild(i++); if (sn.getType().equals("pathmap")) { String pathMatch = sn.getAttributeValue("match"); String pathReplace = sn.getAttributeValue("replace"); matchMap.appendMatchPair(pathMatch, pathReplace); } } i = 0; String ingestAllMetadata = "false"; while (i < ds.getChildCount()) { SpecificationNode sn = ds.getChild(i++); if (sn.getType().equals("allmetadata")) { ingestAllMetadata = sn.getAttributeValue("all"); if (ingestAllMetadata == null) ingestAllMetadata = "false"; } } if (tabName.equals(Messages.getString(locale, "LivelinkConnector.Metadata")) && connectionSequenceNumber == actualSequenceNumber) { out.print("<input type=\"hidden\" name=\"" + seqPrefix + "specmappingcount\" value=\"" + Integer.toString(matchMap.getMatchCount()) + "\"/>\n" + "<input type=\"hidden\" name=\"" + seqPrefix + "specmappingop\" value=\"\"/>\n" + "\n" + "<table class=\"displaytable\">\n" + " <tr><td class=\"separator\" colspan=\"4\"><hr/></td></tr>\n" + " <tr>\n" + " <td class=\"description\" colspan=\"1\"><nobr>" + Messages.getBodyString(locale, "LivelinkConnector.IngestALLMetadata") + "</nobr></td>\n" + " <td class=\"value\" colspan=\"3\">\n" + " <nobr><input type=\"radio\" name=\"" + seqPrefix + "specallmetadata\" value=\"true\" " + (ingestAllMetadata.equals("true") ? "checked=\"true\"" : "") + "/>" + Messages.getBodyString(locale, "LivelinkConnector.Yes") + "</nobr> \n" + " <nobr><input type=\"radio\" name=\"" + seqPrefix + "specallmetadata\" value=\"false\" " + (ingestAllMetadata.equals("false") ? "checked=\"true\"" : "") + "/>" + Messages.getBodyString(locale, "LivelinkConnector.No") + "</nobr>\n" + " </td>\n" + " </tr>\n" + " <tr><td class=\"separator\" colspan=\"4\"><hr/></td></tr>\n"); // Go through the selected metadata attributes i = 0; k = 0; while (i < ds.getChildCount()) { SpecificationNode sn = ds.getChild(i++); if (sn.getType().equals("metadata")) { String accessDescription = "_" + Integer.toString(k); String accessOpName = seqPrefix + "metadataop" + accessDescription; String categoryPath = sn.getAttributeValue("category"); String isAll = sn.getAttributeValue("all"); if (isAll == null) isAll = "false"; String attributeName = sn.getAttributeValue("attribute"); if (attributeName == null) attributeName = ""; out.print(" <tr>\n" + " <td class=\"description\" colspan=\"1\">\n" + " <input type=\"hidden\" name=\"" + accessOpName + "\" value=\"\"/>\n" + " <input type=\"hidden\" name=\"" + seqPrefix + "speccategory" + accessDescription + "\" value=\"" + org.apache.manifoldcf.ui.util.Encoder.attributeEscape(categoryPath) + "\"/>\n" + " <input type=\"hidden\" name=\"" + seqPrefix + "specattributeall" + accessDescription + "\" value=\"" + isAll + "\"/>\n" + " <input type=\"hidden\" name=\"" + seqPrefix + "specattribute" + accessDescription + "\" value=\"" + org.apache.manifoldcf.ui.util.Encoder.attributeEscape(attributeName) + "\"/>\n" + " <a name=\"" + seqPrefix + "metadata_" + Integer.toString(k) + "\">\n" + " <input type=\"button\" value=\"Delete\" onClick='Javascript:" + seqPrefix + "SpecOp(\"" + accessOpName + "\",\"Delete\",\"" + seqPrefix + "metadata_" + Integer.toString(k) + "\")' alt=\"" + Messages.getAttributeString(locale, "LivelinkConnector.DeleteMetadata") + Integer.toString(k) + "\"/>\n" + " </a> \n" + " </td>\n" + " <td class=\"value\" colspan=\"3\">\n" + " " + org.apache.manifoldcf.ui.util.Encoder.bodyEscape(categoryPath) + ":" + ((isAll != null && isAll.equals("true")) ? "(All metadata attributes)" : org.apache.manifoldcf.ui.util.Encoder.bodyEscape(attributeName)) + "\n" + " </td>\n" + " </tr>\n"); k++; } } if (k == 0) { out.print(" <tr>\n" + " <td class=\"message\" colspan=\"4\">" + Messages.getBodyString(locale, "LivelinkConnector.NoMetadataSpecified") + "</td>\n" + " </tr>\n"); } out.print(" <tr><td class=\"lightseparator\" colspan=\"4\"><hr/></td></tr>\n" + " <tr>\n" + " <td class=\"description\" colspan=\"1\">\n" + " <a name=\"" + seqPrefix + "metadata_" + Integer.toString(k) + "\"></a>\n" + " <input type=\"hidden\" name=\"" + seqPrefix + "metadatacount\" value=\"" + Integer.toString(k) + "\"/>\n"); String categorySoFar = (String) currentContext.get(seqPrefix + "speccategory"); if (categorySoFar == null) categorySoFar = ""; // Grab next folder/project list, and the appropriate category list try { String[] childList = null; String[] workspaceList = null; String[] categoryList = null; String[] attributeList = null; if (categorySoFar.length() == 0) { workspaceList = getWorkspaceNames(); } else { attributeList = getCategoryAttributes(categorySoFar); if (attributeList == null) { childList = getChildFolderNames(categorySoFar); if (childList == null) { // Illegal path - set it back categorySoFar = ""; childList = getChildFolderNames(""); if (childList == null) throw new ManifoldCFException("Can't find any children for root folder"); } categoryList = getChildCategoryNames(categorySoFar); if (categoryList == null) throw new ManifoldCFException("Can't find any categories for root folder folder"); } } out.print(" <input type=\"hidden\" name=\"" + seqPrefix + "speccategory\" value=\"" + org.apache.manifoldcf.ui.util.Encoder.attributeEscape(categorySoFar) + "\"/>\n" + " <input type=\"hidden\" name=\"" + seqPrefix + "metadataop\" value=\"\"/>\n"); if (attributeList != null) { // We have a valid category! out.print(" <input type=\"button\" value=\"Add\" onClick='Javascript:" + seqPrefix + "SpecAddMetadata(\"" + seqPrefix + "metadata_" + Integer.toString(k + 1) + "\")' alt=\"" + Messages.getAttributeString(locale, "LivelinkConnector.AddMetadataItem") + "\"/> \n" + " </td>\n" + " <td class=\"value\" colspan=\"3\">\n" + " " + org.apache.manifoldcf.ui.util.Encoder.bodyEscape(categorySoFar) + ":<input type=\"button\" value=\"-\" onClick='Javascript:" + seqPrefix + "SpecOp(\"" + seqPrefix + "metadataop\",\"Up\",\"" + seqPrefix + "metadata_" + Integer.toString(k) + "\")' alt=\"" + Messages.getAttributeString(locale, "LivelinkConnector.BackUpMetadataPath") + "\"/> \n" + " <table class=\"displaytable\">\n" + " <tr>\n" + " <td class=\"value\">\n" + " <input type=\"checkbox\" name=\"" + seqPrefix + "attributeall\" value=\"true\"/>" + Messages.getBodyString(locale, "LivelinkConnector.AllAttributesInThisCategory") + "<br/>\n" + " <select multiple=\"true\" name=\"" + seqPrefix + "attributeselect\" size=\"2\">\n" + " <option value=\"\" selected=\"selected\">" + Messages.getBodyString(locale, "LivelinkConnector.PickAttributes") + "</option>\n"); int l = 0; while (l < attributeList.length) { String attributeName = attributeList[l++]; out.print(" <option value=\"" + org.apache.manifoldcf.ui.util.Encoder.attributeEscape(attributeName) + "\">" + org.apache.manifoldcf.ui.util.Encoder.bodyEscape(attributeName) + "</option>\n"); } out.print(" </select>\n" + " </td>\n" + " </tr>\n" + " </table>\n"); } else if (workspaceList != null) { out.print(" </td>\n" + " <td class=\"value\" colspan=\"3\">\n" + " <input type=\"button\" value=\"+\" onClick='Javascript:" + seqPrefix + "SpecSetWorkspace(\"" + seqPrefix + "metadata_" + Integer.toString(k) + "\")' alt=\"" + Messages.getAttributeString(locale, "LivelinkConnector.AddToMetadataPath") + "\"/> \n" + " <select multiple=\"false\" name=\"" + seqPrefix + "metadataaddon\" size=\"2\">\n" + " <option value=\"\" selected=\"selected\">" + Messages.getBodyString(locale, "LivelinkConnector.PickWorkspace") + "</option>\n"); int j = 0; while (j < workspaceList.length) { out.print(" <option value=\"" + org.apache.manifoldcf.ui.util.Encoder.attributeEscape(workspaceList[j]) + "\">" + org.apache.manifoldcf.ui.util.Encoder.bodyEscape(workspaceList[j]) + "</option>\n"); j++; } out.print(" </select>\n"); } else { out.print(" </td>\n" + " <td class=\"value\" colspan=\"3\">\n" + " " + ((categorySoFar.length() == 0) ? "(root)" : org.apache.manifoldcf.ui.util.Encoder.bodyEscape(categorySoFar)) + " \n"); if (categorySoFar.length() > 0) { out.print( " <input type=\"button\" value=\"-\" onClick='Javascript:" + seqPrefix + "SpecOp(\"" + seqPrefix + "metadataop\",\"Up\",\"" + seqPrefix + "metadata_" + Integer.toString(k) + "\")' alt=\"" + Messages .getAttributeString(locale, "LivelinkConnector.BackUpMetadataPath") + "\"/> \n"); } if (childList.length > 0) { out.print(" <input type=\"button\" value=\"+\" onClick='Javascript:" + seqPrefix + "SpecAddToMetadata(\"" + seqPrefix + "metadata_" + Integer.toString(k) + "\")' alt=\"" + Messages.getAttributeString(locale, "LivelinkConnector.AddToMetadataPath") + "\"/> \n" + " <select multiple=\"false\" name=\"" + seqPrefix + "metadataaddon\" size=\"2\">\n" + " <option value=\"\" selected=\"selected\">" + Messages.getBodyString(locale, "LivelinkConnector.PickAFolder") + "</option>\n"); int j = 0; while (j < childList.length) { out.print(" <option value=\"" + org.apache.manifoldcf.ui.util.Encoder.attributeEscape(childList[j]) + "\">" + org.apache.manifoldcf.ui.util.Encoder.bodyEscape(childList[j]) + "</option>\n"); j++; } out.print(" </select>\n"); } if (categoryList.length > 0) { out.print(" <input type=\"button\" value=\"+\" onClick='Javascript:" + seqPrefix + "SpecAddCategory(\"" + seqPrefix + "metadata_" + Integer.toString(k) + "\")' alt=\"" + Messages.getAttributeString(locale, "LivelinkConnector.AddCategory") + "\"/> \n" + " <select multiple=\"false\" name=\"" + seqPrefix + "categoryaddon\" size=\"2\">\n" + " <option value=\"\" selected=\"selected\">" + Messages.getBodyString(locale, "LivelinkConnector.PickACategory") + "</option>\n"); int j = 0; while (j < categoryList.length) { out.print(" <option value=\"" + org.apache.manifoldcf.ui.util.Encoder.attributeEscape(categoryList[j]) + "\">" + org.apache.manifoldcf.ui.util.Encoder.bodyEscape(categoryList[j]) + "</option>\n"); j++; } out.print(" </select>\n"); } } } catch (ServiceInterruption e) { //e.printStackTrace(); out.println(org.apache.manifoldcf.ui.util.Encoder.bodyEscape(e.getMessage())); } catch (ManifoldCFException e) { //e.printStackTrace(); out.println(org.apache.manifoldcf.ui.util.Encoder.bodyEscape(e.getMessage())); } out.print(" </td>\n" + " </tr>\n" + " <tr><td class=\"separator\" colspan=\"4\"><hr/></td></tr>\n" + " <tr>\n" + " <td class=\"description\" colspan=\"1\"><nobr>" + Messages.getBodyString(locale, "LivelinkConnector.PathAttributeName") + "</nobr></td>\n" + " <td class=\"value\" colspan=\"1\">\n" + " <input type=\"text\" name=\"" + seqPrefix + "specpathnameattribute\" size=\"20\" value=\"" + org.apache.manifoldcf.ui.util.Encoder.attributeEscape(pathNameAttribute) + "\"/>\n" + " </td>\n" + " <td class=\"description\" colspan=\"1\"><nobr>" + Messages.getBodyString(locale, "LivelinkConnector.PathSeparatorString") + "</nobr></td>\n" + " <td class=\"value\" colspan=\"1\">\n" + " <input type=\"text\" name=\"" + seqPrefix + "specpathnameseparator\" size=\"20\" value=\"" + org.apache.manifoldcf.ui.util.Encoder.attributeEscape(pathNameSeparator) + "\"/>\n" + " </td>\n" + " </tr>\n" + " <tr><td class=\"separator\" colspan=\"4\"><hr/></td></tr>\n"); i = 0; while (i < matchMap.getMatchCount()) { String matchString = matchMap.getMatchString(i); String replaceString = matchMap.getReplaceString(i); out.print(" <tr>\n" + " <td class=\"description\">\n" + " <input type=\"hidden\" name=\"" + seqPrefix + "specmappingop_" + Integer.toString(i) + "\" value=\"\"/>\n" + " <a name=\"" + seqPrefix + "mapping_" + Integer.toString(i) + "\">\n" + " <input type=\"button\" onClick='Javascript:" + seqPrefix + "SpecOp(\"" + seqPrefix + "specmappingop_" + Integer.toString(i) + "\",\"Delete\",\"" + seqPrefix + "mapping_" + Integer.toString(i) + "\")' alt=\"" + Messages.getAttributeString(locale, "LivelinkConnector.DeleteMapping") + Integer.toString(i) + "\" value=\"Delete\"/>\n" + " </a>\n" + " </td>\n" + " <td class=\"value\">\n" + " <input type=\"hidden\" name=\"" + seqPrefix + "specmatch_" + Integer.toString(i) + "\" value=\"" + org.apache.manifoldcf.ui.util.Encoder.attributeEscape(matchString) + "\"/>" + org.apache.manifoldcf.ui.util.Encoder.bodyEscape(matchString) + "\n" + " </td>\n" + " <td class=\"value\">==></td>\n" + " <td class=\"value\">\n" + " <input type=\"hidden\" name=\"" + seqPrefix + "specreplace_" + Integer.toString(i) + "\" value=\"" + org.apache.manifoldcf.ui.util.Encoder.attributeEscape(replaceString) + "\"/>" + org.apache.manifoldcf.ui.util.Encoder.bodyEscape(replaceString) + "\n" + " </td>\n" + " </tr>\n"); i++; } if (i == 0) { out.print(" <tr><td colspan=\"4\" class=\"message\">" + Messages.getBodyString(locale, "LivelinkConnector.NoMappingsSpecified") + "</td></tr>\n"); } out.print(" <tr><td class=\"lightseparator\" colspan=\"4\"><hr/></td></tr>\n" + " <tr>\n" + " <td class=\"description\">\n" + " <a name=\"" + seqPrefix + "mapping_" + Integer.toString(i) + "\">\n" + " <input type=\"button\" onClick='Javascript:" + seqPrefix + "SpecAddMapping(\"" + seqPrefix + "mapping_" + Integer.toString(i + 1) + "\")' alt=\"" + Messages.getAttributeString(locale, "LivelinkConnector.AddToMappings") + "\" value=\"Add\"/>\n" + " </a>\n" + " </td>\n" + " <td class=\"value\">Match regexp: <input type=\"text\" name=\"" + seqPrefix + "specmatch\" size=\"32\" value=\"\"/></td>\n" + " <td class=\"value\">==></td>\n" + " <td class=\"value\">Replace string: <input type=\"text\" name=\"" + seqPrefix + "specreplace\" size=\"32\" value=\"\"/></td>\n" + " </tr>\n" + "</table>\n"); } else { out.print("<input type=\"hidden\" name=\"" + seqPrefix + "specallmetadata\" value=\"" + ingestAllMetadata + "\"/>\n"); // Go through the selected metadata attributes i = 0; k = 0; while (i < ds.getChildCount()) { SpecificationNode sn = ds.getChild(i++); if (sn.getType().equals("metadata")) { String accessDescription = "_" + Integer.toString(k); String categoryPath = sn.getAttributeValue("category"); String isAll = sn.getAttributeValue("all"); if (isAll == null) isAll = "false"; String attributeName = sn.getAttributeValue("attribute"); if (attributeName == null) attributeName = ""; out.print("<input type=\"hidden\" name=\"" + seqPrefix + "speccategory" + accessDescription + "\" value=\"" + org.apache.manifoldcf.ui.util.Encoder.attributeEscape(categoryPath) + "\"/>\n" + "<input type=\"hidden\" name=\"" + seqPrefix + "specattributeall" + accessDescription + "\" value=\"" + isAll + "\"/>\n" + "<input type=\"hidden\" name=\"" + seqPrefix + "specattribute" + accessDescription + "\" value=\"" + org.apache.manifoldcf.ui.util.Encoder.attributeEscape(attributeName) + "\"/>\n"); k++; } } out.print("<input type=\"hidden\" name=\"" + seqPrefix + "metadatacount\" value=\"" + Integer.toString(k) + "\"/>\n" + "<input type=\"hidden\" name=\"" + seqPrefix + "specpathnameattribute\" value=\"" + org.apache.manifoldcf.ui.util.Encoder.attributeEscape(pathNameAttribute) + "\"/>\n" + "<input type=\"hidden\" name=\"" + seqPrefix + "specpathnameseparator\" value=\"" + org.apache.manifoldcf.ui.util.Encoder.attributeEscape(pathNameSeparator) + "\"/>\n" + "<input type=\"hidden\" name=\"" + seqPrefix + "specmappingcount\" value=\"" + Integer.toString(matchMap.getMatchCount()) + "\"/>\n"); i = 0; while (i < matchMap.getMatchCount()) { String matchString = matchMap.getMatchString(i); String replaceString = matchMap.getReplaceString(i); out.print("<input type=\"hidden\" name=\"" + seqPrefix + "specmatch_" + Integer.toString(i) + "\" value=\"" + org.apache.manifoldcf.ui.util.Encoder.attributeEscape(matchString) + "\"/>\n" + "<input type=\"hidden\" name=\"" + seqPrefix + "specreplace_" + Integer.toString(i) + "\" value=\"" + org.apache.manifoldcf.ui.util.Encoder.attributeEscape(replaceString) + "\"/>\n"); i++; } } } /** Process a specification post. * This method is called at the start of job's edit or view page, whenever there is a possibility that form * data for a connection has been posted. Its purpose is to gather form information and modify the * document specification accordingly. The name of the posted form is always "editjob". * The connector will be connected before this method can be called. *@param variableContext contains the post data, including binary file-upload information. *@param locale is the locale the output is preferred to be in. *@param ds is the current document specification for this job. *@param connectionSequenceNumber is the unique number of this connection within the job. *@return null if all is well, or a string error message if there is an error that should prevent saving of * the job (and cause a redirection to an error page). */ @Override public String processSpecificationPost(IPostParameters variableContext, Locale locale, Specification ds, int connectionSequenceNumber) throws ManifoldCFException { String seqPrefix = "s" + connectionSequenceNumber + "_"; String userWorkspacesPresent = variableContext.getParameter(seqPrefix + "userworkspace_present"); if (userWorkspacesPresent != null) { String value = variableContext.getParameter(seqPrefix + "userworkspace"); int i = 0; while (i < ds.getChildCount()) { SpecificationNode sn = ds.getChild(i); if (sn.getType().equals("userworkspace")) ds.removeChild(i); else i++; } SpecificationNode sn = new SpecificationNode("userworkspace"); sn.setAttribute("value", value); ds.addChild(ds.getChildCount(), sn); } String xc = variableContext.getParameter(seqPrefix + "pathcount"); if (xc != null) { // Delete all path specs first int i = 0; while (i < ds.getChildCount()) { SpecificationNode sn = ds.getChild(i); if (sn.getType().equals("startpoint")) ds.removeChild(i); else i++; } // Find out how many children were sent int pathCount = Integer.parseInt(xc); // Gather up these i = 0; while (i < pathCount) { String pathDescription = "_" + Integer.toString(i); String pathOpName = seqPrefix + "pathop" + pathDescription; xc = variableContext.getParameter(pathOpName); if (xc != null && xc.equals("Delete")) { // Skip to the next i++; continue; } // Path inserts won't happen until the very end String path = variableContext.getParameter(seqPrefix + "specpath" + pathDescription); SpecificationNode node = new SpecificationNode("startpoint"); node.setAttribute("path", path); ds.addChild(ds.getChildCount(), node); i++; } // See if there's a global add operation String op = variableContext.getParameter(seqPrefix + "pathop"); if (op != null && op.equals("Add")) { String path = variableContext.getParameter("specpath"); SpecificationNode node = new SpecificationNode("startpoint"); node.setAttribute("path", path); ds.addChild(ds.getChildCount(), node); } else if (op != null && op.equals("Up")) { // Strip off end String path = variableContext.getParameter(seqPrefix + "specpath"); int lastSlash = -1; int k = 0; while (k < path.length()) { char x = path.charAt(k++); if (x == '/') { lastSlash = k - 1; continue; } if (x == '\\') k++; } if (lastSlash == -1) path = ""; else path = path.substring(0, lastSlash); currentContext.save(seqPrefix + "specpath", path); } else if (op != null && op.equals("AddToPath")) { String path = variableContext.getParameter(seqPrefix + "specpath"); String addon = variableContext.getParameter(seqPrefix + "pathaddon"); if (addon != null && addon.length() > 0) { StringBuilder sb = new StringBuilder(); int k = 0; while (k < addon.length()) { char x = addon.charAt(k++); if (x == '/' || x == '\\' || x == ':') sb.append('\\'); sb.append(x); } if (path.length() == 0) path = sb.toString(); else path += "/" + sb.toString(); } currentContext.save(seqPrefix + "specpath", path); } } xc = variableContext.getParameter(seqPrefix + "filecount"); if (xc != null) { // Delete all file specs first int i = 0; while (i < ds.getChildCount()) { SpecificationNode sn = ds.getChild(i); if (sn.getType().equals("include") || sn.getType().equals("exclude")) ds.removeChild(i); else i++; } int fileCount = Integer.parseInt(xc); i = 0; while (i < fileCount) { String fileSpecDescription = "_" + Integer.toString(i); String fileOpName = seqPrefix + "fileop" + fileSpecDescription; xc = variableContext.getParameter(fileOpName); if (xc != null && xc.equals("Delete")) { // Next row i++; continue; } // Get the stuff we need String filespecType = variableContext .getParameter(seqPrefix + "specfiletype" + fileSpecDescription); String filespec = variableContext.getParameter(seqPrefix + "specfile" + fileSpecDescription); SpecificationNode node = new SpecificationNode(filespecType); node.setAttribute("filespec", filespec); ds.addChild(ds.getChildCount(), node); i++; } String op = variableContext.getParameter(seqPrefix + "fileop"); if (op != null && op.equals("Add")) { String filespec = variableContext.getParameter(seqPrefix + "specfile"); String filespectype = variableContext.getParameter(seqPrefix + "specfiletype"); SpecificationNode node = new SpecificationNode(filespectype); node.setAttribute("filespec", filespec); ds.addChild(ds.getChildCount(), node); } } xc = variableContext.getParameter(seqPrefix + "specsecurity"); if (xc != null) { // Delete all security entries first int i = 0; while (i < ds.getChildCount()) { SpecificationNode sn = ds.getChild(i); if (sn.getType().equals("security")) ds.removeChild(i); else i++; } SpecificationNode node = new SpecificationNode("security"); node.setAttribute("value", xc); ds.addChild(ds.getChildCount(), node); } xc = variableContext.getParameter(seqPrefix + "tokencount"); if (xc != null) { // Delete all file specs first int i = 0; while (i < ds.getChildCount()) { SpecificationNode sn = ds.getChild(i); if (sn.getType().equals("access")) ds.removeChild(i); else i++; } int accessCount = Integer.parseInt(xc); i = 0; while (i < accessCount) { String accessDescription = "_" + Integer.toString(i); String accessOpName = seqPrefix + "accessop" + accessDescription; xc = variableContext.getParameter(accessOpName); if (xc != null && xc.equals("Delete")) { // Next row i++; continue; } // Get the stuff we need String accessSpec = variableContext.getParameter(seqPrefix + "spectoken" + accessDescription); SpecificationNode node = new SpecificationNode("access"); node.setAttribute("token", accessSpec); ds.addChild(ds.getChildCount(), node); i++; } String op = variableContext.getParameter(seqPrefix + "accessop"); if (op != null && op.equals("Add")) { String accessspec = variableContext.getParameter(seqPrefix + "spectoken"); SpecificationNode node = new SpecificationNode("access"); node.setAttribute("token", accessspec); ds.addChild(ds.getChildCount(), node); } } xc = variableContext.getParameter(seqPrefix + "specallmetadata"); if (xc != null) { // Look for the 'all metadata' checkbox int i = 0; while (i < ds.getChildCount()) { SpecificationNode sn = ds.getChild(i); if (sn.getType().equals("allmetadata")) ds.removeChild(i); else i++; } if (xc.equals("true")) { SpecificationNode newNode = new SpecificationNode("allmetadata"); newNode.setAttribute("all", xc); ds.addChild(ds.getChildCount(), newNode); } } xc = variableContext.getParameter(seqPrefix + "metadatacount"); if (xc != null) { // Delete all metadata specs first int i = 0; while (i < ds.getChildCount()) { SpecificationNode sn = ds.getChild(i); if (sn.getType().equals("metadata")) ds.removeChild(i); else i++; } // Find out how many children were sent int metadataCount = Integer.parseInt(xc); // Gather up these i = 0; while (i < metadataCount) { String pathDescription = "_" + Integer.toString(i); String pathOpName = seqPrefix + "metadataop" + pathDescription; xc = variableContext.getParameter(pathOpName); if (xc != null && xc.equals("Delete")) { // Skip to the next i++; continue; } // Metadata inserts won't happen until the very end String category = variableContext.getParameter(seqPrefix + "speccategory" + pathDescription); String attributeName = variableContext.getParameter(seqPrefix + "specattribute" + pathDescription); String isAll = variableContext.getParameter(seqPrefix + "specattributeall" + pathDescription); SpecificationNode node = new SpecificationNode("metadata"); node.setAttribute("category", category); if (isAll != null && isAll.equals("true")) node.setAttribute("all", "true"); else node.setAttribute("attribute", attributeName); ds.addChild(ds.getChildCount(), node); i++; } // See if there's a global add operation String op = variableContext.getParameter(seqPrefix + "metadataop"); if (op != null && op.equals("Add")) { String category = variableContext.getParameter(seqPrefix + "speccategory"); String isAll = variableContext.getParameter(seqPrefix + "attributeall"); if (isAll != null && isAll.equals("true")) { SpecificationNode node = new SpecificationNode("metadata"); node.setAttribute("category", category); node.setAttribute("all", "true"); ds.addChild(ds.getChildCount(), node); } else { String[] attributes = variableContext.getParameterValues(seqPrefix + "attributeselect"); if (attributes != null && attributes.length > 0) { int k = 0; while (k < attributes.length) { String attribute = attributes[k++]; SpecificationNode node = new SpecificationNode("metadata"); node.setAttribute("category", category); node.setAttribute("attribute", attribute); ds.addChild(ds.getChildCount(), node); } } } } else if (op != null && op.equals("Up")) { // Strip off end String category = variableContext.getParameter(seqPrefix + "speccategory"); int lastSlash = -1; int firstColon = -1; int k = 0; while (k < category.length()) { char x = category.charAt(k++); if (x == '/') { lastSlash = k - 1; continue; } if (x == ':') { firstColon = k; continue; } if (x == '\\') k++; } if (lastSlash == -1) { if (firstColon == -1 || firstColon == category.length()) category = ""; else category = category.substring(0, firstColon); } else category = category.substring(0, lastSlash); currentContext.save(seqPrefix + "speccategory", category); } else if (op != null && op.equals("AddToPath")) { String category = variableContext.getParameter(seqPrefix + "speccategory"); String addon = variableContext.getParameter(seqPrefix + "metadataaddon"); if (addon != null && addon.length() > 0) { StringBuilder sb = new StringBuilder(); int k = 0; while (k < addon.length()) { char x = addon.charAt(k++); if (x == '/' || x == '\\' || x == ':') sb.append('\\'); sb.append(x); } if (category.length() == 0 || category.endsWith(":")) category += sb.toString(); else category += "/" + sb.toString(); } currentContext.save(seqPrefix + "speccategory", category); } else if (op != null && op.equals("SetWorkspace")) { String addon = variableContext.getParameter(seqPrefix + "metadataaddon"); if (addon != null && addon.length() > 0) { StringBuilder sb = new StringBuilder(); int k = 0; while (k < addon.length()) { char x = addon.charAt(k++); if (x == '/' || x == '\\' || x == ':') sb.append('\\'); sb.append(x); } String category = sb.toString() + ":"; currentContext.save(seqPrefix + "speccategory", category); } } else if (op != null && op.equals("AddCategory")) { String category = variableContext.getParameter(seqPrefix + "speccategory"); String addon = variableContext.getParameter(seqPrefix + "categoryaddon"); if (addon != null && addon.length() > 0) { StringBuilder sb = new StringBuilder(); int k = 0; while (k < addon.length()) { char x = addon.charAt(k++); if (x == '/' || x == '\\' || x == ':') sb.append('\\'); sb.append(x); } if (category.length() == 0 || category.endsWith(":")) category += sb.toString(); else category += "/" + sb.toString(); } currentContext.save(seqPrefix + "speccategory", category); } } xc = variableContext.getParameter(seqPrefix + "specpathnameattribute"); if (xc != null) { String separator = variableContext.getParameter(seqPrefix + "specpathnameseparator"); if (separator == null) separator = "/"; // Delete old one int i = 0; while (i < ds.getChildCount()) { SpecificationNode sn = ds.getChild(i); if (sn.getType().equals("pathnameattribute")) ds.removeChild(i); else i++; } if (xc.length() > 0) { SpecificationNode node = new SpecificationNode("pathnameattribute"); node.setAttribute("value", xc); node.setAttribute("separator", separator); ds.addChild(ds.getChildCount(), node); } } xc = variableContext.getParameter(seqPrefix + "specmappingcount"); if (xc != null) { // Delete old spec int i = 0; while (i < ds.getChildCount()) { SpecificationNode sn = ds.getChild(i); if (sn.getType().equals("pathmap")) ds.removeChild(i); else i++; } // Now, go through the data and assemble a new list. int mappingCount = Integer.parseInt(xc); // Gather up these i = 0; while (i < mappingCount) { String pathDescription = "_" + Integer.toString(i); String pathOpName = seqPrefix + "specmappingop" + pathDescription; xc = variableContext.getParameter(pathOpName); if (xc != null && xc.equals("Delete")) { // Skip to the next i++; continue; } // Inserts won't happen until the very end String match = variableContext.getParameter(seqPrefix + "specmatch" + pathDescription); String replace = variableContext.getParameter(seqPrefix + "specreplace" + pathDescription); SpecificationNode node = new SpecificationNode("pathmap"); node.setAttribute("match", match); node.setAttribute("replace", replace); ds.addChild(ds.getChildCount(), node); i++; } // Check for add xc = variableContext.getParameter(seqPrefix + "specmappingop"); if (xc != null && xc.equals("Add")) { String match = variableContext.getParameter(seqPrefix + "specmatch"); String replace = variableContext.getParameter(seqPrefix + "specreplace"); SpecificationNode node = new SpecificationNode("pathmap"); node.setAttribute("match", match); node.setAttribute("replace", replace); ds.addChild(ds.getChildCount(), node); } } return null; } /** View specification. * This method is called in the body section of a job's view page. Its purpose is to present the document * specification information to the user. The coder can presume that the HTML that is output from * this configuration will be within appropriate <html> and <body> tags. * The connector will be connected before this method can be called. *@param out is the output to which any HTML should be sent. *@param locale is the locale the output is preferred to be in. *@param ds is the current document specification for this job. *@param connectionSequenceNumber is the unique number of this connection within the job. */ @Override public void viewSpecification(IHTTPOutput out, Locale locale, Specification ds, int connectionSequenceNumber) throws ManifoldCFException, IOException { out.print("<table class=\"displaytable\">\n" + " <tr>\n"); int i = 0; boolean userWorkspaces = false; while (i < ds.getChildCount()) { SpecificationNode sn = ds.getChild(i++); if (sn.getType().equals("userworkspace")) { String value = sn.getAttributeValue("value"); if (value != null && value.equals("true")) userWorkspaces = true; } } out.print(" <td class=\"description\"/>\n" + " <nobr>" + Messages.getBodyString(locale, "LivelinkConnector.CrawlUserWorkspaces") + "</nobr>\n" + " </td>\n" + " <td class=\"value\"/>\n" + " " + (userWorkspaces ? Messages.getBodyString(locale, "LivelinkConnector.Yes") : Messages.getBodyString(locale, "LivelinkConnector.No")) + "\n" + " </td>\n" + " </tr>"); out.print(" <tr><td class=\"separator\" colspan=\"2\"><hr/></td></tr>\n"); out.print(" <tr>"); i = 0; boolean seenAny = false; while (i < ds.getChildCount()) { SpecificationNode sn = ds.getChild(i++); if (sn.getType().equals("startpoint")) { if (seenAny == false) { out.print(" <td class=\"description\">" + Messages.getBodyString(locale, "LivelinkConnector.Roots") + "</td>\n" + " <td class=\"value\">\n"); seenAny = true; } out.print(" " + org.apache.manifoldcf.ui.util.Encoder.bodyEscape(sn.getAttributeValue("path")) + "<br/>\n"); } } if (seenAny) { out.print(" </td>\n" + " </tr>\n"); } else { out.print(" <tr><td class=\"message\" colspan=\"2\">" + Messages.getBodyString(locale, "LivelinkConnector.NoStartPointsSpecified") + "</td></tr>\n"); } out.print(" <tr><td class=\"separator\" colspan=\"2\"><hr/></td></tr>\n"); seenAny = false; // Go through looking for include or exclude file specs i = 0; while (i < ds.getChildCount()) { SpecificationNode sn = ds.getChild(i++); if (sn.getType().equals("include") || sn.getType().equals("exclude")) { if (seenAny == false) { out.print(" <tr><td class=\"description\">" + Messages.getBodyString(locale, "LivelinkConnector.FileSpecs") + "</td>\n" + " <td class=\"value\">\n"); seenAny = true; } String filespec = sn.getAttributeValue("filespec"); out.print(" " + (sn.getType().equals("include") ? "Include file:" : "") + "\n" + " " + (sn.getType().equals("exclude") ? "Exclude file:" : "") + "\n" + " " + org.apache.manifoldcf.ui.util.Encoder.bodyEscape(filespec) + "<br/>\n"); } } if (seenAny) { out.print(" </td>\n" + " </tr>\n"); } else { out.print(" <tr><td class=\"message\" colspan=\"2\">" + Messages.getBodyString(locale, "LivelinkConnector.NoFileSpecsSpecified") + "</td></tr>\n"); } out.print(" <tr><td class=\"separator\" colspan=\"2\"><hr/></td></tr>\n"); // Find whether security is on or off i = 0; boolean securityOn = true; while (i < ds.getChildCount()) { SpecificationNode sn = ds.getChild(i++); if (sn.getType().equals("security")) { String securityValue = sn.getAttributeValue("value"); if (securityValue.equals("off")) securityOn = false; else if (securityValue.equals("on")) securityOn = true; } } out.print(" <tr>\n" + " <td class=\"description\">" + Messages.getBodyString(locale, "LivelinkConnector.SecurityColon") + "</td>\n" + " <td class=\"value\">" + (securityOn ? Messages.getBodyString(locale, "LivelinkConnector.Enabled2") : Messages.getBodyString(locale, "LivelinkConnector.Disabled")) + "</td>\n" + " </tr>\n" + "\n" + " <tr><td class=\"separator\" colspan=\"2\"><hr/></td></tr>\n"); // Go through looking for access tokens seenAny = false; i = 0; while (i < ds.getChildCount()) { SpecificationNode sn = ds.getChild(i++); if (sn.getType().equals("access")) { if (seenAny == false) { out.print(" <tr><td class=\"description\">" + Messages.getBodyString(locale, "LivelinkConnector.AccessTokens") + "</td>\n" + " <td class=\"value\">\n"); seenAny = true; } String token = sn.getAttributeValue("token"); out.print(" " + org.apache.manifoldcf.ui.util.Encoder.bodyEscape(token) + "<br/>\n"); } } if (seenAny) { out.print(" </td>\n" + " </tr>\n"); } else { out.print(" <tr><td class=\"message\" colspan=\"2\">" + Messages.getBodyString(locale, "LivelinkConnector.NoAccessTokensSpecified") + "</td></tr>\n"); } out.print(" <tr><td class=\"separator\" colspan=\"2\"><hr/></td></tr>\n"); i = 0; String allMetadata = Messages.getBodyString(locale, "LivelinkConnector.OnlySpecifiedMetadataWillBeIngested"); while (i < ds.getChildCount()) { SpecificationNode sn = ds.getChild(i++); if (sn.getType().equals("allmetadata")) { String value = sn.getAttributeValue("all"); if (value != null && value.equals("true")) { allMetadata = Messages.getBodyString(locale, "LivelinkConnector.AllDocumentMetadataWillBeIngested"); } } } out.print(" <tr>\n" + " <td class=\"description\"><nobr>" + Messages.getBodyString(locale, "LivelinkConnector.MetadataSpecification") + "</nobr></td>\n" + " <td class=\"value\"><nobr>" + allMetadata + "</nobr></td>\n" + " </tr>\n" + " <tr><td class=\"separator\" colspan=\"2\"><hr/></td></tr>\n"); // Go through looking for metadata spec seenAny = false; i = 0; while (i < ds.getChildCount()) { SpecificationNode sn = ds.getChild(i++); if (sn.getType().equals("metadata")) { if (seenAny == false) { out.print(" <tr><td class=\"description\"><nobr>" + Messages.getBodyString(locale, "LivelinkConnector.SpecificMetadata") + "</nobr></td>\n" + " <td class=\"value\">\n"); seenAny = true; } String category = sn.getAttributeValue("category"); String attribute = sn.getAttributeValue("attribute"); String isAll = sn.getAttributeValue("all"); out.print(" " + org.apache.manifoldcf.ui.util.Encoder.bodyEscape(category) + ":" + ((isAll != null && isAll.equals("true")) ? "(All metadata attributes)" : org.apache.manifoldcf.ui.util.Encoder.bodyEscape(attribute)) + "<br/>\n"); } } if (seenAny) { out.print(" </td>\n" + " </tr>\n"); } else { out.print(" <tr><td class=\"message\" colspan=\"2\">" + Messages.getBodyString(locale, "LivelinkConnector.NoMetadataSpecified") + "</td></tr>\n"); } out.print(" <tr><td class=\"separator\" colspan=\"2\"><hr/></td></tr>\n"); // Find the path-name metadata attribute name i = 0; String pathNameAttribute = ""; String pathSeparator = "/"; while (i < ds.getChildCount()) { SpecificationNode sn = ds.getChild(i++); if (sn.getType().equals("pathnameattribute")) { pathNameAttribute = sn.getAttributeValue("value"); if (sn.getAttributeValue("separator") != null) pathSeparator = sn.getAttributeValue("separator"); } } if (pathNameAttribute.length() > 0) { out.print(" <tr>\n" + " <td class=\"description\">" + Messages.getBodyString(locale, "LivelinkConnector.PathNameMetadataAttribute") + "</td>\n" + " <td class=\"value\">" + org.apache.manifoldcf.ui.util.Encoder.bodyEscape(pathNameAttribute) + "</td>\n" + " </tr>\n" + " <tr>\n" + " <td class=\"description\">" + Messages.getBodyString(locale, "LivelinkConnector.PathSeparatorString") + "</td>\n" + " <td class=\"value\">" + org.apache.manifoldcf.ui.util.Encoder.bodyEscape(pathSeparator) + "</td>\n" + " </tr>\n"); } else { out.print(" <tr>\n" + " <td class=\"message\" colspan=\"2\">" + Messages.getBodyString(locale, "LivelinkConnector.NoPathNameMetadataAttributeSpecified") + "</td>\n" + " </tr>\n"); } out.print("\n" + " <tr><td class=\"separator\" colspan=\"2\"><hr/></td></tr>\n" + "\n" + " <tr>\n"); // Find the path-value mapping data i = 0; MatchMap matchMap = new MatchMap(); while (i < ds.getChildCount()) { SpecificationNode sn = ds.getChild(i++); if (sn.getType().equals("pathmap")) { String pathMatch = sn.getAttributeValue("match"); String pathReplace = sn.getAttributeValue("replace"); matchMap.appendMatchPair(pathMatch, pathReplace); } } if (matchMap.getMatchCount() > 0) { out.print(" <td class=\"description\">" + Messages.getBodyString(locale, "LivelinkConnector.PathValueMapping") + "</td>\n" + " <td class=\"value\">\n" + " <table class=\"displaytable\">\n"); i = 0; while (i < matchMap.getMatchCount()) { String matchString = matchMap.getMatchString(i); String replaceString = matchMap.getReplaceString(i); out.print(" <tr>\n" + " <td class=\"value\">" + org.apache.manifoldcf.ui.util.Encoder.bodyEscape(matchString) + "</td>\n" + " <td class=\"value\">--></td>\n" + " <td class=\"value\">" + org.apache.manifoldcf.ui.util.Encoder.bodyEscape(replaceString) + "</td>\n" + " </tr>\n"); i++; } out.print(" </table>\n" + " </td>\n"); } else { out.print(" <td class=\"message\" colspan=\"2\">" + Messages.getBodyString(locale, "LivelinkConnector.NoMappingsSpecified") + "</td>\n"); } out.print(" </tr>\n" + "</table>\n"); } // The following public methods are NOT part of the interface. They are here so that the UI can present information // that will allow users to select what they need. protected final static String CATEGORY_NAME = "CATEGORY"; protected final static String ENTWKSPACE_NAME = "ENTERPRISE"; /** Get the allowed workspace names. *@return a list of workspace names. */ public String[] getWorkspaceNames() throws ManifoldCFException, ServiceInterruption { return new String[] { CATEGORY_NAME, ENTWKSPACE_NAME }; } /** Given a path string, get a list of folders and projects under that node. *@param pathString is the current path (folder names and project names, separated by dots (.)). *@return a list of folder and project names, in sorted order, or null if the path was invalid. */ public String[] getChildFolderNames(String pathString) throws ManifoldCFException, ServiceInterruption { getSession(); return getChildFolders(new LivelinkContext(), pathString); } /** Given a path string, get a list of categories under that node. *@param pathString is the current path (folder names and project names, separated by dots (.)). *@return a list of category names, in sorted order, or null if the path was invalid. */ public String[] getChildCategoryNames(String pathString) throws ManifoldCFException, ServiceInterruption { getSession(); return getChildCategories(new LivelinkContext(), pathString); } /** Given a category path, get a list of legal attribute names. *@param pathString is the current path of a category (with path components separated by dots). *@return a list of attribute names, in sorted order, or null of the path was invalid. */ public String[] getCategoryAttributes(String pathString) throws ManifoldCFException, ServiceInterruption { getSession(); return getCategoryAttributes(new LivelinkContext(), pathString); } protected String[] getCategoryAttributes(LivelinkContext llc, String pathString) throws ManifoldCFException, ServiceInterruption { // Start at root RootValue rv = new RootValue(llc, pathString); // Get the object id of the category the path describes int catObjectID = getCategoryId(rv); if (catObjectID == -1) return null; String[] rval = getCategoryAttributes(catObjectID); if (rval == null) return new String[0]; return rval; } // Protected methods and classes /** Create the login URI. This must be a relative URI. */ protected String createLivelinkLoginURI() throws ManifoldCFException { StringBuilder llURI = new StringBuilder(); llURI.append(ingestCgiPath); llURI.append("?func=ll.login&CurrentClientTime=D%2F2005%2F3%2F9%3A13%3A16%3A30&NextURL="); llURI.append(org.apache.manifoldcf.core.util.URLEncoder.encode(ingestCgiPath)); llURI.append("%3FRedirect%3D1&Username="); llURI.append(org.apache.manifoldcf.core.util.URLEncoder.encode(llServer.getLLUser())); llURI.append("&Password="); llURI.append(org.apache.manifoldcf.core.util.URLEncoder.encode(llServer.getLLPwd())); return llURI.toString(); } /** * Connects to the specified Livelink document using HTTP protocol * @param documentIdentifier is the document identifier (as far as the crawler knows). * @param activities is the process activity structure, so we can ingest */ protected void ingestFromLiveLink(LivelinkContext llc, String documentIdentifier, String version, String[] actualAcls, String[] denyAcls, String[] categoryPaths, IProcessActivity activities, MetadataDescription desc, SystemMetadataDescription sDesc) throws ManifoldCFException, ServiceInterruption { String contextMsg = "for '" + documentIdentifier + "'"; // Fetch logging long startTime = System.currentTimeMillis(); String resultCode = null; String resultDescription = null; Long readSize = null; int objID; int vol; int colonPos = documentIdentifier.indexOf(":", 1); if (colonPos == -1) { objID = new Integer(documentIdentifier.substring(1)).intValue(); vol = LLENTWK_VOL; } else { objID = new Integer(documentIdentifier.substring(colonPos + 1)).intValue(); vol = new Integer(documentIdentifier.substring(1, colonPos)).intValue(); } // Try/finally for fetch logging try { String viewHttpAddress = convertToViewURI(documentIdentifier); if (viewHttpAddress == null) { if (Logging.connectors.isDebugEnabled()) Logging.connectors.debug("Livelink: No view URI " + contextMsg + " - not ingesting"); resultCode = "NOVIEWURI"; resultDescription = "Document had no view URI"; activities.noDocument(documentIdentifier, version); return; } // Check URL first if (!activities.checkURLIndexable(viewHttpAddress)) { // Document not ingestable due to URL resultCode = activities.EXCLUDED_URL; resultDescription = "URL (" + viewHttpAddress + ") was rejected by output connector"; if (Logging.connectors.isDebugEnabled()) Logging.connectors.debug("Livelink: Excluding document " + documentIdentifier + " because its URL (" + viewHttpAddress + ") was rejected by output connector"); activities.noDocument(documentIdentifier, version); return; } // Add general metadata ObjectInformation objInfo = llc.getObjectInformation(vol, objID); VersionInformation versInfo = llc.getVersionInformation(vol, objID, 0); if (!objInfo.exists()) { resultCode = "OBJECTNOTFOUND"; resultDescription = "Object was not found in Livelink"; Logging.connectors.debug("Livelink: No object " + contextMsg + ": not ingesting"); activities.noDocument(documentIdentifier, version); return; } if (!versInfo.exists()) { resultCode = "VERSIONNOTFOUND"; resultDescription = "Version was not found in Livelink"; Logging.connectors.debug("Livelink: No version data " + contextMsg + ": not ingesting"); activities.noDocument(documentIdentifier, version); return; } String mimeType = versInfo.getMimeType(); if (!activities.checkMimeTypeIndexable(mimeType)) { // Document not indexable because of its mime type resultCode = activities.EXCLUDED_MIMETYPE; resultDescription = "Mime type (" + mimeType + ") was rejected by output connector"; if (Logging.connectors.isDebugEnabled()) Logging.connectors.debug("Livelink: Excluding document " + documentIdentifier + " because its mime type (" + mimeType + ") was rejected by output connector"); activities.noDocument(documentIdentifier, version); return; } Long dataSize = versInfo.getDataSize(); if (dataSize == null) { // Document had no length resultCode = "DOCUMENTNOLENGTH"; resultDescription = "Document had no length in Livelink"; if (Logging.connectors.isDebugEnabled()) Logging.connectors.debug( "Livelink: Excluding document " + documentIdentifier + " because it had no length"); activities.noDocument(documentIdentifier, version); return; } if (!activities.checkLengthIndexable(dataSize.longValue())) { // Document not indexable because of its length resultCode = activities.EXCLUDED_LENGTH; resultDescription = "Document length (" + dataSize + ") was rejected by output connector"; if (Logging.connectors.isDebugEnabled()) Logging.connectors.debug("Livelink: Excluding document " + documentIdentifier + " because its length (" + dataSize + ") was rejected by output connector"); activities.noDocument(documentIdentifier, version); return; } Date modifyDate = versInfo.getModifyDate(); if (!activities.checkDateIndexable(modifyDate)) { // Document not indexable because of its date resultCode = activities.EXCLUDED_DATE; resultDescription = "Document date (" + modifyDate + ") was rejected by output connector"; if (Logging.connectors.isDebugEnabled()) Logging.connectors.debug("Livelink: Excluding document " + documentIdentifier + " because its date (" + modifyDate + ") was rejected by output connector"); activities.noDocument(documentIdentifier, version); return; } String fileName = versInfo.getFileName(); Date creationDate = objInfo.getCreationDate(); Integer parentID = objInfo.getParentId(); RepositoryDocument rd = new RepositoryDocument(); // Add general data we need for the output connector if (mimeType != null) rd.setMimeType(mimeType); if (fileName != null) rd.setFileName(fileName); if (creationDate != null) rd.setCreatedDate(creationDate); if (modifyDate != null) rd.setModifiedDate(modifyDate); rd.addField(GENERAL_NAME_FIELD, objInfo.getName()); rd.addField(GENERAL_DESCRIPTION_FIELD, objInfo.getComments()); if (creationDate != null) rd.addField(GENERAL_CREATIONDATE_FIELD, DateParser.formatISO8601Date(creationDate)); if (modifyDate != null) rd.addField(GENERAL_MODIFYDATE_FIELD, DateParser.formatISO8601Date(modifyDate)); if (parentID != null) rd.addField(GENERAL_PARENTID, parentID.toString()); UserInformation owner = llc.getUserInformation(objInfo.getOwnerId().intValue()); UserInformation creator = llc.getUserInformation(objInfo.getCreatorId().intValue()); UserInformation modifier = llc.getUserInformation(versInfo.getOwnerId().intValue()); if (owner != null) rd.addField(GENERAL_OWNER, owner.getName()); if (creator != null) rd.addField(GENERAL_CREATOR, creator.getName()); if (modifier != null) rd.addField(GENERAL_MODIFIER, modifier.getName()); // Iterate over the metadata items. These are organized by category // for speed of lookup. Iterator<MetadataItem> catIter = desc.getItems(categoryPaths); while (catIter.hasNext()) { MetadataItem item = catIter.next(); MetadataPathItem pathItem = item.getPathItem(); if (pathItem != null) { int catID = pathItem.getCatID(); // grab the associated catversion LLValue catVersion = getCatVersion(objID, catID); if (catVersion != null) { // Go through attributes now Iterator<String> attrIter = item.getAttributeNames(); while (attrIter.hasNext()) { String attrName = attrIter.next(); // Create a unique metadata name String metadataName = pathItem.getCatName() + ":" + attrName; // Fetch the metadata and stuff it into the RepositoryData structure String[] metadataValue = getAttributeValue(catVersion, attrName); if (metadataValue != null) rd.addField(metadataName, metadataValue); else Logging.connectors.warn("Livelink: Metadata attribute '" + metadataName + "' does not seem to exist; please correct the job"); } } } } if (actualAcls != null && denyAcls != null) rd.setSecurity(RepositoryDocument.SECURITY_TYPE_DOCUMENT, actualAcls, denyAcls); // Add the path metadata item into the mix, if enabled String pathAttributeName = sDesc.getPathAttributeName(); if (pathAttributeName != null && pathAttributeName.length() > 0) { String pathString = sDesc.getPathAttributeValue(documentIdentifier); if (pathString != null) { if (Logging.connectors.isDebugEnabled()) Logging.connectors.debug("Livelink: Path attribute name is '" + pathAttributeName + "'" + contextMsg + ", value is '" + pathString + "'"); rd.addField(pathAttributeName, pathString); } } if (ingestProtocol != null) { // Use HTTP to fetch document! String ingestHttpAddress = convertToIngestURI(documentIdentifier); if (ingestHttpAddress == null) { if (Logging.connectors.isDebugEnabled()) Logging.connectors.debug("Livelink: No fetch URI " + contextMsg + " - not ingesting"); resultCode = "NOURI"; resultDescription = "Document had no fetch URI"; activities.noDocument(documentIdentifier, version); return; } // Set up connection HttpClient client = getInitializedClient(contextMsg); long currentTime; if (Logging.connectors.isInfoEnabled()) Logging.connectors.info("Livelink: " + ingestHttpAddress); HttpGet method = new HttpGet(getHost().toURI() + ingestHttpAddress); method.setHeader(new BasicHeader("Accept", "*/*")); boolean wasInterrupted = false; ExecuteMethodThread methodThread = new ExecuteMethodThread(client, method); methodThread.start(); try { int statusCode = methodThread.getResponseCode(); switch (statusCode) { case 500: case 502: Logging.connectors.warn("Livelink: Service interruption during fetch " + contextMsg + " with Livelink HTTP Server, retrying..."); resultCode = "FETCHFAILED"; resultDescription = "HTTP error code " + statusCode + " fetching document"; throw new ServiceInterruption("Service interruption during fetch", new ManifoldCFException(Integer.toString(statusCode) + " error while fetching"), System.currentTimeMillis() + 60000L, System.currentTimeMillis() + 600000L, -1, true); case HttpStatus.SC_UNAUTHORIZED: Logging.connectors.warn("Livelink: Document fetch unauthorized for " + ingestHttpAddress + " (" + contextMsg + ")"); // Since we logged in, we should fail here if the ingestion user doesn't have access to the // the document, but if we do, don't fail hard. resultCode = "UNAUTHORIZED"; resultDescription = "Document fetch was unauthorized by IIS"; activities.noDocument(documentIdentifier, version); return; case HttpStatus.SC_OK: if (Logging.connectors.isDebugEnabled()) Logging.connectors .debug("Livelink: Created http document connection to Livelink " + contextMsg); // A non-existent content length will cause a value of -1 to be returned. This seems to indicate that the session login did not work right. if (methodThread.getResponseContentLength() < 0) { resultCode = "SESSIONLOGINFAILED"; resultDescription = "Response content length was -1, which usually means session login did not succeed"; activities.noDocument(documentIdentifier, version); return; } try { InputStream is = methodThread.getSafeInputStream(); try { rd.setBinary(is, dataSize); activities.ingestDocumentWithException(documentIdentifier, version, viewHttpAddress, rd); resultCode = "OK"; readSize = dataSize; if (Logging.connectors.isDebugEnabled()) Logging.connectors.debug("Livelink: Ingesting done " + contextMsg); } finally { // Close stream via thread, since otherwise this can hang is.close(); } } catch (InterruptedException e) { wasInterrupted = true; throw new ManifoldCFException("Interrupted: " + e.getMessage(), e, ManifoldCFException.INTERRUPTED); } catch (HttpException e) { resultCode = e.getClass().getSimpleName().toUpperCase(Locale.ROOT); resultDescription = e.getMessage(); handleHttpException(contextMsg, e); } catch (IOException e) { resultCode = e.getClass().getSimpleName().toUpperCase(Locale.ROOT); resultDescription = e.getMessage(); handleIOException(contextMsg, e); } break; case HttpStatus.SC_BAD_REQUEST: case HttpStatus.SC_USE_PROXY: case HttpStatus.SC_GONE: resultCode = "HTTPERROR"; resultDescription = "Http request returned status " + Integer.toString(statusCode); throw new ManifoldCFException( "Unrecoverable request failure; error = " + Integer.toString(statusCode)); default: resultCode = "UNKNOWNHTTPCODE"; resultDescription = "Http request returned status " + Integer.toString(statusCode); Logging.connectors.warn("Livelink: Attempt to retrieve document from '" + ingestHttpAddress + "' received a response of " + Integer.toString(statusCode) + "; retrying in one minute"); currentTime = System.currentTimeMillis(); throw new ServiceInterruption("Fetch failed; retrying in 1 minute", new ManifoldCFException( "Fetch failed with unknown code " + Integer.toString(statusCode)), currentTime + 60000L, currentTime + 600000L, -1, true); } } catch (InterruptedException e) { // Drop the connection on the floor methodThread.interrupt(); methodThread = null; throw new ManifoldCFException("Interrupted: " + e.getMessage(), e, ManifoldCFException.INTERRUPTED); } catch (HttpException e) { resultCode = e.getClass().getSimpleName().toUpperCase(Locale.ROOT); resultDescription = e.getMessage(); handleHttpException(contextMsg, e); } catch (IOException e) { resultCode = e.getClass().getSimpleName().toUpperCase(Locale.ROOT); resultDescription = e.getMessage(); handleIOException(contextMsg, e); } finally { if (methodThread != null) { methodThread.abort(); try { if (!wasInterrupted) methodThread.finishUp(); } catch (InterruptedException e) { throw new ManifoldCFException(e.getMessage(), e, ManifoldCFException.INTERRUPTED); } } } } else { // Use FetchVersion instead long currentTime; // Fire up the document reading thread DocumentReadingThread t = new DocumentReadingThread(vol, objID, 0); boolean wasInterrupted = false; t.start(); try { try { InputStream is = t.getSafeInputStream(); try { // Can only index while background thread is running! rd.setBinary(is, dataSize); activities.ingestDocumentWithException(documentIdentifier, version, viewHttpAddress, rd); resultCode = "OK"; readSize = dataSize; } finally { is.close(); } } catch (java.net.SocketTimeoutException e) { throw e; } catch (InterruptedIOException e) { wasInterrupted = true; throw e; } finally { if (!wasInterrupted) t.finishUp(); } // No errors. Record the fact that we made it. } catch (InterruptedException e) { t.interrupt(); throw new ManifoldCFException("Interrupted: " + e.getMessage(), e, ManifoldCFException.INTERRUPTED); } catch (IOException e) { resultCode = e.getClass().getSimpleName().toUpperCase(Locale.ROOT); resultDescription = e.getMessage(); handleIOException(contextMsg, e); } catch (RuntimeException e) { resultCode = e.getClass().getSimpleName().toUpperCase(Locale.ROOT); resultDescription = e.getMessage(); handleLivelinkRuntimeException(e, 0, true); } } } catch (ManifoldCFException e) { if (e.getErrorCode() == ManifoldCFException.INTERRUPTED) resultCode = null; throw e; } finally { if (resultCode != null) activities.recordActivity(new Long(startTime), ACTIVITY_FETCH, readSize, vol + ":" + objID, resultCode, resultDescription, null); } } protected static void handleHttpException(String contextMsg, HttpException e) throws ManifoldCFException, ServiceInterruption { long currentTime = System.currentTimeMillis(); // Treat unknown error ingesting data as a transient condition Logging.connectors.warn("Livelink: HTTP exception ingesting " + contextMsg + ": " + e.getMessage(), e); throw new ServiceInterruption("HTTP exception ingesting " + contextMsg + ": " + e.getMessage(), e, currentTime + 300000L, currentTime + 6 * 3600000L, -1, false); } protected static void handleIOException(String contextMsg, IOException e) throws ManifoldCFException, ServiceInterruption { long currentTime = System.currentTimeMillis(); if (e instanceof java.net.SocketTimeoutException) { Logging.connectors.warn("Livelink: Livelink socket timed out ingesting from the Livelink HTTP Server " + contextMsg + ": " + e.getMessage(), e); throw new ServiceInterruption("Socket timed out: " + e.getMessage(), e, currentTime + 300000L, currentTime + 6 * 3600000L, -1, false); } if (e instanceof java.net.SocketException) { Logging.connectors.warn("Livelink: Livelink socket error ingesting from the Livelink HTTP Server " + contextMsg + ": " + e.getMessage(), e); throw new ServiceInterruption("Socket error: " + e.getMessage(), e, currentTime + 300000L, currentTime + 6 * 3600000L, -1, false); } if (e instanceof javax.net.ssl.SSLHandshakeException) { Logging.connectors .warn("Livelink: SSL handshake failed authenticating " + contextMsg + ": " + e.getMessage(), e); throw new ServiceInterruption("SSL handshake error: " + e.getMessage(), e, currentTime + 60000L, currentTime + 300000L, -1, true); } if (e instanceof ConnectTimeoutException) { Logging.connectors.warn("Livelink: Livelink socket timed out connecting to the Livelink HTTP Server " + contextMsg + ": " + e.getMessage(), e); throw new ServiceInterruption("Connect timed out: " + e.getMessage(), e, currentTime + 300000L, currentTime + 6 * 3600000L, -1, false); } if (e instanceof InterruptedIOException) throw new ManifoldCFException("Interrupted: " + e.getMessage(), e, ManifoldCFException.INTERRUPTED); // Treat unknown error ingesting data as a transient condition Logging.connectors.warn("Livelink: IO exception ingesting " + contextMsg + ": " + e.getMessage(), e); throw new ServiceInterruption("IO exception ingesting " + contextMsg + ": " + e.getMessage(), e, currentTime + 300000L, currentTime + 6 * 3600000L, -1, false); } /** Initialize a livelink client connection */ protected HttpClient getInitializedClient(String contextMsg) throws ServiceInterruption, ManifoldCFException { long currentTime; if (Logging.connectors.isDebugEnabled()) Logging.connectors.debug("Livelink: Session authenticating via http " + contextMsg + "..."); HttpGet authget = new HttpGet(getHost().toURI() + createLivelinkLoginURI()); authget.setHeader(new BasicHeader("Accept", "*/*")); try { if (Logging.connectors.isDebugEnabled()) Logging.connectors .debug("Livelink: Created new HttpGet " + contextMsg + "; executing authentication method"); int statusCode = executeMethodViaThread(httpClient, authget); if (statusCode == 502 || statusCode == 500) { Logging.connectors.warn("Livelink: Service interruption during authentication " + contextMsg + " with Livelink HTTP Server, retrying..."); currentTime = System.currentTimeMillis(); throw new ServiceInterruption("502 error during authentication", new ManifoldCFException("502 error while authenticating"), currentTime + 60000L, currentTime + 600000L, -1, true); } if (statusCode != HttpStatus.SC_OK) { Logging.connectors.error("Livelink: Failed to authenticate " + contextMsg + " against Livelink HTTP Server; Status code: " + statusCode); // Ok, so we didn't get in - simply do not ingest if (statusCode == HttpStatus.SC_UNAUTHORIZED) throw new ManifoldCFException( "Session authorization failed with a 401 code; are credentials correct?"); else throw new ManifoldCFException( "Session authorization failed with code " + Integer.toString(statusCode)); } } catch (InterruptedException e) { throw new ManifoldCFException("Interrupted: " + e.getMessage(), e, ManifoldCFException.INTERRUPTED); } catch (java.net.SocketTimeoutException e) { currentTime = System.currentTimeMillis(); Logging.connectors.warn("Livelink: Socket timed out authenticating to the Livelink HTTP Server " + contextMsg + ": " + e.getMessage(), e); throw new ServiceInterruption("Socket timed out: " + e.getMessage(), e, currentTime + 300000L, currentTime + 6 * 3600000L, -1, true); } catch (java.net.SocketException e) { currentTime = System.currentTimeMillis(); Logging.connectors.warn("Livelink: Socket error authenticating to the Livelink HTTP Server " + contextMsg + ": " + e.getMessage(), e); throw new ServiceInterruption("Socket error: " + e.getMessage(), e, currentTime + 300000L, currentTime + 6 * 3600000L, -1, true); } catch (javax.net.ssl.SSLHandshakeException e) { currentTime = System.currentTimeMillis(); Logging.connectors .warn("Livelink: SSL handshake failed authenticating " + contextMsg + ": " + e.getMessage(), e); throw new ServiceInterruption("SSL handshake error: " + e.getMessage(), e, currentTime + 60000L, currentTime + 300000L, -1, true); } catch (ConnectTimeoutException e) { currentTime = System.currentTimeMillis(); Logging.connectors.warn("Livelink: Connect timed out authenticating to the Livelink HTTP Server " + contextMsg + ": " + e.getMessage(), e); throw new ServiceInterruption("Connect timed out: " + e.getMessage(), e, currentTime + 300000L, currentTime + 6 * 3600000L, -1, true); } catch (InterruptedIOException e) { throw new ManifoldCFException("Interrupted: " + e.getMessage(), e, ManifoldCFException.INTERRUPTED); } catch (HttpException e) { Logging.connectors.error("Livelink: HTTP exception when authenticating to the Livelink HTTP Server " + contextMsg + ": " + e.getMessage(), e); throw new ManifoldCFException("Unable to communicate with the Livelink HTTP Server: " + e.getMessage(), e); } catch (IOException e) { Logging.connectors.error("Livelink: IO exception when authenticating to the Livelink HTTP Server " + contextMsg + ": " + e.getMessage(), e); throw new ManifoldCFException("Unable to communicate with the Livelink HTTP Server: " + e.getMessage(), e); } return httpClient; } /** Pack category and attribute */ protected static String packCategoryAttribute(String category, String attribute) { StringBuilder sb = new StringBuilder(); pack(sb, category, ':'); pack(sb, attribute, ':'); return sb.toString(); } /** Unpack category and attribute */ protected static void unpackCategoryAttribute(StringBuilder category, StringBuilder attribute, String value) { int startPos = 0; startPos = unpack(category, value, startPos, ':'); startPos = unpack(attribute, value, startPos, ':'); } /** Given a path string, get a list of folders and projects under that node. *@param pathString is the current path (folder names and project names, separated by dots (.)). *@return a list of folder and project names, in sorted order, or null if the path was invalid. */ protected String[] getChildFolders(LivelinkContext llc, String pathString) throws ManifoldCFException, ServiceInterruption { RootValue rv = new RootValue(llc, pathString); // Get the volume, object id of the folder/project the path describes VolumeAndId vid = getPathId(rv); if (vid == null) return null; String filterString = "(SubType=" + LAPI_DOCUMENTS.FOLDERSUBTYPE + " or SubType=" + LAPI_DOCUMENTS.PROJECTSUBTYPE + " or SubType=" + LAPI_DOCUMENTS.COMPOUNDDOCUMENTSUBTYPE + ")"; int sanityRetryCount = FAILURE_RETRY_COUNT; while (true) { ListObjectsThread t = new ListObjectsThread(vid.getVolumeID(), vid.getPathId(), filterString); try { t.start(); LLValue children; try { children = t.finishUp(); } catch (ManifoldCFException e) { sanityRetryCount = assessRetry(sanityRetryCount, e); continue; } String[] rval = new String[children.size()]; int j = 0; while (j < children.size()) { rval[j] = children.toString(j, "Name"); j++; } return rval; } catch (InterruptedException e) { t.interrupt(); throw new ManifoldCFException("Interrupted: " + e.getMessage(), e, ManifoldCFException.INTERRUPTED); } catch (RuntimeException e) { sanityRetryCount = handleLivelinkRuntimeException(e, sanityRetryCount, true); continue; } } } /** Given a path string, get a list of categories under that node. *@param pathString is the current path (folder names and project names, separated by dots (.)). *@return a list of category names, in sorted order, or null if the path was invalid. */ protected String[] getChildCategories(LivelinkContext llc, String pathString) throws ManifoldCFException, ServiceInterruption { // Start at root RootValue rv = new RootValue(llc, pathString); // Get the volume, object id of the folder/project the path describes VolumeAndId vid = getPathId(rv); if (vid == null) return null; // We want only folders that are children of the current object and which match the specified subfolder String filterString = "SubType=" + LAPI_DOCUMENTS.CATEGORYSUBTYPE; int sanityRetryCount = FAILURE_RETRY_COUNT; while (true) { ListObjectsThread t = new ListObjectsThread(vid.getVolumeID(), vid.getPathId(), filterString); try { t.start(); LLValue children; try { children = t.finishUp(); } catch (ManifoldCFException e) { sanityRetryCount = assessRetry(sanityRetryCount, e); continue; } String[] rval = new String[children.size()]; int j = 0; while (j < children.size()) { rval[j] = children.toString(j, "Name"); j++; } return rval; } catch (InterruptedException e) { t.interrupt(); throw new ManifoldCFException("Interrupted: " + e.getMessage(), e, ManifoldCFException.INTERRUPTED); } catch (RuntimeException e) { sanityRetryCount = handleLivelinkRuntimeException(e, sanityRetryCount, true); continue; } } } protected class GetCategoryAttributesThread extends Thread { protected final int catObjectID; protected Throwable exception = null; protected LLValue rval = null; public GetCategoryAttributesThread(int catObjectID) { super(); setDaemon(true); this.catObjectID = catObjectID; } public void run() { try { LLValue catID = new LLValue(); catID.setAssoc(); catID.add("ID", catObjectID); catID.add("Type", LAPI_ATTRIBUTES.CATEGORY_TYPE_LIBRARY); LLValue catVersion = new LLValue(); int status = LLDocs.FetchCategoryVersion(catID, catVersion); if (status == 107105 || status == 107106) return; if (status != 0) { throw new ManifoldCFException("Error getting category version: " + Integer.toString(status)); } LLValue children = new LLValue(); status = LLAttributes.AttrListNames(catVersion, null, children); if (status != 0) { throw new ManifoldCFException("Error getting attribute names: " + Integer.toString(status)); } rval = children; } catch (Throwable e) { this.exception = e; } } public LLValue finishUp() throws ManifoldCFException, InterruptedException { join(); Throwable thr = exception; if (thr != null) { if (thr instanceof RuntimeException) throw (RuntimeException) thr; else if (thr instanceof ManifoldCFException) throw (ManifoldCFException) thr; else if (thr instanceof Error) throw (Error) thr; else throw new RuntimeException( "Unrecognized exception type: " + thr.getClass().getName() + ": " + thr.getMessage(), thr); } return rval; } } /** Given a category path, get a list of legal attribute names. *@param catObjectID is the object id of the category. *@return a list of attribute names, in sorted order, or null of the path was invalid. */ protected String[] getCategoryAttributes(int catObjectID) throws ManifoldCFException, ServiceInterruption { int sanityRetryCount = FAILURE_RETRY_COUNT; while (true) { GetCategoryAttributesThread t = new GetCategoryAttributesThread(catObjectID); try { t.start(); LLValue children; try { children = t.finishUp(); } catch (ManifoldCFException e) { sanityRetryCount = assessRetry(sanityRetryCount, e); continue; } if (children == null) return null; String[] rval = new String[children.size()]; LLValueEnumeration en = children.enumerateValues(); int j = 0; while (en.hasMoreElements()) { LLValue v = (LLValue) en.nextElement(); rval[j] = v.toString(); j++; } return rval; } catch (InterruptedException e) { t.interrupt(); throw new ManifoldCFException("Interrupted: " + e.getMessage(), e, ManifoldCFException.INTERRUPTED); } catch (RuntimeException e) { sanityRetryCount = handleLivelinkRuntimeException(e, sanityRetryCount, true); continue; } } } protected class GetCategoryVersionThread extends Thread { protected final int objID; protected final int catID; protected Throwable exception = null; protected LLValue rval = null; public GetCategoryVersionThread(int objID, int catID) { super(); setDaemon(true); this.objID = objID; this.catID = catID; } public void run() { try { // Set up the right llvalues // Object ID LLValue objIDValue = new LLValue().setAssoc(); objIDValue.add("ID", objID); // Current version, so don't set the "Version" field // CatID LLValue catIDValue = new LLValue().setAssoc(); catIDValue.add("ID", catID); catIDValue.add("Type", LAPI_ATTRIBUTES.CATEGORY_TYPE_LIBRARY); LLValue rvalue = new LLValue(); int status = LLDocs.GetObjectAttributesEx(objIDValue, catIDValue, rvalue); // If either the object is wrong, or the object does not have the specified category, return null. if (status == 103101 || status == 107205) return; if (status != 0) { throw new ManifoldCFException("Error retrieving category version: " + Integer.toString(status) + ": " + llServer.getErrors()); } rval = rvalue; } catch (Throwable e) { this.exception = e; } } public LLValue finishUp() throws ManifoldCFException, InterruptedException { join(); Throwable thr = exception; if (thr != null) { if (thr instanceof RuntimeException) throw (RuntimeException) thr; else if (thr instanceof ManifoldCFException) throw (ManifoldCFException) thr; else if (thr instanceof Error) throw (Error) thr; else throw new RuntimeException( "Unrecognized exception type: " + thr.getClass().getName() + ": " + thr.getMessage(), thr); } return rval; } } /** Get a category version for document. */ protected LLValue getCatVersion(int objID, int catID) throws ManifoldCFException, ServiceInterruption { int sanityRetryCount = FAILURE_RETRY_COUNT; while (true) { GetCategoryVersionThread t = new GetCategoryVersionThread(objID, catID); try { t.start(); try { return t.finishUp(); } catch (ManifoldCFException e) { sanityRetryCount = assessRetry(sanityRetryCount, e); continue; } } catch (InterruptedException e) { t.interrupt(); throw new ManifoldCFException("Interrupted: " + e.getMessage(), e, ManifoldCFException.INTERRUPTED); } catch (NullPointerException npe) { // LAPI throws a null pointer exception under very rare conditions when the GetObjectAttributesEx is // called. The conditions are not clear at this time - it could even be due to Livelink corruption. // However, I'm going to have to treat this as // indicating that this category version does not exist for this document. Logging.connectors .warn("Livelink: Null pointer exception thrown trying to get cat version for category " + Integer.toString(catID) + " for object " + Integer.toString(objID)); return null; } catch (RuntimeException e) { sanityRetryCount = handleLivelinkRuntimeException(e, sanityRetryCount, true); continue; } } } protected class GetAttributeValueThread extends Thread { protected final LLValue categoryVersion; protected final String attributeName; protected Throwable exception = null; protected LLValue rval = null; public GetAttributeValueThread(LLValue categoryVersion, String attributeName) { super(); setDaemon(true); this.categoryVersion = categoryVersion; this.attributeName = attributeName; } public void run() { try { // Set up the right llvalues LLValue children = new LLValue(); int status = LLAttributes.AttrGetValues(categoryVersion, attributeName, 0, null, children); // "Not found" status - I don't know if it possible to get this here, but if so, behave civilly if (status == 103101) return; // This seems to be the real error LAPI returns if you don't have an attribute of this name if (status == 8000604) return; if (status != 0) { throw new ManifoldCFException("Error retrieving attribute value: " + Integer.toString(status) + ": " + llServer.getErrors()); } rval = children; } catch (Throwable e) { this.exception = e; } } public LLValue finishUp() throws ManifoldCFException, InterruptedException { join(); Throwable thr = exception; if (thr != null) { if (thr instanceof RuntimeException) throw (RuntimeException) thr; else if (thr instanceof ManifoldCFException) throw (ManifoldCFException) thr; else if (thr instanceof Error) throw (Error) thr; else throw new RuntimeException( "Unrecognized exception type: " + thr.getClass().getName() + ": " + thr.getMessage(), thr); } return rval; } } /** Get an attribute value from a category version. */ protected String[] getAttributeValue(LLValue categoryVersion, String attributeName) throws ManifoldCFException, ServiceInterruption { int sanityRetryCount = FAILURE_RETRY_COUNT; while (true) { GetAttributeValueThread t = new GetAttributeValueThread(categoryVersion, attributeName); try { t.start(); LLValue children; try { children = t.finishUp(); } catch (ManifoldCFException e) { sanityRetryCount = assessRetry(sanityRetryCount, e); continue; } if (children == null) return null; String[] rval = new String[children.size()]; LLValueEnumeration en = children.enumerateValues(); int j = 0; while (en.hasMoreElements()) { LLValue v = (LLValue) en.nextElement(); rval[j] = v.toString(); j++; } return rval; } catch (InterruptedException e) { t.interrupt(); throw new ManifoldCFException("Interrupted: " + e.getMessage(), e, ManifoldCFException.INTERRUPTED); } catch (RuntimeException e) { sanityRetryCount = handleLivelinkRuntimeException(e, sanityRetryCount, true); continue; } } } protected class GetObjectRightsThread extends Thread { protected final int vol; protected final int objID; protected Throwable exception = null; protected LLValue rval = null; public GetObjectRightsThread(int vol, int objID) { super(); setDaemon(true); this.vol = vol; this.objID = objID; } public void run() { try { LLValue childrenObjects = new LLValue(); int status = LLDocs.GetObjectRights(vol, objID, childrenObjects); // If the rights object doesn't exist, behave civilly if (status == 103101) return; if (status != 0) { throw new ManifoldCFException("Error retrieving document rights: " + Integer.toString(status) + ": " + llServer.getErrors()); } rval = childrenObjects; } catch (Throwable e) { this.exception = e; } } public LLValue finishUp() throws ManifoldCFException, InterruptedException { join(); Throwable thr = exception; if (thr != null) { if (thr instanceof RuntimeException) throw (RuntimeException) thr; else if (thr instanceof ManifoldCFException) throw (ManifoldCFException) thr; else if (thr instanceof Error) throw (Error) thr; else throw new RuntimeException( "Unrecognized exception type: " + thr.getClass().getName() + ": " + thr.getMessage(), thr); } return rval; } } /** Get an object's rights. This will be an array of right id's, including the special * ones defined by Livelink, or null will be returned (if the object is not found). *@param vol is the volume id *@param objID is the object id *@return the array. */ protected int[] getObjectRights(int vol, int objID) throws ManifoldCFException, ServiceInterruption { int sanityRetryCount = FAILURE_RETRY_COUNT; while (true) { GetObjectRightsThread t = new GetObjectRightsThread(vol, objID); try { t.start(); LLValue childrenObjects; try { childrenObjects = t.finishUp(); } catch (ManifoldCFException e) { sanityRetryCount = assessRetry(sanityRetryCount, e); continue; } if (childrenObjects == null) return null; int size; if (childrenObjects.isRecord()) size = 1; else if (childrenObjects.isTable()) size = childrenObjects.size(); else size = 0; int minPermission = LAPI_DOCUMENTS.PERM_SEE + LAPI_DOCUMENTS.PERM_SEECONTENTS; int j = 0; int count = 0; while (j < size) { int permission = childrenObjects.toInteger(j, "Permissions"); // Only if the permission is "see contents" can we consider this // access token! if ((permission & minPermission) == minPermission) count++; j++; } int[] rval = new int[count]; j = 0; count = 0; while (j < size) { int token = childrenObjects.toInteger(j, "RightID"); int permission = childrenObjects.toInteger(j, "Permissions"); // Only if the permission is "see contents" can we consider this // access token! if ((permission & minPermission) == minPermission) rval[count++] = token; j++; } return rval; } catch (InterruptedException e) { t.interrupt(); throw new ManifoldCFException("Interrupted: " + e.getMessage(), e, ManifoldCFException.INTERRUPTED); } catch (RuntimeException e) { sanityRetryCount = handleLivelinkRuntimeException(e, sanityRetryCount, true); continue; } } } /** Local cache for various kinds of objects that may be useful more than once. */ protected class LivelinkContext { /** Cache of ObjectInformation objects. */ protected final Map<ObjectInformation, ObjectInformation> objectInfoMap = new HashMap<ObjectInformation, ObjectInformation>(); /** Cache of VersionInformation objects. */ protected final Map<VersionInformation, VersionInformation> versionInfoMap = new HashMap<VersionInformation, VersionInformation>(); /** Cache of UserInformation objects */ protected final Map<UserInformation, UserInformation> userInfoMap = new HashMap<UserInformation, UserInformation>(); public LivelinkContext() { } public ObjectInformation getObjectInformation(int volumeID, int objectID) { ObjectInformation oi = new ObjectInformation(volumeID, objectID); ObjectInformation lookupValue = objectInfoMap.get(oi); if (lookupValue == null) { objectInfoMap.put(oi, oi); return oi; } return lookupValue; } public VersionInformation getVersionInformation(int volumeID, int objectID, int revisionNumber) { VersionInformation vi = new VersionInformation(volumeID, objectID, revisionNumber); VersionInformation lookupValue = versionInfoMap.get(vi); if (lookupValue == null) { versionInfoMap.put(vi, vi); return vi; } return lookupValue; } public UserInformation getUserInformation(int userID) { UserInformation ui = new UserInformation(userID); UserInformation lookupValue = userInfoMap.get(ui); if (lookupValue == null) { userInfoMap.put(ui, ui); return ui; } return lookupValue; } } /** This object represents a cache of user information. * Initialize it with the user ID. Then, request desired fields from it. */ protected class UserInformation { protected final int userID; protected LLValue userValue = null; public UserInformation(int userID) { this.userID = userID; } public boolean exists() throws ServiceInterruption, ManifoldCFException { return getUserValue() != null; } public String getName() throws ServiceInterruption, ManifoldCFException { LLValue userValue = getUserValue(); if (userValue == null) return null; return userValue.toString("NAME"); } protected LLValue getUserValue() throws ServiceInterruption, ManifoldCFException { if (userValue == null) { int sanityRetryCount = FAILURE_RETRY_COUNT; while (true) { GetUserInfoThread t = new GetUserInfoThread(userID); try { t.start(); try { userValue = t.finishUp(); } catch (ManifoldCFException e) { sanityRetryCount = assessRetry(sanityRetryCount, e); continue; } break; } catch (InterruptedException e) { t.interrupt(); throw new ManifoldCFException("Interrupted: " + e.getMessage(), e, ManifoldCFException.INTERRUPTED); } catch (RuntimeException e) { sanityRetryCount = handleLivelinkRuntimeException(e, sanityRetryCount, true); continue; } } } return userValue; } @Override public String toString() { return "(" + userID + ")"; } @Override public int hashCode() { return (userID << 5) ^ (userID >> 3); } @Override public boolean equals(Object o) { if (!(o instanceof UserInformation)) return false; UserInformation other = (UserInformation) o; return userID == other.userID; } } /** This object represents a cache of version information. * Initialize it with the volume ID and object ID and revision number (usually zero). * Then, request the desired fields from it. */ protected class VersionInformation { protected final int volumeID; protected final int objectID; protected final int revisionNumber; protected LLValue versionValue = null; public VersionInformation(int volumeID, int objectID, int revisionNumber) { this.volumeID = volumeID; this.objectID = objectID; this.revisionNumber = revisionNumber; } public boolean exists() throws ServiceInterruption, ManifoldCFException { return getVersionValue() != null; } /** Get data size. */ public Long getDataSize() throws ServiceInterruption, ManifoldCFException { LLValue elem = getVersionValue(); if (elem == null) return null; return new Long(elem.toLong("FILEDATASIZE")); } /** Get file name. */ public String getFileName() throws ServiceInterruption, ManifoldCFException { LLValue elem = getVersionValue(); if (elem == null) return null; return elem.toString("FILENAME"); } /** Get mime type. */ public String getMimeType() throws ServiceInterruption, ManifoldCFException { LLValue elem = getVersionValue(); if (elem == null) return null; return elem.toString("MIMETYPE"); } /** Get modify date. */ public Date getModifyDate() throws ServiceInterruption, ManifoldCFException { LLValue elem = getVersionValue(); if (elem == null) return null; return elem.toDate("MODIFYDATE"); } /** Get modifier. */ public Integer getOwnerId() throws ServiceInterruption, ManifoldCFException { LLValue elem = getVersionValue(); if (elem == null) return null; return new Integer(elem.toInteger("OWNER")); } /** Get version LLValue */ protected LLValue getVersionValue() throws ServiceInterruption, ManifoldCFException { if (versionValue == null) { int sanityRetryCount = FAILURE_RETRY_COUNT; while (true) { GetVersionInfoThread t = new GetVersionInfoThread(volumeID, objectID, revisionNumber); try { t.start(); try { versionValue = t.finishUp(); } catch (ManifoldCFException e) { sanityRetryCount = assessRetry(sanityRetryCount, e); continue; } break; } catch (InterruptedException e) { t.interrupt(); throw new ManifoldCFException("Interrupted: " + e.getMessage(), e, ManifoldCFException.INTERRUPTED); } catch (RuntimeException e) { sanityRetryCount = handleLivelinkRuntimeException(e, sanityRetryCount, true); continue; } } } return versionValue; } @Override public int hashCode() { return (volumeID << 5) ^ (volumeID >> 3) ^ (objectID << 5) ^ (objectID >> 3) ^ (revisionNumber << 5) ^ (revisionNumber >> 3); } @Override public boolean equals(Object o) { if (!(o instanceof VersionInformation)) return false; VersionInformation other = (VersionInformation) o; return volumeID == other.volumeID && objectID == other.objectID && revisionNumber == other.revisionNumber; } } /** This object represents an object information cache. * Initialize it with the volume ID and object ID, and then request * the appropriate fields from it. Keep it around as long as needed; it functions as a cache * of sorts... */ protected class ObjectInformation { protected final int volumeID; protected final int objectID; protected LLValue objectValue = null; public ObjectInformation(int volumeID, int objectID) { this.volumeID = volumeID; this.objectID = objectID; } /** * Check whether object seems to exist or not. */ public boolean exists() throws ServiceInterruption, ManifoldCFException { return getObjectValue() != null; } /** Check if this object is the category workspace. */ public boolean isCategoryWorkspace() { return objectID == LLCATWK_ID; } /** Check if this object is the entity workspace. */ public boolean isEntityWorkspace() { return objectID == LLENTWK_ID; } /** toString override */ @Override public String toString() { return "(Volume: " + volumeID + ", Object: " + objectID + ")"; } /** * Returns the object ID specified by the path name. * @param startPath is the folder name (a string with dots as separators) */ public VolumeAndId getPathId(String startPath) throws ServiceInterruption, ManifoldCFException { LLValue objInfo = getObjectValue(); if (objInfo == null) return null; // Grab the volume ID and starting object int obj = objInfo.toInteger("ID"); int vol = objInfo.toInteger("VolumeID"); // Pick apart the start path. This is a string separated by slashes. int charindex = 0; while (charindex < startPath.length()) { StringBuilder currentTokenBuffer = new StringBuilder(); // Find the current token while (charindex < startPath.length()) { char x = startPath.charAt(charindex++); if (x == '/') break; if (x == '\\') { // Attempt to escape what follows x = startPath.charAt(charindex); charindex++; } currentTokenBuffer.append(x); } String subFolder = currentTokenBuffer.toString(); // We want only folders that are children of the current object and which match the specified subfolder String filterString = "(SubType=" + LAPI_DOCUMENTS.FOLDERSUBTYPE + " or SubType=" + LAPI_DOCUMENTS.PROJECTSUBTYPE + " or SubType=" + LAPI_DOCUMENTS.COMPOUNDDOCUMENTSUBTYPE + ") and Name='" + subFolder + "'"; int sanityRetryCount = FAILURE_RETRY_COUNT; while (true) { ListObjectsThread t = new ListObjectsThread(vol, obj, filterString); try { t.start(); LLValue children; try { children = t.finishUp(); } catch (ManifoldCFException e) { sanityRetryCount = assessRetry(sanityRetryCount, e); continue; } if (children == null) return null; // If there is one child, then we are okay. if (children.size() == 1) { // New starting point is the one we found. obj = children.toInteger(0, "ID"); int subtype = children.toInteger(0, "SubType"); if (subtype == LAPI_DOCUMENTS.PROJECTSUBTYPE) { vol = obj; obj = -obj; } } else { // Couldn't find the path. Instead of throwing up, return null to indicate // illegal node. return null; } break; } catch (InterruptedException e) { t.interrupt(); throw new ManifoldCFException("Interrupted: " + e.getMessage(), e, ManifoldCFException.INTERRUPTED); } catch (RuntimeException e) { sanityRetryCount = handleLivelinkRuntimeException(e, sanityRetryCount, true); continue; } } } return new VolumeAndId(vol, obj); } /** * Returns the category ID specified by the path name. * @param startPath is the folder name, ending in a category name (a string with slashes as separators) */ public int getCategoryId(String startPath) throws ManifoldCFException, ServiceInterruption { LLValue objInfo = getObjectValue(); if (objInfo == null) return -1; // Grab the volume ID and starting object int obj = objInfo.toInteger("ID"); int vol = objInfo.toInteger("VolumeID"); // Pick apart the start path. This is a string separated by slashes. if (startPath.length() == 0) return -1; int charindex = 0; while (charindex < startPath.length()) { StringBuilder currentTokenBuffer = new StringBuilder(); // Find the current token while (charindex < startPath.length()) { char x = startPath.charAt(charindex++); if (x == '/') break; if (x == '\\') { // Attempt to escape what follows x = startPath.charAt(charindex); charindex++; } currentTokenBuffer.append(x); } String subFolder = currentTokenBuffer.toString(); String filterString; // We want only folders that are children of the current object and which match the specified subfolder if (charindex < startPath.length()) filterString = "(SubType=" + LAPI_DOCUMENTS.FOLDERSUBTYPE + " or SubType=" + LAPI_DOCUMENTS.PROJECTSUBTYPE + " or SubType=" + LAPI_DOCUMENTS.COMPOUNDDOCUMENTSUBTYPE + ")"; else filterString = "SubType=" + LAPI_DOCUMENTS.CATEGORYSUBTYPE; filterString += " and Name='" + subFolder + "'"; int sanityRetryCount = FAILURE_RETRY_COUNT; while (true) { ListObjectsThread t = new ListObjectsThread(vol, obj, filterString); try { t.start(); LLValue children; try { children = t.finishUp(); } catch (ManifoldCFException e) { sanityRetryCount = assessRetry(sanityRetryCount, e); continue; } if (children == null) return -1; // If there is one child, then we are okay. if (children.size() == 1) { // New starting point is the one we found. obj = children.toInteger(0, "ID"); int subtype = children.toInteger(0, "SubType"); if (subtype == LAPI_DOCUMENTS.PROJECTSUBTYPE) { vol = obj; obj = -obj; } } else { // Couldn't find the path. Instead of throwing up, return null to indicate // illegal node. return -1; } break; } catch (InterruptedException e) { t.interrupt(); throw new ManifoldCFException("Interrupted: " + e.getMessage(), e, ManifoldCFException.INTERRUPTED); } catch (RuntimeException e) { sanityRetryCount = handleLivelinkRuntimeException(e, sanityRetryCount, true); continue; } } } return obj; } /** Get permissions. */ public Integer getPermissions() throws ServiceInterruption, ManifoldCFException { LLValue elem = getObjectValue(); if (elem == null) return null; return new Integer(objectValue.toInteger("Permissions")); } /** Get OpenText document name. */ public String getName() throws ServiceInterruption, ManifoldCFException { LLValue elem = getObjectValue(); if (elem == null) return null; return elem.toString("NAME"); } /** Get OpenText comments/description. */ public String getComments() throws ServiceInterruption, ManifoldCFException { LLValue elem = getObjectValue(); if (elem == null) return null; return elem.toString("COMMENT"); } /** Get parent ID. */ public Integer getParentId() throws ServiceInterruption, ManifoldCFException { LLValue elem = getObjectValue(); if (elem == null) return null; return new Integer(elem.toInteger("ParentId")); } /** Get owner ID. */ public Integer getOwnerId() throws ServiceInterruption, ManifoldCFException { LLValue elem = getObjectValue(); if (elem == null) return null; return new Integer(elem.toInteger("UserId")); } /** Get group ID. */ public Integer getGroupId() throws ServiceInterruption, ManifoldCFException { LLValue elem = getObjectValue(); if (elem == null) return null; return new Integer(elem.toInteger("GroupId")); } /** Get creation date. */ public Date getCreationDate() throws ServiceInterruption, ManifoldCFException { LLValue elem = getObjectValue(); if (elem == null) return null; return elem.toDate("CREATEDATE"); } /** Get creator ID. */ public Integer getCreatorId() throws ServiceInterruption, ManifoldCFException { LLValue elem = getObjectValue(); if (elem == null) return null; return new Integer(elem.toInteger("CREATEDBY")); } /* Get modify date. */ public Date getModifyDate() throws ServiceInterruption, ManifoldCFException { LLValue elem = getObjectValue(); if (elem == null) return null; return elem.toDate("ModifyDate"); } /** Get the objInfo object. */ protected LLValue getObjectValue() throws ServiceInterruption, ManifoldCFException { if (objectValue == null) { int sanityRetryCount = FAILURE_RETRY_COUNT; while (true) { GetObjectInfoThread t = new GetObjectInfoThread(volumeID, objectID); try { t.start(); try { objectValue = t.finishUp(); } catch (ManifoldCFException e) { sanityRetryCount = assessRetry(sanityRetryCount, e); continue; } break; } catch (InterruptedException e) { t.interrupt(); throw new ManifoldCFException("Interrupted: " + e.getMessage(), e, ManifoldCFException.INTERRUPTED); } catch (RuntimeException e) { sanityRetryCount = handleLivelinkRuntimeException(e, sanityRetryCount, true); continue; } } } return objectValue; } @Override public int hashCode() { return (volumeID << 5) ^ (volumeID >> 3) ^ (objectID << 5) ^ (objectID >> 3); } @Override public boolean equals(Object o) { if (!(o instanceof ObjectInformation)) return false; ObjectInformation other = (ObjectInformation) o; return volumeID == other.volumeID && objectID == other.objectID; } } /** Thread we can abandon that lists all users (except admin). */ protected class ListUsersThread extends Thread { protected LLValue rval = null; protected Throwable exception = null; public ListUsersThread() { super(); setDaemon(true); } public void run() { try { LLValue userList = new LLValue(); int status = LLUsers.ListUsers(userList); if (Logging.connectors.isDebugEnabled()) { Logging.connectors.debug("Livelink: User list retrieved: status=" + Integer.toString(status)); } if (status < 0) { Logging.connectors.debug("Livelink: User list inaccessable (" + llServer.getErrors() + ")"); return; } if (status != 0) { throw new ManifoldCFException("Error retrieving user list: status=" + Integer.toString(status) + " (" + llServer.getErrors() + ")"); } rval = userList; } catch (Throwable e) { this.exception = e; } } public LLValue finishUp() throws ManifoldCFException, InterruptedException { join(); Throwable thr = exception; if (thr != null) { if (thr instanceof RuntimeException) throw (RuntimeException) thr; else if (thr instanceof ManifoldCFException) throw (ManifoldCFException) thr; else if (thr instanceof Error) throw (Error) thr; else throw new RuntimeException( "Unrecognized exception type: " + thr.getClass().getName() + ": " + thr.getMessage(), thr); } return rval; } } /** Thread we can abandon that gets user information for a userID. */ protected class GetUserInfoThread extends Thread { protected final int user; protected Throwable exception = null; protected LLValue rval = null; public GetUserInfoThread(int user) { super(); setDaemon(true); this.user = user; } public void run() { try { LLValue userinfo = new LLValue().setAssoc(); int status = LLUsers.GetUserByID(user, userinfo); // Need to detect if object was deleted, and return null in this case!!! if (Logging.connectors.isDebugEnabled()) { Logging.connectors.debug("Livelink: User status retrieved for " + Integer.toString(user) + ": status=" + Integer.toString(status)); } // Treat both 103101 and 103102 as 'object not found'. 401101 is 'user not found'. if (status == 103101 || status == 103102 || status == 401101) return; // This error means we don't have permission to get the object's status, apparently if (status < 0) { Logging.connectors.debug("Livelink: User info inaccessable for user " + Integer.toString(user) + " (" + llServer.getErrors() + ")"); return; } if (status != 0) { throw new ManifoldCFException("Error retrieving user " + Integer.toString(user) + ": status=" + Integer.toString(status) + " (" + llServer.getErrors() + ")"); } rval = userinfo; } catch (Throwable e) { this.exception = e; } } public LLValue finishUp() throws ManifoldCFException, InterruptedException { join(); Throwable thr = exception; if (thr != null) { if (thr instanceof RuntimeException) throw (RuntimeException) thr; else if (thr instanceof ManifoldCFException) throw (ManifoldCFException) thr; else if (thr instanceof Error) throw (Error) thr; else throw new RuntimeException( "Unrecognized exception type: " + thr.getClass().getName() + ": " + thr.getMessage(), thr); } return rval; } } /** Thread we can abandon that gets version information for a volume and an id and a revision. */ protected class GetVersionInfoThread extends Thread { protected final int vol; protected final int id; protected final int revNumber; protected Throwable exception = null; protected LLValue rval = null; public GetVersionInfoThread(int vol, int id, int revNumber) { super(); setDaemon(true); this.vol = vol; this.id = id; this.revNumber = revNumber; } public void run() { try { LLValue versioninfo = new LLValue().setAssocNotSet(); int status = LLDocs.GetVersionInfo(vol, id, revNumber, versioninfo); // Need to detect if object was deleted, and return null in this case!!! if (Logging.connectors.isDebugEnabled()) { Logging.connectors.debug("Livelink: Version status retrieved for " + Integer.toString(vol) + ":" + Integer.toString(id) + ", rev " + revNumber + ": status=" + Integer.toString(status)); } // Treat both 103101 and 103102 as 'object not found'. if (status == 103101 || status == 103102) return; // This error means we don't have permission to get the object's status, apparently if (status < 0) { Logging.connectors.debug("Livelink: Version info inaccessable for object " + Integer.toString(vol) + ":" + Integer.toString(id) + ", rev " + revNumber + " (" + llServer.getErrors() + ")"); return; } if (status != 0) { throw new ManifoldCFException("Error retrieving document version " + Integer.toString(vol) + ":" + Integer.toString(id) + ", rev " + revNumber + ": status=" + Integer.toString(status) + " (" + llServer.getErrors() + ")"); } rval = versioninfo; } catch (Throwable e) { this.exception = e; } } public LLValue finishUp() throws ManifoldCFException, InterruptedException { join(); Throwable thr = exception; if (thr != null) { if (thr instanceof RuntimeException) throw (RuntimeException) thr; else if (thr instanceof ManifoldCFException) throw (ManifoldCFException) thr; else if (thr instanceof Error) throw (Error) thr; else throw new RuntimeException( "Unrecognized exception type: " + thr.getClass().getName() + ": " + thr.getMessage(), thr); } return rval; } } /** Thread we can abandon that gets object information for a volume and an id. */ protected class GetObjectInfoThread extends Thread { protected int vol; protected int id; protected Throwable exception = null; protected LLValue rval = null; public GetObjectInfoThread(int vol, int id) { super(); setDaemon(true); this.vol = vol; this.id = id; } public void run() { try { LLValue objinfo = new LLValue().setAssocNotSet(); int status = LLDocs.GetObjectInfo(vol, id, objinfo); // Need to detect if object was deleted, and return null in this case!!! if (Logging.connectors.isDebugEnabled()) { Logging.connectors.debug("Livelink: Status retrieved for " + Integer.toString(vol) + ":" + Integer.toString(id) + ": status=" + Integer.toString(status)); } // Treat both 103101 and 103102 as 'object not found'. if (status == 103101 || status == 103102) return; // This error means we don't have permission to get the object's status, apparently if (status < 0) { Logging.connectors .debug("Livelink: Object info inaccessable for object " + Integer.toString(vol) + ":" + Integer.toString(id) + " (" + llServer.getErrors() + ")"); return; } if (status != 0) { throw new ManifoldCFException( "Error retrieving document object " + Integer.toString(vol) + ":" + Integer.toString(id) + ": status=" + Integer.toString(status) + " (" + llServer.getErrors() + ")"); } rval = objinfo; } catch (Throwable e) { this.exception = e; } } public LLValue finishUp() throws ManifoldCFException, InterruptedException { join(); Throwable thr = exception; if (thr != null) { if (thr instanceof RuntimeException) throw (RuntimeException) thr; else if (thr instanceof ManifoldCFException) throw (ManifoldCFException) thr; else if (thr instanceof Error) throw (Error) thr; else throw new RuntimeException( "Unrecognized exception type: " + thr.getClass().getName() + ": " + thr.getMessage(), thr); } return rval; } } /** Build a set of actual acls given a set of rights */ protected String[] lookupTokens(int[] rights, ObjectInformation objInfo) throws ManifoldCFException, ServiceInterruption { if (!objInfo.exists()) return null; String[] convertedAcls = new String[rights.length]; LLValue infoObject = null; int j = 0; int k = 0; while (j < rights.length) { int token = rights[j++]; String tokenValue; // Consider this token switch (token) { case LAPI_DOCUMENTS.RIGHT_OWNER: // Look up user for current document (UserID attribute) tokenValue = objInfo.getOwnerId().toString(); break; case LAPI_DOCUMENTS.RIGHT_GROUP: tokenValue = objInfo.getGroupId().toString(); break; case LAPI_DOCUMENTS.RIGHT_WORLD: // Add "Guest" token tokenValue = "GUEST"; break; case LAPI_DOCUMENTS.RIGHT_SYSTEM: // Add "System" token tokenValue = "SYSTEM"; break; default: tokenValue = Integer.toString(token); break; } // This might return a null if we could not look up the object corresponding to the right. If so, it is safe to skip it because // that always RESTRICTS view of the object (maybe erroneously), but does not expand visibility. if (tokenValue != null) convertedAcls[k++] = tokenValue; } String[] actualAcls = new String[k]; j = 0; while (j < k) { actualAcls[j] = convertedAcls[j]; j++; } return actualAcls; } protected class GetObjectCategoryIDsThread extends Thread { protected final int vol; protected final int id; protected Throwable exception = null; protected LLValue rval = null; public GetObjectCategoryIDsThread(int vol, int id) { super(); setDaemon(true); this.vol = vol; this.id = id; } public void run() { try { // Object ID LLValue objIDValue = new LLValue().setAssocNotSet(); objIDValue.add("ID", id); // Category ID List LLValue catIDList = new LLValue().setAssocNotSet(); int status = LLDocs.ListObjectCategoryIDs(objIDValue, catIDList); // Need to detect if object was deleted, and return null in this case!!! if (Logging.connectors.isDebugEnabled()) { Logging.connectors.debug( "Livelink: Status value for getting object categories for " + Integer.toString(vol) + ":" + Integer.toString(id) + " is: " + Integer.toString(status)); } if (status == 103101) return; if (status != 0) { throw new ManifoldCFException("Error retrieving document categories for " + Integer.toString(vol) + ":" + Integer.toString(id) + ": status=" + Integer.toString(status) + " (" + llServer.getErrors() + ")"); } rval = catIDList; } catch (Throwable e) { this.exception = e; } } public LLValue finishUp() throws ManifoldCFException, InterruptedException { join(); Throwable thr = exception; if (thr != null) { if (thr instanceof RuntimeException) throw (RuntimeException) thr; else if (thr instanceof ManifoldCFException) throw (ManifoldCFException) thr; else if (thr instanceof Error) throw (Error) thr; else throw new RuntimeException( "Unrecognized exception type: " + thr.getClass().getName() + ": " + thr.getMessage(), thr); } return rval; } } /** Get category IDs associated with an object. * @param vol is the volume ID * @param id the object ID * @return an array of integers containing category identifiers, or null if the object is not found. */ protected int[] getObjectCategoryIDs(int vol, int id) throws ManifoldCFException, ServiceInterruption { int sanityRetryCount = FAILURE_RETRY_COUNT; while (true) { GetObjectCategoryIDsThread t = new GetObjectCategoryIDsThread(vol, id); try { t.start(); LLValue catIDList; try { catIDList = t.finishUp(); } catch (ManifoldCFException e) { sanityRetryCount = assessRetry(sanityRetryCount, e); continue; } if (catIDList == null) return null; int size = catIDList.size(); if (Logging.connectors.isDebugEnabled()) { Logging.connectors.debug("Livelink: Object " + Integer.toString(vol) + ":" + Integer.toString(id) + " has " + Integer.toString(size) + " attached categories"); } // Count the category ids int count = 0; int j = 0; while (j < size) { int type = catIDList.toValue(j).toInteger("Type"); if (type == LAPI_ATTRIBUTES.CATEGORY_TYPE_LIBRARY) count++; j++; } int[] rval = new int[count]; // Do the scan j = 0; count = 0; while (j < size) { int type = catIDList.toValue(j).toInteger("Type"); if (type == LAPI_ATTRIBUTES.CATEGORY_TYPE_LIBRARY) { int childID = catIDList.toValue(j).toInteger("ID"); rval[count++] = childID; } j++; } if (Logging.connectors.isDebugEnabled()) { Logging.connectors .debug("Livelink: Object " + Integer.toString(vol) + ":" + Integer.toString(id) + " has " + Integer.toString(rval.length) + " attached library categories"); } return rval; } catch (InterruptedException e) { t.interrupt(); throw new ManifoldCFException("Interrupted: " + e.getMessage(), e, ManifoldCFException.INTERRUPTED); } catch (RuntimeException e) { sanityRetryCount = handleLivelinkRuntimeException(e, sanityRetryCount, true); continue; } } } /** RootValue version of getPathId. */ protected VolumeAndId getPathId(RootValue rv) throws ManifoldCFException, ServiceInterruption { return rv.getRootValue().getPathId(rv.getRemainderPath()); } /** Rootvalue version of getCategoryId. */ protected int getCategoryId(RootValue rv) throws ManifoldCFException, ServiceInterruption { return rv.getRootValue().getCategoryId(rv.getRemainderPath()); } // Protected static methods /** Check if a file or directory should be included, given a document specification. *@param filename is the name of the "file". *@param documentSpecification is the specification. *@return true if it should be included. */ protected static boolean checkInclude(String filename, Specification documentSpecification) throws ManifoldCFException { // Scan includes to insure we match int i = 0; while (i < documentSpecification.getChildCount()) { SpecificationNode sn = documentSpecification.getChild(i); if (sn.getType().equals("include")) { String filespec = sn.getAttributeValue("filespec"); // If it matches, we can exit this loop. if (checkMatch(filename, 0, filespec)) break; } i++; } if (i == documentSpecification.getChildCount()) return false; // We matched an include. Now, scan excludes to ditch it if needed. i = 0; while (i < documentSpecification.getChildCount()) { SpecificationNode sn = documentSpecification.getChild(i); if (sn.getType().equals("exclude")) { String filespec = sn.getAttributeValue("filespec"); // If it matches, we return false. if (checkMatch(filename, 0, filespec)) return false; } i++; } // System.out.println("Match!"); return true; } /** Check if a file should be ingested, given a document specification. It is presumed that * documents that pass checkInclude() will be checked with this method. *@param objID is the file ID. *@param documentSpecification is the specification. */ protected boolean checkIngest(LivelinkContext llc, int objID, Specification documentSpecification) throws ManifoldCFException { // Since the only exclusions at this point are not based on file contents, this is a no-op. return true; } /** Check a match between two strings with wildcards. *@param sourceMatch is the expanded string (no wildcards) *@param sourceIndex is the starting point in the expanded string. *@param match is the wildcard-based string. *@return true if there is a match. */ protected static boolean checkMatch(String sourceMatch, int sourceIndex, String match) { // Note: The java regex stuff looks pretty heavyweight for this purpose. // I've opted to try and do a simple recursive version myself, which is not compiled. // Basically, the match proceeds by recursive descent through the string, so that all *'s cause // recursion. boolean caseSensitive = false; return processCheck(caseSensitive, sourceMatch, sourceIndex, match, 0); } /** Recursive worker method for checkMatch. Returns 'true' if there is a path that consumes both * strings in their entirety in a matched way. *@param caseSensitive is true if file names are case sensitive. *@param sourceMatch is the source string (w/o wildcards) *@param sourceIndex is the current point in the source string. *@param match is the match string (w/wildcards) *@param matchIndex is the current point in the match string. *@return true if there is a match. */ protected static boolean processCheck(boolean caseSensitive, String sourceMatch, int sourceIndex, String match, int matchIndex) { // Logging.connectors.debug("Matching '"+sourceMatch+"' position "+Integer.toString(sourceIndex)+ // " against '"+match+"' position "+Integer.toString(matchIndex)); // Match up through the next * we encounter while (true) { // If we've reached the end, it's a match. if (sourceMatch.length() == sourceIndex && match.length() == matchIndex) return true; // If one has reached the end but the other hasn't, no match if (match.length() == matchIndex) return false; if (sourceMatch.length() == sourceIndex) { if (match.charAt(matchIndex) != '*') return false; matchIndex++; continue; } char x = sourceMatch.charAt(sourceIndex); char y = match.charAt(matchIndex); if (!caseSensitive) { if (x >= 'A' && x <= 'Z') x -= 'A' - 'a'; if (y >= 'A' && y <= 'Z') y -= 'A' - 'a'; } if (y == '*') { // Wildcard! // We will recurse at this point. // Basically, we want to combine the results for leaving the "*" in the match string // at this point and advancing the source index, with skipping the "*" and leaving the source // string alone. return processCheck(caseSensitive, sourceMatch, sourceIndex + 1, match, matchIndex) || processCheck(caseSensitive, sourceMatch, sourceIndex, match, matchIndex + 1); } if (y == '?' || x == y) { sourceIndex++; matchIndex++; } else return false; } } /** Class for returning volume id/folder id combination on path lookup. */ protected static class VolumeAndId { protected final int volumeID; protected final int folderID; public VolumeAndId(int volumeID, int folderID) { this.volumeID = volumeID; this.folderID = folderID; } public int getVolumeID() { return volumeID; } public int getPathId() { return folderID; } } /** Class that describes a metadata catid and path. */ protected static class MetadataPathItem { protected final int catID; protected final String catName; /** Constructor. */ public MetadataPathItem(int catID, String catName) { this.catID = catID; this.catName = catName; } /** Get the cat ID. *@return the id. */ public int getCatID() { return catID; } /** Get the cat name. *@return the category name path. */ public String getCatName() { return catName; } } /** Class that describes a metadata catid and attribute set. */ protected static class MetadataItem { protected final MetadataPathItem pathItem; protected final Set<String> attributeNames = new HashSet<String>(); /** Constructor. */ public MetadataItem(MetadataPathItem pathItem) { this.pathItem = pathItem; } /** Add an attribute name. */ public void addAttribute(String attributeName) { attributeNames.add(attributeName); } /** Get the path object. *@return the object. */ public MetadataPathItem getPathItem() { return pathItem; } /** Get an iterator over the attribute names. *@return the iterator. */ public Iterator<String> getAttributeNames() { return attributeNames.iterator(); } } /** Class that tracks paths associated with nodes, and also keeps track of the name * of the metadata attribute to use for the path. */ protected class SystemMetadataDescription { // The livelink context protected final LivelinkContext llc; // The path attribute name protected final String pathAttributeName; // The path separator protected final String pathSeparator; // The node ID to path name mapping (which acts like a cache) protected final Map<String, String> pathMap = new HashMap<String, String>(); // The path name map protected final MatchMap matchMap = new MatchMap(); // Acls protected final Set<String> aclMap = new HashSet<String>(); protected final boolean securityOn; // Filter string protected final String filterString; protected final Set<String> holder = new HashSet<String>(); protected final boolean includeAllMetadata; /** Constructor */ public SystemMetadataDescription(LivelinkContext llc, Specification spec) throws ManifoldCFException, ServiceInterruption { this.llc = llc; String pathAttributeName = null; String pathSeparator = null; boolean securityOn = true; StringBuilder fsb = new StringBuilder(); boolean first = true; boolean includeAllMetadata = false; for (int i = 0; i < spec.getChildCount(); i++) { SpecificationNode n = spec.getChild(i); if (n.getType().equals("pathnameattribute")) { pathAttributeName = n.getAttributeValue("value"); pathSeparator = n.getAttributeValue("separator"); if (pathSeparator == null) pathSeparator = "/"; } else if (n.getType().equals("pathmap")) { String pathMatch = n.getAttributeValue("match"); String pathReplace = n.getAttributeValue("replace"); matchMap.appendMatchPair(pathMatch, pathReplace); } else if (n.getType().equals("access")) { String token = n.getAttributeValue("token"); aclMap.add(token); } else if (n.getType().equals("security")) { String value = n.getAttributeValue("value"); if (value.equals("on")) securityOn = true; else if (value.equals("off")) securityOn = false; } else if (n.getType().equals("include")) { String includeMatch = n.getAttributeValue("filespec"); if (includeMatch != null) { // Peel off the extension int index = includeMatch.lastIndexOf("."); if (index != -1) { String type = includeMatch.substring(index + 1).toLowerCase().replace('*', '%'); if (first) first = false; else fsb.append(" or "); fsb.append("lower(FileType) like '").append(type).append("'"); } } } else if (n.getType().equals("allmetadata")) { String isAll = n.getAttributeValue("all"); if (isAll != null && isAll.equals("true")) includeAllMetadata = true; } else if (n.getType().equals("metadata")) { String category = n.getAttributeValue("category"); String attributeName = n.getAttributeValue("attribute"); String isAll = n.getAttributeValue("all"); if (isAll != null && isAll.equals("true")) { // Locate all metadata items for the specified category path, // and enter them into the array getSession(); String[] attrs = getCategoryAttributes(llc, category); if (attrs != null) { int j = 0; while (j < attrs.length) { attributeName = attrs[j++]; String metadataName = packCategoryAttribute(category, attributeName); holder.add(metadataName); } } } else { String metadataName = packCategoryAttribute(category, attributeName); holder.add(metadataName); } } } this.includeAllMetadata = includeAllMetadata; this.pathAttributeName = pathAttributeName; this.pathSeparator = pathSeparator; this.securityOn = securityOn; String filterStringPiece = fsb.toString(); if (filterStringPiece.length() == 0) this.filterString = "0>1"; else { StringBuilder sb = new StringBuilder(); sb.append("SubType=").append(new Integer(LAPI_DOCUMENTS.FOLDERSUBTYPE).toString()); sb.append(" or SubType=").append(new Integer(LAPI_DOCUMENTS.COMPOUNDDOCUMENTSUBTYPE).toString()); sb.append(" or SubType=").append(new Integer(LAPI_DOCUMENTS.PROJECTSUBTYPE).toString()); sb.append(" or (SubType=").append(new Integer(LAPI_DOCUMENTS.DOCUMENTSUBTYPE).toString()); sb.append(" and ("); // Walk through the document spec to find the documents that match under the specified root // include lower(column)=spec sb.append(filterStringPiece); sb.append("))"); this.filterString = sb.toString(); } } public boolean includeAllMetadata() { return includeAllMetadata; } public String[] getMetadataAttributes() { // Put into an array String[] specifiedMetadataAttributes = new String[holder.size()]; int i = 0; for (String attrName : holder) { specifiedMetadataAttributes[i++] = attrName; } return specifiedMetadataAttributes; } public String getFilterString() { return filterString; } public String[] getAcls() { if (!securityOn) return null; String[] rval = new String[aclMap.size()]; int i = 0; for (String token : aclMap) { rval[i++] = token; } return rval; } /** Get the path attribute name. *@return the path attribute name, or null if none specified. */ public String getPathAttributeName() { return pathAttributeName; } /** Get the path separator. */ public String getPathSeparator() { return pathSeparator; } /** Given an identifier, get the translated string that goes into the metadata. */ public String getPathAttributeValue(String documentIdentifier) throws ManifoldCFException, ServiceInterruption { String path = getNodePathString(documentIdentifier); if (path == null) return null; return matchMap.translate(path); } /** Get the matchmap string. */ public String getMatchMapString() { return matchMap.toString(); } /** For a given node, get its path. */ public String getNodePathString(String documentIdentifier) throws ManifoldCFException, ServiceInterruption { if (Logging.connectors.isDebugEnabled()) Logging.connectors.debug("Looking up path for '" + documentIdentifier + "'"); String path = pathMap.get(documentIdentifier); if (path == null) { // Not yet present. Look it up, recursively String identifierPart = documentIdentifier; // Get the current node's name first // D = Document; anything else = Folder if (identifierPart.startsWith("D") || identifierPart.startsWith("F")) { // Strip off the letter identifierPart = identifierPart.substring(1); } // See if there's a volume label; if not, use the default. int colonPosition = identifierPart.indexOf(":"); int volumeID; int objectID; try { if (colonPosition == -1) { // Default volume ID volumeID = LLENTWK_VOL; objectID = Integer.parseInt(identifierPart); } else { volumeID = Integer.parseInt(identifierPart.substring(0, colonPosition)); objectID = Integer.parseInt(identifierPart.substring(colonPosition + 1)); } } catch (NumberFormatException e) { throw new ManifoldCFException("Bad document identifier: " + e.getMessage(), e); } ObjectInformation objInfo = llc.getObjectInformation(volumeID, objectID); if (!objInfo.exists()) { // The document identifier describes a path that does not exist. // This is unexpected, but don't die: just log a warning and allow the higher level to deal with it. Logging.connectors.warn("Livelink: Bad document identifier: '" + documentIdentifier + "' apparently does not exist, but need to find its path"); return null; } // Get the name attribute String name = objInfo.getName(); // Get the parentID attribute int parentID = objInfo.getParentId().intValue(); if (parentID == -1) path = name; else { String parentIdentifier = "F" + Integer.toString(volumeID) + ":" + Integer.toString(parentID); String parentPath = getNodePathString(parentIdentifier); if (parentPath == null) return null; path = parentPath + pathSeparator + name; } pathMap.put(documentIdentifier, path); } return path; } } /** Class that manages to find catid's and attribute names that have been specified. * This accepts a part of the version string which contains the string-ified metadata * spec, rather than pulling it out of the document specification. That guarantees that * the version string actually corresponds to the document that was ingested. */ protected class MetadataDescription { protected final LivelinkContext llc; // This is a map of category name to category ID and attributes protected final Map<String, MetadataPathItem> categoryMap = new HashMap<String, MetadataPathItem>(); /** Constructor. */ public MetadataDescription(LivelinkContext llc) { this.llc = llc; } /** Iterate over the metadata items represented by the specified chunk of version string. *@return an iterator over MetadataItem objects. */ public Iterator<MetadataItem> getItems(String[] metadataItems) throws ManifoldCFException, ServiceInterruption { // This is the map that will be iterated over for a return value. // It gets built out of (hopefully cached) data from categoryMap. Map<String, MetadataItem> newMap = new HashMap<String, MetadataItem>(); // Start at root ObjectInformation rootValue = null; // Walk through string and process each metadata element in turn. for (String metadataSpec : metadataItems) { StringBuilder categoryBuffer = new StringBuilder(); StringBuilder attributeBuffer = new StringBuilder(); unpackCategoryAttribute(categoryBuffer, attributeBuffer, metadataSpec); String category = categoryBuffer.toString(); String attributeName = attributeBuffer.toString(); // If there's already an entry for this category in the return map, use it MetadataItem mi = newMap.get(category); if (mi == null) { // Now, look up the node information // Convert category to cat id. MetadataPathItem item = categoryMap.get(category); if (item == null) { RootValue rv = new RootValue(llc, category); if (rootValue == null) { rootValue = rv.getRootValue(); } // Get the object id of the category the path describes. // NOTE: We don't use the RootValue version of getCategoryId because // we want to use the cached value of rootValue, if it was around. int catObjectID = rootValue.getCategoryId(rv.getRemainderPath()); if (catObjectID != -1) { item = new MetadataPathItem(catObjectID, rv.getRemainderPath()); categoryMap.put(category, item); } } mi = new MetadataItem(item); newMap.put(category, mi); } // Add attribute name to category mi.addAttribute(attributeName); } return newMap.values().iterator(); } } /** This class caches the category path strings associated with a given category object identifier. * The goal is to allow reasonably speedy lookup of the path name, so we can put it into the metadata part of the * version string. */ protected class CategoryPathAccumulator { // Livelink context protected final LivelinkContext llc; // This is the map from category ID to category path name. // It's keyed by an Integer formed from the id, and has String values. protected final Map<Integer, String> categoryPathMap = new HashMap<Integer, String>(); // This is the map from category ID to attribute names. Keyed // by an Integer formed from the id, and has a String[] value. protected final Map<Integer, String[]> attributeMap = new HashMap<Integer, String[]>(); /** Constructor */ public CategoryPathAccumulator(LivelinkContext llc) { this.llc = llc; } /** Get a specified set of packed category paths with attribute names, given the category identifiers */ public String[] getCategoryPathsAttributeNames(int[] catIDs) throws ManifoldCFException, ServiceInterruption { Set<String> set = new HashSet<String>(); for (int x : catIDs) { Integer key = new Integer(x); String pathValue = categoryPathMap.get(key); if (pathValue == null) { // Chase the path back up the chain pathValue = findPath(key.intValue()); if (pathValue == null) continue; categoryPathMap.put(key, pathValue); } String[] attributeNames = attributeMap.get(key); if (attributeNames == null) { // Get the attributes for this category attributeNames = findAttributes(key.intValue()); if (attributeNames == null) continue; attributeMap.put(key, attributeNames); } // Now, put the path and the attributes into the hash. for (String attributeName : attributeNames) { String metadataName = packCategoryAttribute(pathValue, attributeName); set.add(metadataName); } } String[] rval = new String[set.size()]; int i = 0; for (String value : set) { rval[i++] = value; } return rval; } /** Find a category path given a category ID */ protected String findPath(int catID) throws ManifoldCFException, ServiceInterruption { return getObjectPath(llc.getObjectInformation(0, catID)); } /** Get the complete path for an object. */ protected String getObjectPath(ObjectInformation currentObject) throws ManifoldCFException, ServiceInterruption { String path = null; while (true) { if (currentObject.isCategoryWorkspace()) return CATEGORY_NAME + ((path == null) ? "" : ":" + path); else if (currentObject.isEntityWorkspace()) return ENTWKSPACE_NAME + ((path == null) ? "" : ":" + path); if (!currentObject.exists()) { // The document identifier describes a path that does not exist. // This is unexpected, but an exception would terminate the job, and we don't want that. Logging.connectors.warn("Livelink: Bad identifier found? " + currentObject.toString() + " apparently does not exist, but need to look up its path"); return null; } // Get the name attribute String name = currentObject.getName(); if (path == null) path = name; else path = name + "/" + path; // Get the parentID attribute int parentID = currentObject.getParentId().intValue(); if (parentID == -1) { // Oops, hit the top of the path without finding the workspace we're in. // No idea where it lives; note this condition and exit. Logging.connectors.warn("Livelink: Object ID " + currentObject.toString() + " doesn't seem to live in enterprise or category workspace! Path I got was '" + path + "'"); return null; } currentObject = llc.getObjectInformation(0, parentID); } } /** Find a set of attributes given a category ID */ protected String[] findAttributes(int catID) throws ManifoldCFException, ServiceInterruption { return getCategoryAttributes(catID); } } /** Class representing a root value object, plus remainder string. * This class peels off the workspace name prefix from a path string or * attribute string, and finds the right workspace root node and remainder * path. */ protected class RootValue { protected final LivelinkContext llc; protected final String workspaceName; protected ObjectInformation rootValue = null; protected final String remainderPath; /** Constructor. *@param pathString is the path string. */ public RootValue(LivelinkContext llc, String pathString) { this.llc = llc; int colonPos = pathString.indexOf(":"); if (colonPos == -1) { remainderPath = pathString; workspaceName = ENTWKSPACE_NAME; } else { workspaceName = pathString.substring(0, colonPos); remainderPath = pathString.substring(colonPos + 1); } } /** Get the path string. *@return the path string (without the workspace name prefix). */ public String getRemainderPath() { return remainderPath; } /** Get the root node. *@return the root node. */ public ObjectInformation getRootValue() throws ManifoldCFException, ServiceInterruption { if (rootValue == null) { if (workspaceName.equals(CATEGORY_NAME)) rootValue = llc.getObjectInformation(LLCATWK_VOL, LLCATWK_ID); else if (workspaceName.equals(ENTWKSPACE_NAME)) rootValue = llc.getObjectInformation(LLENTWK_VOL, LLENTWK_ID); else throw new ManifoldCFException("Bad workspace name: " + workspaceName); } if (!rootValue.exists()) { Logging.connectors.warn("Livelink: Could not get workspace/volume ID! Retrying..."); // This cannot mean a real failure; it MUST mean that we have had an intermittent communication hiccup. So, pass it off as a service interruption. throw new ServiceInterruption("Service interruption getting root value", new ManifoldCFException("Could not get workspace/volume id"), System.currentTimeMillis() + 60000L, System.currentTimeMillis() + 600000L, -1, true); } return rootValue; } } // Here's an interesting note. All of the LAPI exceptions are subclassed off of RuntimeException. This makes life // hell because there is no superclass exception to capture, and even tweaky server communication issues wind up throwing // uncaught RuntimeException's up the stack. // // To fix this rather bad design, all places that invoke LAPI need to catch RuntimeException and run it through the following // method for interpretation and logging. // /** Interpret runtimeexception to search for livelink API errors. Throws an appropriately reinterpreted exception, or * just returns if the exception indicates that a short-cycle retry attempt should be made. (In that case, the appropriate * wait has been already performed). *@param e is the RuntimeException caught *@param failIfTimeout is true if, for transient conditions, we want to signal failure if the timeout condition is acheived. */ protected int handleLivelinkRuntimeException(RuntimeException e, int sanityRetryCount, boolean failIfTimeout) throws ManifoldCFException, ServiceInterruption { if (e instanceof com.opentext.api.LLHTTPAccessDeniedException || e instanceof com.opentext.api.LLHTTPClientException || e instanceof com.opentext.api.LLHTTPServerException || e instanceof com.opentext.api.LLIndexOutOfBoundsException || e instanceof com.opentext.api.LLNoFieldSpecifiedException || e instanceof com.opentext.api.LLNoValueSpecifiedException || e instanceof com.opentext.api.LLSecurityProviderException || e instanceof com.opentext.api.LLUnknownFieldException || e instanceof NumberFormatException || e instanceof ArrayIndexOutOfBoundsException) { String details = llServer.getErrors(); long currentTime = System.currentTimeMillis(); throw new ServiceInterruption( "Livelink API error: " + e.getMessage() + ((details == null) ? "" : "; " + details), e, currentTime + 5 * 60000L, currentTime + 12 * 60 * 60000L, -1, failIfTimeout); } else if (e instanceof com.opentext.api.LLBadServerCertificateException || e instanceof com.opentext.api.LLHTTPCGINotFoundException || e instanceof com.opentext.api.LLCouldNotConnectHTTPException || e instanceof com.opentext.api.LLHTTPForbiddenException || e instanceof com.opentext.api.LLHTTPProxyAuthRequiredException || e instanceof com.opentext.api.LLHTTPRedirectionException || e instanceof com.opentext.api.LLUnsupportedAuthMethodException || e instanceof com.opentext.api.LLWebAuthInitException) { String details = llServer.getErrors(); throw new ManifoldCFException( "Livelink API error: " + e.getMessage() + ((details == null) ? "" : "; " + details), e); } else if (e instanceof com.opentext.api.LLSSLNotAvailableException) { String details = llServer.getErrors(); throw new ManifoldCFException( "Missing llssl.jar error: " + e.getMessage() + ((details == null) ? "" : "; " + details), e); } else if (e instanceof com.opentext.api.LLIllegalOperationException) { // This usually means that LAPI has had a minor communication difficulty but hasn't reported it accurately. // We *could* throw a ServiceInterruption, but OpenText recommends to just retry almost immediately. String details = llServer.getErrors(); return assessRetry(sanityRetryCount, new ManifoldCFException("Livelink API illegal operation error: " + e.getMessage() + ((details == null) ? "" : "; " + details), e)); } else if (e instanceof com.opentext.api.LLIOException || (e instanceof RuntimeException && e.getClass().getName().startsWith("com.opentext.api."))) { // Catching obfuscated and unspecified opentext runtime exceptions now too - these come from llssl.jar. We // have to presume these are SSL connection errors; nothing else to go by unfortunately. UGH. // Treat this as a transient error; try again in 5 minutes, and only fail after 12 hours of trying // LAPI is returning errors that are not terribly explicit, and I don't have control over their wording, so check that server can be resolved by DNS, // so that a better error message can be returned. try { InetAddress.getByName(serverName); } catch (UnknownHostException e2) { throw new ManifoldCFException("Server name '" + serverName + "' cannot be resolved", e2); } long currentTime = System.currentTimeMillis(); throw new ServiceInterruption(e.getMessage(), e, currentTime + 5 * 60000L, currentTime + 12 * 60 * 60000L, -1, failIfTimeout); } else throw e; } /** Do a retry, or throw an exception if the retry count has been exhausted */ protected static int assessRetry(int sanityRetryCount, ManifoldCFException e) throws ManifoldCFException { if (sanityRetryCount == 0) { throw e; } sanityRetryCount--; try { ManifoldCF.sleep(1000L); } catch (InterruptedException e2) { throw new ManifoldCFException(e2.getMessage(), e2, ManifoldCFException.INTERRUPTED); } // Exit the method return sanityRetryCount; } /** This thread performs a LAPI FetchVersion command, streaming the resulting * document back through a XThreadInputStream to the invoking thread. */ protected class DocumentReadingThread extends Thread { protected Throwable exception = null; protected final int volumeID; protected final int docID; protected final int versionNumber; protected final XThreadInputStream stream; public DocumentReadingThread(int volumeID, int docID, int versionNumber) { super(); this.volumeID = volumeID; this.docID = docID; this.versionNumber = versionNumber; this.stream = new XThreadInputStream(); setDaemon(true); } @Override public void run() { try { XThreadOutputStream outputStream = new XThreadOutputStream(stream); try { int status = LLDocs.FetchVersion(volumeID, docID, versionNumber, outputStream); if (status != 0) { throw new ManifoldCFException( "Error retrieving contents of document " + Integer.toString(volumeID) + ":" + Integer.toString(docID) + " revision " + versionNumber + " : Status=" + Integer.toString(status) + " (" + llServer.getErrors() + ")"); } } finally { outputStream.close(); } } catch (Throwable e) { this.exception = e; } } public InputStream getSafeInputStream() { return stream; } public void finishUp() throws InterruptedException, ManifoldCFException { // This will be called during the finally // block in the case where all is well (and // the stream completed) and in the case where // there were exceptions. stream.abort(); join(); Throwable thr = exception; if (thr != null) { if (thr instanceof ManifoldCFException) throw (ManifoldCFException) thr; else if (thr instanceof RuntimeException) throw (RuntimeException) thr; else if (thr instanceof Error) throw (Error) thr; else throw new RuntimeException("Unhandled exception of type: " + thr.getClass().getName(), thr); } } } /** This thread does the actual socket communication with the server. * It's set up so that it can be abandoned at shutdown time. * * The way it works is as follows: * - it starts the transaction * - it receives the response, and saves that for the calling class to inspect * - it transfers the data part to an input stream provided to the calling class * - it shuts the connection down * * If there is an error, the sequence is aborted, and an exception is recorded * for the calling class to examine. * * The calling class basically accepts the sequence above. It starts the * thread, and tries to get a response code. If instead an exception is seen, * the exception is thrown up the stack. */ protected static class ExecuteMethodThread extends Thread { /** Client and method, all preconfigured */ protected final HttpClient httpClient; protected final HttpRequestBase executeMethod; protected HttpResponse response = null; protected Throwable responseException = null; protected XThreadInputStream threadStream = null; protected InputStream bodyStream = null; protected boolean streamCreated = false; protected Throwable streamException = null; protected boolean abortThread = false; protected Throwable shutdownException = null; protected Throwable generalException = null; public ExecuteMethodThread(HttpClient httpClient, HttpRequestBase executeMethod) { super(); setDaemon(true); this.httpClient = httpClient; this.executeMethod = executeMethod; } public void run() { try { try { // Call the execute method appropriately synchronized (this) { if (!abortThread) { try { response = httpClient.execute(executeMethod); } catch (java.net.SocketTimeoutException e) { responseException = e; } catch (ConnectTimeoutException e) { responseException = e; } catch (InterruptedIOException e) { throw e; } catch (Throwable e) { responseException = e; } this.notifyAll(); } } // Start the transfer of the content if (responseException == null) { synchronized (this) { if (!abortThread) { try { bodyStream = response.getEntity().getContent(); if (bodyStream != null) { threadStream = new XThreadInputStream(bodyStream); } streamCreated = true; } catch (java.net.SocketTimeoutException e) { streamException = e; } catch (ConnectTimeoutException e) { streamException = e; } catch (InterruptedIOException e) { throw e; } catch (Throwable e) { streamException = e; } this.notifyAll(); } } } if (responseException == null && streamException == null) { if (threadStream != null) { // Stuff the content until we are done threadStream.stuffQueue(); } } } finally { if (bodyStream != null) { try { bodyStream.close(); } catch (IOException e) { } bodyStream = null; } synchronized (this) { try { executeMethod.abort(); } catch (Throwable e) { shutdownException = e; } this.notifyAll(); } } } catch (Throwable e) { // We catch exceptions here that should ONLY be InterruptedExceptions, as a result of the thread being aborted. this.generalException = e; } } public int getResponseCode() throws InterruptedException, IOException, HttpException { // Must wait until the response object is there while (true) { synchronized (this) { checkException(responseException); if (response != null) return response.getStatusLine().getStatusCode(); wait(); } } } public long getResponseContentLength() throws InterruptedException, IOException, HttpException { String contentLength = getFirstHeader("Content-Length"); if (contentLength == null || contentLength.length() == 0) return -1L; return new Long(contentLength.trim()).longValue(); } public String getFirstHeader(String headerName) throws InterruptedException, IOException, HttpException { // Must wait for the response object to appear while (true) { synchronized (this) { checkException(responseException); if (response != null) { Header h = response.getFirstHeader(headerName); if (h == null) return null; return h.getValue(); } wait(); } } } public InputStream getSafeInputStream() throws InterruptedException, IOException, HttpException { // Must wait until stream is created, or until we note an exception was thrown. while (true) { synchronized (this) { if (responseException != null) throw new IllegalStateException("Check for response before getting stream"); checkException(streamException); if (streamCreated) return threadStream; wait(); } } } public void abort() { // This will be called during the finally // block in the case where all is well (and // the stream completed) and in the case where // there were exceptions. synchronized (this) { if (streamCreated) { if (threadStream != null) threadStream.abort(); } abortThread = true; } } public void finishUp() throws InterruptedException { join(); } protected synchronized void checkException(Throwable exception) throws IOException, HttpException { if (exception != null) { // Throw the current exception, but clear it, so no further throwing is possible on the same problem. Throwable e = exception; if (e instanceof IOException) throw (IOException) e; else if (e instanceof HttpException) throw (HttpException) e; else if (e instanceof RuntimeException) throw (RuntimeException) e; else if (e instanceof Error) throw (Error) e; else throw new RuntimeException("Unhandled exception of type: " + e.getClass().getName(), e); } } } }