blob: 72b39412b47c0f216db48ab8ad953ea3ab6009cb [file] [log] [blame]
// Copyright 2011 Google Inc. All Rights Reserved.
//
// Licensed 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 adaptorlib;
import java.io.*;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.logging.Logger;
import java.util.regex.Pattern;
import java.util.zip.GZIPOutputStream;
/** Takes an XML feed file for the GSA, sends it to GSA and
then reads reply from GSA. */
class GsaFeedFileSender {
private static final Logger log
= Logger.getLogger(GsaFeedFileSender.class.getName());
private static final Pattern DATASOURCE_FORMAT
= Pattern.compile("[a-zA-Z_][a-zA-Z0-9_-]*");
/** Indicates failure creating connection to GSA. */
static class FailedToConnect extends Exception {
public FailedToConnect(IOException e) {
super(e);
}
// TODO(pjo): Add corrective tips.
}
/** Indicates failure to send XML feed file to GSA. */
static class FailedWriting extends Exception {
public FailedWriting(IOException e) {
super(e);
}
// TODO(pjo): Add corrective tips.
}
/** Indicates failure to read response to sent XML feed file. */
static class FailedReadingReply extends Exception {
public FailedReadingReply(IOException e) {
super(e);
}
// TODO(pjo): Add corrective tips.
}
/** Configuration for GSA's encoding and whether to use HTTPS. */
private final Config config;
// Feed file XML will not contain "<<".
private static final String BOUNDARY = "<<";
// Another frequently used constant of sent message.
private static final String CRLF = "\r\n";
public GsaFeedFileSender(Config config) {
this.config = config;
}
// Get bytes of string in communication's encoding.
private byte[] toEncodedBytes(String s) {
return s.getBytes(config.getGsaCharacterEncoding());
}
/** Helper method for creating a multipart/form-data HTTP post.
Creates a post parameter made of a name and value. */
private void buildPostParameter(StringBuilder sb, String name,
String mimetype, String value) {
sb.append("--").append(BOUNDARY).append(CRLF);
sb.append("Content-Disposition: form-data;");
sb.append(" name=\"").append(name).append("\"").append(CRLF);
sb.append("Content-Type: ").append(mimetype).append(CRLF);
sb.append(CRLF).append(value).append(CRLF);
}
private byte[] buildMessage(String datasource, String feedtype,
String xmlDocument) {
StringBuilder sb = new StringBuilder();
buildPostParameter(sb, "datasource", "text/plain", datasource);
buildPostParameter(sb, "feedtype", "text/plain", feedtype);
buildPostParameter(sb, "data", "text/xml", xmlDocument);
sb.append("--").append(BOUNDARY).append("--").append(CRLF);
return toEncodedBytes("" + sb);
}
/** Tries to get in touch with our GSA. */
private HttpURLConnection setupConnection(URL feedUrl, int len,
boolean useCompression)
throws IOException {
HttpURLConnection uc = (HttpURLConnection) feedUrl.openConnection();
uc.setDoInput(true);
uc.setDoOutput(true);
if (useCompression) {
uc.setChunkedStreamingMode(0);
// GSA can handle gziped content, although there isn't a way to find out
// other than just trying
uc.setRequestProperty("Content-Encoding", "gzip");
} else {
uc.setFixedLengthStreamingMode(len);
}
uc.setRequestProperty("Content-Type",
"multipart/form-data; boundary=" + BOUNDARY);
return uc;
}
/** Put bytes onto output stream. */
private void writeToGsa(HttpURLConnection uc, byte msgbytes[],
boolean useCompression)
throws IOException {
OutputStream outputStream = uc.getOutputStream();
try {
if (useCompression) {
// setupConnection set Content-Encoding: gzip
outputStream = new GZIPOutputStream(outputStream);
}
// Use copyStream(), because using a single write() prevents errors from
// propagating during writing and causes them to be discovered at read
// time. Using copyStream() isn't perfect either though, in that if
// buffered data eventually causes an error, then that will still be
// discovered at read time.
IOHelper.copyStream(new ByteArrayInputStream(msgbytes), outputStream);
outputStream.flush();
} finally {
outputStream.close();
}
}
/** Get GSA's response. */
private String readGsaReply(HttpURLConnection uc) throws IOException {
InputStream inputStream = uc.getInputStream();
String reply;
try {
reply = IOHelper.readInputStreamToString(inputStream,
config.getGsaCharacterEncoding());
} finally {
inputStream.close();
}
return reply;
}
private void handleGsaReply(String reply) {
if ("Success".equals(reply)) {
log.info("success message received");
} else {
throw new IllegalStateException("GSA reply: " + reply);
}
/* TODO(pjo): Recognize additional replies.
if ("Error - Unauthorized Request".equals(reply)) {
// TODO(pjo): Improve message with Admin Console details.
throw new IllegalStateException("GSA is not configured "
+ "to accept feeds from this IP. Please add <IP> "
+ "to permitted machines.");
}
if ("Internal Error".equals(reply))
if ("".equals(reply))
*/
}
/**
* Sends XML with provided datasource name and feedtype "metadata-and-url".
* Datasource name is limited to [a-zA-Z0-9_].
*/
void sendMetadataAndUrl(String host, String datasource,
String xmlString, boolean useCompression)
throws FailedToConnect, FailedWriting, FailedReadingReply {
URL feedUrl;
try {
if (config.isServerSecure()) {
feedUrl = new URL("https://" + host + ":19902/xmlfeed");
} else {
feedUrl = new URL("http://" + host + ":19900/xmlfeed");
}
} catch (MalformedURLException ex) {
throw new FailedToConnect(ex);
}
sendMetadataAndUrl(feedUrl, datasource, xmlString, useCompression);
}
/**
* Sends XML with provided datasource name and feedtype "metadata-and-url".
* Datasource name is limited to [a-zA-Z_][a-zA-Z0-9_-]*.
*/
void sendMetadataAndUrl(URL feedUrl, String datasource,
String xmlString, boolean useCompression)
throws FailedToConnect, FailedWriting, FailedReadingReply {
if (!DATASOURCE_FORMAT.matcher(datasource).matches()) {
throw new IllegalArgumentException("Data source contains illegal "
+ "characters: " + datasource);
}
String feedtype = "metadata-and-url";
byte msg[] = buildMessage(datasource, feedtype, xmlString);
// GSA only allows request content up to 1 MB to be compressed
if (msg.length >= 1 * 1024 * 1024) {
useCompression = false;
}
HttpURLConnection uc;
try {
uc = setupConnection(feedUrl, msg.length, useCompression);
uc.connect();
} catch (IOException ioe) {
throw new FailedToConnect(ioe);
}
try {
writeToGsa(uc, msg, useCompression);
} catch (IOException ioe) {
uc.disconnect();
throw new FailedWriting(ioe);
}
try {
String reply = readGsaReply(uc);
handleGsaReply(reply);
} catch (IOException ioe) {
uc.disconnect();
throw new FailedReadingReply(ioe);
}
}
}