0001: /*
0002: * Copyright 2007 Hippo.
0003: *
0004: * Licensed under the Apache License, Version 2.0 (the "License");
0005: * you may not use this file except in compliance with the License.
0006: * You may obtain a copy of the License at
0007: *
0008: * http://www.apache.org/licenses/LICENSE-2.0
0009: *
0010: * Unless required by applicable law or agreed to in writing, software
0011: * distributed under the License is distributed on an "AS IS" BASIS,
0012: * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
0013: * See the License for the specific language governing permissions and
0014: * limitations under the License.
0015: */
0016: package nl.hippo.cms.searchandreplace;
0017:
0018: import java.io.BufferedInputStream;
0019: import java.io.ByteArrayInputStream;
0020: import java.io.ByteArrayOutputStream;
0021: import java.io.IOException;
0022: import java.io.InputStream;
0023: import java.util.Enumeration;
0024: import java.util.HashMap;
0025: import java.util.Iterator;
0026: import java.util.List;
0027: import java.util.Locale;
0028: import java.util.Map;
0029: import java.util.Vector;
0030: import nl.hippo.cms.searchandreplace.log.SearchAndReplaceLog;
0031: import nl.hippo.cms.searchandreplace.util.MethodCleanup;
0032: import nl.hippo.cms.searchandreplace.util.StreamCleanup;
0033: import org.apache.commons.httpclient.HttpClient;
0034: import org.apache.commons.httpclient.HttpState;
0035: import org.apache.commons.httpclient.HttpStatus;
0036: import org.apache.commons.httpclient.MultiThreadedHttpConnectionManager;
0037: import org.apache.commons.httpclient.methods.GetMethod;
0038: import org.apache.commons.httpclient.methods.PutMethod;
0039: import org.apache.webdav.lib.Property;
0040: import org.apache.webdav.lib.PropertyName;
0041: import org.apache.webdav.lib.methods.PropFindMethod;
0042: import org.jdom.Attribute;
0043: import org.jdom.Document;
0044: import org.jdom.JDOMException;
0045: import org.jdom.Text;
0046: import org.jdom.input.SAXBuilder;
0047: import org.jdom.output.Format;
0048: import org.jdom.output.XMLOutputter;
0049: import org.jdom.xpath.XPath;
0050: import org.w3c.dom.Element;
0051: import org.w3c.dom.NodeList;
0052:
0053: /**
0054: * <p>
0055: * This class replaces text in text, CDATA and attribute nodes of a set of
0056: * documents. It only supports plain searches (i.e. no regular expressions,
0057: * wildcards, etc.), and can search case-sensitivie and insensitive.
0058: * </p>
0059: */
0060: public class SearchAndReplace implements Runnable {
0061: /**
0062: * <p>
0063: * The key of the HTTP status code in the map containing the document
0064: * information needed to make the decision of the document can be
0065: * processed.
0066: * </p>
0067: */
0068: private static final String HTTP_STATUS_CODE_KEY = "httpStatusCode";
0069:
0070: /**
0071: * <p>
0072: * The key of the current user privilege set in the map containing the
0073: * document information needed to make the decision of the document can
0074: * be processed.
0075: * </p>
0076: */
0077: private static final String CURRENT_USER_PRIVILEGE_SET_KEY = "currentUserPrivilegeSet";
0078:
0079: /**
0080: * <p>
0081: * The key of the lock discovery in the map containing the document
0082: * information needed to make the decision of the document can be
0083: * processed.
0084: * </p>
0085: */
0086: private static final String LOCK_DISCOVERY_KEY = "lockDiscovery";
0087:
0088: /**
0089: * <p>
0090: * The namespace for WebDAV. It is used to for resource properties and
0091: * tags in requests and responses.
0092: * </p>
0093: */
0094: private static final String DAV_NAMESPACE = "DAV:";
0095:
0096: /**
0097: * <p>
0098: * The (local) name of the property containing the current user
0099: * privilege set.
0100: * </p>
0101: */
0102: private static final String CURRENT_USER_PRIVILEGE_SET_PROPERTY_LOCAL_NAME = "current-user-privilege-set";
0103:
0104: /**
0105: * <p>
0106: * The property name (the combination of the namespace URI and the local
0107: * name) of the property containing the current user privilege set.
0108: * </p>
0109: */
0110: private static final PropertyName CURRENT_USER_PRIVILEGE_SET_PROPERTY_NAME = new PropertyName(
0111: DAV_NAMESPACE,
0112: CURRENT_USER_PRIVILEGE_SET_PROPERTY_LOCAL_NAME);
0113:
0114: /**
0115: * <p>
0116: * The (local) name of the property containing the lock discovery.
0117: * </p>
0118: */
0119: private static final String LOCK_DISCOVERY_PROPERTY_LOCAL_NAME = "lockdiscovery";
0120:
0121: /**
0122: * <p>
0123: * The property name (the combination of the namespace URI and the local
0124: * name) of the property containing the lock discovery.
0125: * </p>
0126: */
0127: private static final PropertyName LOCK_DISCOVERY_PROPERTY_NAME = new PropertyName(
0128: DAV_NAMESPACE, LOCK_DISCOVERY_PROPERTY_LOCAL_NAME);
0129:
0130: /**
0131: * <p>
0132: * The collection of property names of the properties that are needed
0133: * for making the decision if the document can be processed.
0134: * </p>
0135: */
0136: private static final Vector PROPERTY_NAMES_NEEDED_FOR_PROCESSABLE_DECISION = new Vector();
0137:
0138: static {
0139: PROPERTY_NAMES_NEEDED_FOR_PROCESSABLE_DECISION
0140: .add(CURRENT_USER_PRIVILEGE_SET_PROPERTY_NAME);
0141: PROPERTY_NAMES_NEEDED_FOR_PROCESSABLE_DECISION
0142: .add(LOCK_DISCOVERY_PROPERTY_NAME);
0143: }
0144:
0145: /**
0146: * <p>
0147: * The (local) name of the tag representing the <code>DAV:write</code>
0148: * privilege.
0149: * </p>
0150: */
0151: private static final String WRITE_LOCAL_NAME = "write";
0152:
0153: /**
0154: * <p>
0155: * The (local) name of the tag indicating that a lock is active on the
0156: * document.
0157: * </p>
0158: */
0159: private static final String ACTIVE_LOCK_LOCAL_NAME = "activelock";
0160:
0161: /**
0162: * <p>
0163: * The value returned by {@link String#indexOf(String, int)} (and
0164: * similar functions) if the text cannot be found.
0165: * </p>
0166: */
0167: private static final int STRING_NOT_FOUND_INDEX = -1;
0168:
0169: /**
0170: * <p>
0171: * The configuration containing the information and services needed by
0172: * this class.
0173: * </p>
0174: */
0175: private SearchAndReplaceConfiguration m_configuration;
0176:
0177: /**
0178: * <p>
0179: * The text that should be replaced. If the search is case-insensitive
0180: * this text is the lower case version of the text in the configuration.
0181: * </p>
0182: */
0183: private String m_textToSearchFor;
0184:
0185: /**
0186: * <p>
0187: * The HTTP client that will be used to communicate with the WebDAV
0188: * repository.
0189: * </p>
0190: */
0191: private HttpClient m_httpClient;
0192:
0193: /**
0194: * <p>
0195: * Create an instance of this class passing it the information and
0196: * services it needs.
0197: * </p>
0198: *
0199: * <p>
0200: * The replacement will not be started by the constructor. To start
0201: * replacement invoke {@link #run()}.
0202: * </p>
0203: *
0204: * @param configuration
0205: * the configuration containing the information and
0206: * services needed for replacement.
0207: */
0208: public SearchAndReplace(SearchAndReplaceConfiguration configuration) {
0209: super ();
0210:
0211: assertConfigurationIsValid(configuration);
0212:
0213: m_configuration = configuration;
0214:
0215: determineTextToSearchFor();
0216: createHttpClient();
0217: }
0218:
0219: /**
0220: * <p>
0221: * Start the replacement. The listener (in the configuration) is
0222: * notified of the progress of the replacement.
0223: * </p>
0224: */
0225: public void run() {
0226: try {
0227: Iterator documentUrlsIterator = documentUrlsIterator();
0228: /*
0229: * Continue processing while not all documents have been
0230: * processed and nobody requested this thread to stop.
0231: */
0232: while (documentUrlsIterator.hasNext()
0233: && !Thread.currentThread().isInterrupted()) {
0234: String documentUrl = (String) documentUrlsIterator
0235: .next();
0236:
0237: getListener().startProcessingOfDocument(documentUrl);
0238:
0239: try {
0240: processDocument(documentUrl);
0241: } catch (IOException e) {
0242: notifyListenerOfProblemAndLogWarning(
0243: documentUrl,
0244: SearchAndReplaceErrorCodes.IO_EXCEPTION_CODE,
0245: e,
0246: "I/O error occurred while replacing in document: ");
0247: } catch (JDOMException e) {
0248: notifyListenerOfProblemAndLogWarning(
0249: documentUrl,
0250: SearchAndReplaceErrorCodes.JDOM_EXCEPTION_CODE,
0251: e,
0252: "JDOM error occurred while replacing in document: ");
0253: } catch (UnableToReadException e) {
0254: notifyListenerOfProblemAndLogWarning(
0255: documentUrl,
0256: SearchAndReplaceErrorCodes.UNABLE_TO_READ_CODE,
0257: e, "Could not read the document: ");
0258: } catch (UnableToWriteException e) {
0259: notifyListenerOfProblemAndLogWarning(
0260: documentUrl,
0261: SearchAndReplaceErrorCodes.UNABLE_TO_WRITE_CODE,
0262: e, "Could not write the document: ");
0263: }
0264: }
0265: } finally {
0266: /*
0267: * Make sure that the event handler is notified no matter what
0268: * happens, because this event might be used to stop listening
0269: * by the client.
0270: */
0271: getListener().replacementFinished();
0272: }
0273:
0274: }
0275:
0276: /**
0277: * <p>
0278: * Process a document. Determine if replacement can take place for the
0279: * document. If so, replace text in the document. Otherwise, notify the
0280: * listener of the problem.
0281: * </p>
0282: *
0283: * @param documentUrl
0284: * the relative URL of the document to process.
0285: * @throws IOException
0286: * if an I/O error occurs.
0287: * @throws JDOMException
0288: * if there is a problem parsing or processing the XML.
0289: * @throws UnableToReadException
0290: * if the document cannot be read from the repository.
0291: * @throws UnableToWriteException
0292: * if the document cannot be written to the repository.
0293: */
0294: private void processDocument(String documentUrl)
0295: throws IOException, JDOMException, UnableToReadException,
0296: UnableToWriteException {
0297: Map documentInformation = retrieveDocumentInformationNeededForProcessableDecision(documentUrl);
0298: if (canDocumentBeProcessed(documentInformation)) {
0299: replaceInDocumentAndReportSuccess(documentUrl);
0300: } else {
0301: reportProblem(documentUrl, documentInformation);
0302: }
0303: }
0304:
0305: /**
0306: * <p>
0307: * Replace text in a document. The text to replace, case-sensitivity,
0308: * where to replace (scope) and the replacement text are specified in
0309: * the configuration. After replacing notify the listener of successful
0310: * replacement.
0311: * </p>
0312: *
0313: * @param documentUrl
0314: * the relative URL of the document in which to replace.
0315: * @throws IOException
0316: * if an I/O error occurs.
0317: * @throws JDOMException
0318: * if there is a problem parsing or processing the XML.
0319: * @throws UnableToReadException
0320: * if the document cannot be read from the repository.
0321: * @throws UnableToWriteException
0322: * if the document cannot be written to the repository.
0323: */
0324: private void replaceInDocumentAndReportSuccess(String documentUrl)
0325: throws IOException, JDOMException, UnableToReadException,
0326: UnableToWriteException {
0327: int numberOfReplacements = replaceInDocument(documentUrl);
0328: getListener().successfulProcessingOfDocument(documentUrl,
0329: numberOfReplacements);
0330: }
0331:
0332: /**
0333: * <p>
0334: * Determine why replacement cannot take place for a document and notify
0335: * the listener of the problem.
0336: * </p>
0337: *
0338: * @param documentUrl
0339: * the relative URL of the document which could not be
0340: * processed.
0341: * @param documentInformation
0342: * the information from which to determine the reason for
0343: * the problem.
0344: */
0345: private void reportProblem(String documentUrl,
0346: Map documentInformation) {
0347: int errorCode = determineErrorCode(documentInformation);
0348: getListener().unsuccessfulProcessingOfDocument(documentUrl,
0349: errorCode);
0350: }
0351:
0352: /**
0353: * <p>
0354: * Replace text in a document. The text to replace, case-sensitivity,
0355: * where to replace (scope) and the replacement text are specified in
0356: * the configuration.
0357: * </p>
0358: *
0359: * @param documentUrl
0360: * the relative URL of the document in which to replace.
0361: * @return the number of replacements made in the document.
0362: * @throws IOException
0363: * if an I/O error occurs.
0364: * @throws JDOMException
0365: * if there is a problem parsing or processing the XML.
0366: * @throws UnableToReadException
0367: * if the document cannot be read from the repository.
0368: * @throws UnableToWriteException
0369: * if the document cannot be written to the repository.
0370: */
0371: private int replaceInDocument(String documentUrl)
0372: throws IOException, JDOMException, UnableToReadException,
0373: UnableToWriteException {
0374: int result;
0375:
0376: Document document = retrieveDocument(documentUrl);
0377:
0378: result = replaceInDocument(document);
0379:
0380: if (0 < result) {
0381: // STATE: the document has been changed
0382:
0383: storeDocument(documentUrl, document);
0384: }
0385:
0386: return result;
0387: }
0388:
0389: /**
0390: * <p>
0391: * Replace text in a document. The text to replace, case-sensitivity,
0392: * where to replace (scope) and the replacement text are specified in
0393: * the configuration.
0394: * </p>
0395: *
0396: * @param document
0397: * the document in which to replace.
0398: * @return the number of replacements made in the document.
0399: * @throws JDOMException
0400: * if there is a problem processing the XML.
0401: */
0402: private int replaceInDocument(Document document)
0403: throws JDOMException {
0404: int result = 0;
0405:
0406: List nodes = XPath.selectNodes(document, getScopeXpath());
0407: for (Iterator nodesIterator = nodes.iterator(); nodesIterator
0408: .hasNext();) {
0409: Object node = nodesIterator.next();
0410:
0411: result += replaceInNodeIfAllowed(node);
0412: }
0413:
0414: return result;
0415: }
0416:
0417: /**
0418: * <p>
0419: * Replace text in an XML node. Only text, CDATA and attribute nodes
0420: * will be processed. It is allowed to pass in other nodes but these
0421: * remain unchanged. The text to replace, case-sensitivity and the
0422: * replacement text are specified in the configuration.
0423: * </p>
0424: *
0425: * @param node
0426: * the node in which to replace.
0427: * @return the number of replacements made in the document.
0428: */
0429: private int replaceInNodeIfAllowed(Object node) {
0430: int result = 0;
0431:
0432: if (node instanceof Text) {
0433: Text text = (Text) node;
0434: result = replaceInText(text);
0435: } else if (node instanceof Attribute) {
0436: Attribute attribute = (Attribute) node;
0437: result = replaceInAttribute(attribute);
0438: }
0439:
0440: return result;
0441: }
0442:
0443: /**
0444: * <p>
0445: * Replace text in a text or CDATA node. The text to replace,
0446: * case-sensitivity and the replacement text are specified in the
0447: * configuration.
0448: * </p>
0449: *
0450: * @param text
0451: * the text or CDATA node in which to replace.
0452: * @return the number of replacements made in the document.
0453: */
0454: private int replaceInText(Text text) {
0455: int result;
0456:
0457: ReplacementResult replacementResult = replaceInString(text
0458: .getText());
0459: result = replacementResult.getNumberOfReplacements();
0460: text.setText(replacementResult.getResultString());
0461:
0462: return result;
0463: }
0464:
0465: /**
0466: * <p>
0467: * Replace text in an attribute node. The text to replace,
0468: * case-sensitivity and the replacement text are specified in the
0469: * configuration.
0470: * </p>
0471: *
0472: * @param attribute
0473: * the attribute node in which to replace.
0474: * @return the number of replacements made in the document.
0475: */
0476: private int replaceInAttribute(Attribute attribute) {
0477: int result;
0478:
0479: ReplacementResult replacementResult = replaceInString(attribute
0480: .getValue());
0481: result = replacementResult.getNumberOfReplacements();
0482: attribute.setValue(replacementResult.getResultString());
0483: return result;
0484: }
0485:
0486: /**
0487: * <p>
0488: * Replace text in a string. The actual text to replace in and the
0489: * actual search text that are used depend on the case-sensitivity
0490: * setting. The replacement text is specified in the configuration.
0491: * </p>
0492: *
0493: * @param string
0494: * the string in which to replace.
0495: * @return an object containing the string after replacement and the
0496: * number of replacements done.
0497: */
0498: private ReplacementResult replaceInString(String string) {
0499: ReplacementResult result;
0500:
0501: /*
0502: * Depending on the case-sensitivity of the search the string that
0503: * should be used to search in can differ from the string passed in.
0504: */
0505: String stringToSearchIn = getStringToSearchIn(string);
0506:
0507: int textToSearchForLength = m_textToSearchFor.length();
0508:
0509: StringBuffer resultString = new StringBuffer(string.length());
0510:
0511: int numberOfReplacements = 0;
0512:
0513: int indexToStartSearchingFrom = 0;
0514: int indexOfTextToReplace = stringToSearchIn.indexOf(
0515: m_textToSearchFor, indexToStartSearchingFrom);
0516: while (indexOfTextToReplace != STRING_NOT_FOUND_INDEX) {
0517: /*
0518: * Append the text that does not contain the text to replace.
0519: */
0520: resultString.append(string.substring(
0521: indexToStartSearchingFrom, indexOfTextToReplace));
0522: /*
0523: * Append the replacement text at the location of the text to
0524: * replace.
0525: */
0526: resultString.append(getReplacementText());
0527:
0528: numberOfReplacements += 1;
0529:
0530: indexToStartSearchingFrom = indexOfTextToReplace
0531: + textToSearchForLength;
0532: indexOfTextToReplace = stringToSearchIn.indexOf(
0533: m_textToSearchFor, indexToStartSearchingFrom);
0534: }
0535:
0536: /*
0537: * Append the remaining text that does not contain the text to replace.
0538: */
0539: resultString
0540: .append(string.substring(indexToStartSearchingFrom));
0541:
0542: result = new ReplacementResult(resultString.toString(),
0543: numberOfReplacements);
0544:
0545: return result;
0546: }
0547:
0548: /**
0549: * <p>
0550: * Get the string that should be used to search for the text to replace.
0551: * The result depends on the case-sensitivity setting.
0552: * </p>
0553: *
0554: * @param string
0555: * the original string to replace in.
0556: * @return the string to search in.
0557: */
0558: private String getStringToSearchIn(String string) {
0559: String result;
0560:
0561: if (isSearchCaseSensitive()) {
0562: result = string;
0563: } else {
0564: result = string.toLowerCase(getLocale());
0565: }
0566:
0567: return result;
0568: }
0569:
0570: /**
0571: * <p>
0572: * Retrieve the document identified by <code>documentUrl</code> as a
0573: * JDOM tree. The document URL is appeneded to the documents base URL
0574: * (which is specified in the configuration).
0575: * </p>
0576: *
0577: * @param documentUrl
0578: * the relative URL of the document.
0579: * @return a JDOM representation of the XML document.
0580: * @throws IOException
0581: * if an I/O error occurs.
0582: * @throws JDOMException
0583: * if there is a problem parsing the XML.
0584: * @throws UnableToReadException
0585: * if the document cannot be read from the repository.
0586: */
0587: private Document retrieveDocument(String documentUrl)
0588: throws IOException, JDOMException, UnableToReadException {
0589: Document result;
0590:
0591: String fullDocumentUrl = getDocumentsBaseUrl() + documentUrl;
0592: GetMethod get = new GetMethod(fullDocumentUrl);
0593: try {
0594: get.setDoAuthentication(true);
0595:
0596: int getResult = m_httpClient.executeMethod(get);
0597: if (getResult == HttpStatus.SC_OK) {
0598: result = readDocumentFromGet(get);
0599: } else {
0600: throw new UnableToReadException(
0601: "The HTTP status code of the GET was: "
0602: + getResult);
0603: }
0604: } finally {
0605: MethodCleanup.releaseConnection(get,
0606: "GET for document content", getLog());
0607: }
0608:
0609: return result;
0610: }
0611:
0612: /**
0613: * <p>
0614: * Read a document from the response of a <code>GET</code> method.
0615: * </p>
0616: *
0617: * @param get
0618: * the <code>GET</code> method to read the document
0619: * from.
0620: * @return the read document.
0621: * @throws IOException
0622: * if an I/O error occurs.
0623: * @throws JDOMException
0624: * if there is a problem parsing the XML.
0625: */
0626: private Document readDocumentFromGet(GetMethod get)
0627: throws IOException, JDOMException {
0628: Document result;
0629:
0630: InputStream responseInput = get.getResponseBodyAsStream();
0631: try {
0632: BufferedInputStream bufferedResponseInput = new BufferedInputStream(
0633: responseInput);
0634: try {
0635: SAXBuilder saxBuilder = new SAXBuilder(false);
0636: result = saxBuilder.build(bufferedResponseInput);
0637: } finally {
0638: StreamCleanup.close(bufferedResponseInput,
0639: "buffered document content", getLog());
0640: }
0641: } finally {
0642: StreamCleanup.close(responseInput, "document content",
0643: getLog());
0644: }
0645:
0646: return result;
0647: }
0648:
0649: /**
0650: * <p>
0651: * Store the document in the WebDAV repository using the UTF-8 character
0652: * encoding.
0653: * </p>
0654: *
0655: * @param documentUrl
0656: * the relative document URL.
0657: * @param document
0658: * the document to store.
0659: * @throws IOException
0660: * if an I/O error occurs.
0661: * @throws UnableToWriteException
0662: * if the document cannot be written to the repository.
0663: */
0664: private void storeDocument(String documentUrl, Document document)
0665: throws IOException, UnableToWriteException {
0666: byte[] documentAsBytes = writeDocumentToByteArray(document);
0667:
0668: String fullDocumentUrl = getDocumentsBaseUrl() + documentUrl;
0669: PutMethod put = new PutMethod(fullDocumentUrl);
0670: try {
0671: put.setDoAuthentication(true);
0672:
0673: ByteArrayInputStream byteArrayInput = new ByteArrayInputStream(
0674: documentAsBytes);
0675: try {
0676: put.setRequestBody(byteArrayInput);
0677:
0678: int putResult = m_httpClient.executeMethod(put);
0679: if (putResult != HttpStatus.SC_NO_CONTENT) {
0680: throw new UnableToWriteException(
0681: "The HTTP status code of the PUT was: "
0682: + putResult);
0683: }
0684: } finally {
0685: StreamCleanup.close(byteArrayInput,
0686: "byte array for result document", getLog());
0687: }
0688: } finally {
0689: MethodCleanup.releaseConnection(put,
0690: "PUT for document content", getLog());
0691: }
0692: }
0693:
0694: /**
0695: * <p>
0696: * Write the XML document to a byte array using the UTF-8 character
0697: * encoding.
0698: * </p>
0699: *
0700: * @param document
0701: * the document to write to the byte array.
0702: * @return a byte array containing the UTF-8 representation of the XML
0703: * document.
0704: * @throws IOException
0705: * if an I/O error occurs.
0706: */
0707: private byte[] writeDocumentToByteArray(Document document)
0708: throws IOException {
0709: ByteArrayOutputStream byteArrayOutput = new ByteArrayOutputStream();
0710: try {
0711: XMLOutputter outputter = new XMLOutputter();
0712: Format format = Format.getCompactFormat();
0713: format.setEncoding("UTF-8");
0714: outputter.setFormat(format);
0715: outputter.output(document, byteArrayOutput);
0716: } finally {
0717: StreamCleanup.close(byteArrayOutput,
0718: "byte array for result document", getLog());
0719: }
0720:
0721: return byteArrayOutput.toByteArray();
0722: }
0723:
0724: /**
0725: * <p>
0726: * Retrieve the document information that is needed for making the
0727: * decision if a document can be processed. This method will retrieve
0728: * information about the privileges of the current user and the lock (if
0729: * present).
0730: * </p>
0731: *
0732: * @param documentUrl
0733: * the relative URL of the document.
0734: * @return a map containing the HTTP status code, privileges and lock
0735: * information. (<code>Map<String, Object></code>)
0736: * @throws IOException
0737: * if an I/O error occurs.
0738: */
0739: private Map retrieveDocumentInformationNeededForProcessableDecision(
0740: String documentUrl) throws IOException {
0741: Map result = new HashMap();
0742:
0743: Enumeration propertiesToRetrieveEnumeration = PROPERTY_NAMES_NEEDED_FOR_PROCESSABLE_DECISION
0744: .elements();
0745: String fullDocumentUrl = getDocumentsBaseUrl() + documentUrl;
0746: PropFindMethod propFind = new PropFindMethod(fullDocumentUrl,
0747: propertiesToRetrieveEnumeration);
0748: try {
0749: propFind.setDoAuthentication(true);
0750:
0751: int propFindResult = m_httpClient.executeMethod(propFind);
0752: result.put(HTTP_STATUS_CODE_KEY,
0753: new Integer(propFindResult));
0754:
0755: addPropertiesToDocumentInformation(result, propFind);
0756: } finally {
0757: MethodCleanup
0758: .releaseConnection(
0759: propFind,
0760: "PROPFIND for document properties needed for processable decision",
0761: getLog());
0762: }
0763:
0764: return result;
0765: }
0766:
0767: /**
0768: * <p>
0769: * Add the properties containing information about the privileges of the
0770: * user and the lock returned by a <code>PROPFIND</code> method to the
0771: * document information. This method will only add the properties of the
0772: * first document (if present) returned by the method.
0773: * </p>
0774: *
0775: * @param documentInformation
0776: * the map to which to add the properties.
0777: * @param propFind
0778: * the <code>PROPFIND</code> method that was used to
0779: * request the properties.
0780: */
0781: private void addPropertiesToDocumentInformation(
0782: Map documentInformation, PropFindMethod propFind) {
0783: Enumeration responseUrlsEnumeration = propFind
0784: .getAllResponseURLs();
0785: if (responseUrlsEnumeration.hasMoreElements()) {
0786: String responseUrl = (String) responseUrlsEnumeration
0787: .nextElement();
0788:
0789: Enumeration propertiesEnumeration = propFind
0790: .getResponseProperties(responseUrl);
0791: addPropertiesToDocumentInformation(documentInformation,
0792: propertiesEnumeration);
0793: }
0794: }
0795:
0796: /**
0797: * <p>
0798: * Add the properties containing information about the privileges of the
0799: * user and the lock to the document information.
0800: * </p>
0801: *
0802: * @param documentInformation
0803: * the map to which to add the properties.
0804: * @param propertiesEnumeration
0805: * the enumeration over the properties.
0806: */
0807: private void addPropertiesToDocumentInformation(
0808: Map documentInformation, Enumeration propertiesEnumeration) {
0809: while (propertiesEnumeration.hasMoreElements()) {
0810: Property property = (Property) propertiesEnumeration
0811: .nextElement();
0812:
0813: if (isCurrentUserPrivilegeSetProperty(property)) {
0814: Element currentUserPrivilegeSetElement = property
0815: .getElement();
0816: documentInformation.put(CURRENT_USER_PRIVILEGE_SET_KEY,
0817: currentUserPrivilegeSetElement);
0818: } else if (isLockDiscoveryProperty(property)) {
0819: Element lockDiscoveryElement = property.getElement();
0820: documentInformation.put(LOCK_DISCOVERY_KEY,
0821: lockDiscoveryElement);
0822: }
0823: }
0824: }
0825:
0826: /**
0827: * <p>
0828: * Determine if a property contains the current privileges of a user.
0829: * </p>
0830: *
0831: * @param property
0832: * the property of which to determine if it contains
0833: * privileges.
0834: * @return <code>true</code> if the property contains privileges,
0835: * <code>false</code> otherwise.
0836: */
0837: private boolean isCurrentUserPrivilegeSetProperty(Property property) {
0838: return doesPropertyHaveName(property,
0839: CURRENT_USER_PRIVILEGE_SET_PROPERTY_NAME);
0840: }
0841:
0842: /**
0843: * <p>
0844: * Determine if a property contains information about a lock.
0845: * </p>
0846: *
0847: * @param property
0848: * the property of which to determine if it contains lock
0849: * information.
0850: * @return <code>true</code> if the property contains lock
0851: * information, <code>false</code> otherwise.
0852: */
0853: private boolean isLockDiscoveryProperty(Property property) {
0854: return doesPropertyHaveName(property,
0855: LOCK_DISCOVERY_PROPERTY_NAME);
0856: }
0857:
0858: /**
0859: * <p>
0860: * Determine if a property has a specific name.
0861: * </p>
0862: *
0863: * @param property
0864: * the property of which to determine if it has name
0865: * <code>propertyName</code>.
0866: * @param propertyName
0867: * the proerty name.
0868: * @return <code>true</code> if the property has name
0869: * <code>propertyName</code>, <code>false</code> otherwise.
0870: */
0871: private boolean doesPropertyHaveName(Property property,
0872: PropertyName propertyName) {
0873: boolean areNamespaceUrisEqual = property.getNamespaceURI()
0874: .equals(propertyName.getNamespaceURI());
0875: boolean areLocalNamesEqual = property.getLocalName().equals(
0876: propertyName.getLocalName());
0877: return areNamespaceUrisEqual && areLocalNamesEqual;
0878: }
0879:
0880: /**
0881: * <p>
0882: * Determine if replacement can take place for a document. Replacement
0883: * can take place if the properties of the document were successfully
0884: * retrieved, the user has the privilege to write the document and the
0885: * document is not locked.
0886: * </p>
0887: *
0888: * @param documentInformation
0889: * the map containing information about the result of the
0890: * <code>PROPFIND</code> method used to retrieve the
0891: * properties, and the properties.
0892: * @return <code>true</code> if replacement can take place for the
0893: * document, <code>false</code> otherwise.
0894: */
0895: private boolean canDocumentBeProcessed(Map documentInformation) {
0896: boolean result = false;
0897:
0898: result = wasPropFindSuccessful(documentInformation)
0899: && isWritable(documentInformation)
0900: && isUnlocked(documentInformation);
0901:
0902: return result;
0903: }
0904:
0905: /**
0906: * <p>
0907: * Determine if a <code>PROPFIND</code> method was executed
0908: * successfully. The method was successful if a HTTP status code with
0909: * value <code>SC_MULTI_STATUS</code> (207) is present.
0910: * </p>
0911: *
0912: * @param documentInformation
0913: * the map containing information about the result of the
0914: * <code>PROPFIND</code> method used to retrieve the
0915: * properties.
0916: * @return <code>true</code> if the <code>PROPFIND</code> method was
0917: * successful, <code>false</code> othwewise.
0918: */
0919: private boolean wasPropFindSuccessful(Map documentInformation) {
0920: boolean result = false;
0921:
0922: int httpStatusCode = ((Integer) documentInformation
0923: .get(HTTP_STATUS_CODE_KEY)).intValue();
0924: if (httpStatusCode == HttpStatus.SC_MULTI_STATUS) {
0925: result = true;
0926: }
0927:
0928: return result;
0929: }
0930:
0931: /**
0932: * <p>
0933: * Determine if a document is writable for the user. The document is
0934: * writable if the privileges contain <code>DAV:write</code>.
0935: * </p>
0936: *
0937: * @param documentInformation
0938: * the map containing the properties of the document.
0939: * @return <code>true</code> if the user may write the document,
0940: * <code>false</code> otherwise.
0941: */
0942: private boolean isWritable(Map documentInformation) {
0943: boolean result = false;
0944:
0945: if (documentInformation
0946: .containsKey(CURRENT_USER_PRIVILEGE_SET_KEY)) {
0947: Element currentUserPrivilegeSetElement = (Element) documentInformation
0948: .get(CURRENT_USER_PRIVILEGE_SET_KEY);
0949: NodeList davWriteElements = currentUserPrivilegeSetElement
0950: .getElementsByTagNameNS(DAV_NAMESPACE,
0951: WRITE_LOCAL_NAME);
0952: if (0 < davWriteElements.getLength()) {
0953: result = true;
0954: }
0955: }
0956:
0957: return result;
0958: }
0959:
0960: /**
0961: * <p>
0962: * Determine if a document is unlocked. The document is unlocked if the
0963: * information in property <code>DAV:lockdiscovery</code> does not
0964: * contain <code>DAV:activelock</code>.
0965: * </p>
0966: *
0967: * @param documentInformation
0968: * the map containing the properties of the document.
0969: * @return <code>true</code> if the document is unlocked,
0970: * <code>false</code> otherwise.
0971: */
0972: private boolean isUnlocked(Map documentInformation) {
0973: boolean result = false;
0974:
0975: if (documentInformation.containsKey(LOCK_DISCOVERY_KEY)) {
0976: Element lockDiscoveryElement = (Element) documentInformation
0977: .get(LOCK_DISCOVERY_KEY);
0978: NodeList davActiveLockElements = lockDiscoveryElement
0979: .getElementsByTagNameNS(DAV_NAMESPACE,
0980: ACTIVE_LOCK_LOCAL_NAME);
0981: if (davActiveLockElements.getLength() == 0) {
0982: result = true;
0983: }
0984: }
0985:
0986: return result;
0987: }
0988:
0989: /**
0990: * <p>
0991: * Determine the error code based on the HTTP status code of the
0992: * <code>PROPFIND</code> method used to retrieve the document
0993: * properties, the absence of properties or the value of properties.
0994: * </p>
0995: *
0996: * @param documentInformation
0997: * the map containing information about the result of the
0998: * <code>PROPFIND</code> method used to retrieve the
0999: * properties, and the properties.
1000: * @return the error code describing the problem or
1001: * {@link SearchAndReplaceErrorCodes#UNKNOWN_ERROR_CODE} if the
1002: * problem cannot be determined.
1003: */
1004: private int determineErrorCode(Map documentInformation) {
1005: int result = SearchAndReplaceErrorCodes.UNKNOWN_ERROR_CODE;
1006:
1007: if (!wasPropFindSuccessful(documentInformation)) {
1008: result = determineHttpStatusErrorCode(documentInformation);
1009: } else if (!documentInformation
1010: .containsKey(CURRENT_USER_PRIVILEGE_SET_KEY)) {
1011: result = SearchAndReplaceErrorCodes.MISSING_CURRENT_USER_PRIVILEGE_SET_CODE;
1012: } else if (!isWritable(documentInformation)) {
1013: result = SearchAndReplaceErrorCodes.UNAUTHORIZED_CODE;
1014: } else if (!documentInformation.containsKey(LOCK_DISCOVERY_KEY)) {
1015: result = SearchAndReplaceErrorCodes.MISSING_LOCK_DISCOVERY_CODE;
1016: } else if (!isUnlocked(documentInformation)) {
1017: result = SearchAndReplaceErrorCodes.LOCKED_CODE;
1018: } else {
1019: result = SearchAndReplaceErrorCodes.UNKNOWN_ERROR_CODE;
1020: }
1021:
1022: return result;
1023: }
1024:
1025: /**
1026: * <p>
1027: * Determine the error code based on the HTTP status code returned by
1028: * the <code>PROPFIND</code> method.
1029: * </p>
1030: *
1031: * @param documentInformation
1032: * the map containing information about the result of the
1033: * <code>PROPFIND</code> method used to retrieve the
1034: * document properties.
1035: * @return the error code describing the problem or
1036: * {@link SearchAndReplaceErrorCodes#UNEXPECTED_HTTP_STATUS_CODE}
1037: * if the HTTP status code is not a common one.
1038: */
1039: private int determineHttpStatusErrorCode(Map documentInformation) {
1040: int result;
1041:
1042: int propFindResult = ((Integer) documentInformation
1043: .get(HTTP_STATUS_CODE_KEY)).intValue();
1044: switch (propFindResult) {
1045: case HttpStatus.SC_NOT_FOUND:
1046: result = SearchAndReplaceErrorCodes.DOCUMENT_NOT_FOUND_CODE;
1047: break;
1048: case HttpStatus.SC_FORBIDDEN:
1049: result = SearchAndReplaceErrorCodes.FORBIDDEN_CODE;
1050: break;
1051: default:
1052: result = SearchAndReplaceErrorCodes.UNEXPECTED_HTTP_STATUS_CODE;
1053: break;
1054: }
1055:
1056: return result;
1057: }
1058:
1059: /**
1060: * <p>
1061: * Notify the listener there was a problem (i.e. an exception was
1062: * thrown) during the processing of a document, and log a warning.
1063: * </p>
1064: *
1065: * @param documentUrl
1066: * the relative URL of the document that was being
1067: * processed.
1068: * @param errorCode
1069: * the error code to pass to the listener.
1070: * @param exception
1071: * the exception that occurred.
1072: * @param message
1073: * the message to log. The document URL will be appended
1074: * to this message.
1075: */
1076: private void notifyListenerOfProblemAndLogWarning(
1077: String documentUrl, int errorCode, Exception exception,
1078: String message) {
1079: getListener().unsuccessfulProcessingOfDocument(documentUrl,
1080: errorCode);
1081: getLog().warning(message + documentUrl, exception);
1082: }
1083:
1084: /**
1085: * <p>
1086: * Determine the text that should be searched for. The text to search
1087: * for depends on the case-sensitivity setting.
1088: * </p>
1089: */
1090: private void determineTextToSearchFor() {
1091: if (isSearchCaseSensitive()) {
1092: m_textToSearchFor = getTextToReplace();
1093: } else {
1094: m_textToSearchFor = getTextToReplace().toLowerCase(
1095: getLocale());
1096: }
1097: }
1098:
1099: /**
1100: * <p>
1101: * Create the HTTP client that will be used to communicate with the
1102: * WebDAV repository.
1103: * </p>
1104: */
1105: private void createHttpClient() {
1106: m_httpClient = new HttpClient(
1107: new MultiThreadedHttpConnectionManager());
1108: m_httpClient.setState(getHttpState());
1109: }
1110:
1111: /**
1112: * <p>
1113: * Validate the configuration. If it is not valid throw an
1114: * {@link IllegalArgumentException}. A configuration is valid if:
1115: * </p>
1116: * <table>
1117: * <tr>
1118: * <th>Property</th>
1119: * <th>Requirements</th>
1120: * </tr>
1121: * <tr>
1122: * <td>documentsBaseUrl</td>
1123: * <td>Cannot be <code>null</code> or an empty string</td>
1124: * </tr>
1125: * <tr>
1126: * <td>scopeXpath</td>
1127: * <td>Cannot be <code>null</code> or an empty string.</td>
1128: * </tr>
1129: * <tr>
1130: * <td>textToReplace</td>
1131: * <td>Cannot be <code>null</code> or an empty string.</td>
1132: * </tr>
1133: * <tr>
1134: * <td>locale</td>
1135: * <td>Cannot be <code>null</code> if the search is case-insensitive.</td>
1136: * </tr>
1137: * <tr>
1138: * <td>replacementText</td>
1139: * <td>Cannot be <code>null</code>.</td>
1140: * </tr>
1141: * <tr>
1142: * <td>httpState</td>
1143: * <td>Cannot be <code>null</code>.</td>
1144: * </tr>
1145: * <tr>
1146: * <td>listener</td>
1147: * <td>Cannot be <code>null</code>.</td>
1148: * </tr>
1149: * <tr>
1150: * <td>log</td>
1151: * <td>Cannot be <code>null</code>.</td>
1152: * </tr>
1153: * </table>
1154: */
1155: private static void assertConfigurationIsValid(
1156: SearchAndReplaceConfiguration configuration) {
1157:
1158: String documentsBaseUrl = configuration.getDocumentsBaseUrl();
1159: if (documentsBaseUrl == null || documentsBaseUrl.equals("")) {
1160: throw new IllegalArgumentException(
1161: "Documents base URL must not be 'null' or an empty string.");
1162: }
1163:
1164: String scopeXpath = configuration.getScopeXpath();
1165: if (scopeXpath == null || scopeXpath.equals("")) {
1166: throw new IllegalArgumentException(
1167: "Scope XPath must not be 'null' or an empty string.");
1168: }
1169:
1170: String textToReplace = configuration.getTextToReplace();
1171: if (textToReplace == null || textToReplace.equals("")) {
1172: throw new IllegalArgumentException(
1173: "Text to replace must not be 'null' or an empty string.");
1174: }
1175:
1176: if (!configuration.isSearchCaseSensitive()) {
1177: Locale locale = configuration.getLocale();
1178: if (locale == null) {
1179: throw new IllegalArgumentException(
1180: "Locale must not be 'null' if the search is case-insensitive.");
1181: }
1182: }
1183:
1184: String replacementText = configuration.getReplacementText();
1185: if (replacementText == null) {
1186: throw new IllegalArgumentException(
1187: "Replacement text must not be 'null'.");
1188: }
1189:
1190: HttpState httpState = configuration.getHttpState();
1191: if (httpState == null) {
1192: throw new IllegalArgumentException(
1193: "HTTP state must not be 'null'.");
1194: }
1195:
1196: SearchAndReplaceListener listener = configuration.getListener();
1197: if (listener == null) {
1198: throw new IllegalArgumentException(
1199: "Listener must not be 'null'.");
1200: }
1201:
1202: SearchAndReplaceLog log = configuration.getLog();
1203: if (log == null) {
1204: throw new IllegalArgumentException(
1205: "Log must not be 'null'.");
1206: }
1207: }
1208:
1209: /**
1210: * <p>
1211: * Get the documents base URL from the configuration.
1212: * </p>
1213: *
1214: * @return the documents base URL.
1215: */
1216: private String getDocumentsBaseUrl() {
1217: return m_configuration.getDocumentsBaseUrl();
1218: }
1219:
1220: /**
1221: * <p>
1222: * Get an iterator over the relative document URLs from the
1223: * configuration.
1224: * </p>
1225: *
1226: * @return an iterator over the document URLs.
1227: */
1228: private Iterator documentUrlsIterator() {
1229: return m_configuration.documentUrlsIterator();
1230: }
1231:
1232: /**
1233: * <p>
1234: * Get the XPath of the scope to replace in from the configuration.
1235: * </p>
1236: *
1237: * @return the XPath of the scope to replace in.
1238: */
1239: private String getScopeXpath() {
1240: return m_configuration.getScopeXpath();
1241: }
1242:
1243: /**
1244: * <p>
1245: * Get the text to replace from the configuration.
1246: * </p>
1247: *
1248: * @return the text to replace.
1249: */
1250: private String getTextToReplace() {
1251: return m_configuration.getTextToReplace();
1252: }
1253:
1254: /**
1255: * <p>
1256: * Get the case-sensitivity of the search from the configuration.
1257: * </p>
1258: *
1259: * @return the case-sensitivity of the search.
1260: */
1261: private boolean isSearchCaseSensitive() {
1262: return m_configuration.isSearchCaseSensitive();
1263: }
1264:
1265: /**
1266: * <p>
1267: * Get the locale that should be used to transform strings to lower case
1268: * if the search is case-insensitive from the configuration.
1269: * </p>
1270: *
1271: * @return the locale to use to transform strings to lower case.
1272: */
1273: private Locale getLocale() {
1274: return m_configuration.getLocale();
1275: }
1276:
1277: /**
1278: * <p>
1279: * Get the replacement text from the configuration.
1280: * </p>
1281: *
1282: * @return the replacement text.
1283: */
1284: private String getReplacementText() {
1285: return m_configuration.getReplacementText();
1286: }
1287:
1288: /**
1289: * <p>
1290: * Get the HTTP state that must be used by the HTTP client used to
1291: * communicate with the WebDAV repository from the configuration.
1292: * </p>
1293: *
1294: * @return the HTTP state.
1295: */
1296: private HttpState getHttpState() {
1297: return m_configuration.getHttpState();
1298: }
1299:
1300: /**
1301: * <p>
1302: * Get the listener to notify of the progress from the configuration.
1303: * </p>
1304: *
1305: * @return the listener.
1306: */
1307: public SearchAndReplaceListener getListener() {
1308: return m_configuration.getListener();
1309: }
1310:
1311: /**
1312: * <p>
1313: * Get the log from the configuration.
1314: * </p>
1315: *
1316: * @return the log.
1317: */
1318: private SearchAndReplaceLog getLog() {
1319: return m_configuration.getLog();
1320: }
1321: }
|