001    /*
002     * $Id: Submitter.java,v 1.9 2012/02/19 17:35:50 davep Exp $
003     *
004     * This file is part of McIDAS-V
005     *
006     * Copyright 2007-2012
007     * Space Science and Engineering Center (SSEC)
008     * University of Wisconsin - Madison
009     * 1225 W. Dayton Street, Madison, WI 53706, USA
010     * https://www.ssec.wisc.edu/mcidas
011     * 
012     * All Rights Reserved
013     * 
014     * McIDAS-V is built on Unidata's IDV and SSEC's VisAD libraries, and
015     * some McIDAS-V source code is based on IDV and VisAD source code.  
016     * 
017     * McIDAS-V is free software; you can redistribute it and/or modify
018     * it under the terms of the GNU Lesser Public License as published by
019     * the Free Software Foundation; either version 3 of the License, or
020     * (at your option) any later version.
021     * 
022     * McIDAS-V is distributed in the hope that it will be useful,
023     * but WITHOUT ANY WARRANTY; without even the implied warranty of
024     * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
025     * GNU Lesser Public License for more details.
026     * 
027     * You should have received a copy of the GNU Lesser Public License
028     * along with this program.  If not, see http://www.gnu.org/licenses.
029     */
030    package edu.wisc.ssec.mcidasv.supportform;
031    
032    import java.io.ByteArrayInputStream;
033    import java.io.File;
034    import java.io.InputStream;
035    import java.util.ArrayList;
036    import java.util.List;
037    
038    import org.apache.commons.httpclient.Header;
039    import org.apache.commons.httpclient.HttpClient;
040    import org.apache.commons.httpclient.methods.PostMethod;
041    import org.apache.commons.httpclient.methods.multipart.FilePart;
042    import org.apache.commons.httpclient.methods.multipart.MultipartRequestEntity;
043    import org.apache.commons.httpclient.methods.multipart.Part;
044    import org.apache.commons.httpclient.methods.multipart.PartSource;
045    import org.apache.commons.httpclient.methods.multipart.StringPart;
046    
047    import org.slf4j.Logger;
048    import org.slf4j.LoggerFactory;
049    
050    import ucar.unidata.util.IOUtil;
051    import ucar.unidata.util.WrapperException;
052    
053    import edu.wisc.ssec.mcidasv.util.BackgroundTask;
054    
055    public class Submitter extends BackgroundTask<String> {
056    
057        private static final Logger logger = LoggerFactory.getLogger(Submitter.class);
058    
059        /** We'll follow up to this many redirects for {@code requestUrl}. */
060        private static final int POST_ATTEMPTS = 5;
061    
062        /** Used to gather user input and system information. */
063        private final SupportForm form;
064    
065        /** URL that we'll attempt to {@code POST} our requests at.*/
066        private final String requestUrl = "https://www.ssec.wisc.edu/mcidas/misc/mc-v/supportreq/support.php";
067    
068        /** Keeps track of the most recent redirect for {@code requestUrl}. */
069        private String validFormUrl = requestUrl;
070    
071        /** Number of redirects we've tried since starting. */
072        private int tryCount = 0;
073    
074        /** Handy reference to the status code (and more) of our {@code POST}. */
075        private PostMethod method = null;
076    
077        public Submitter(final SupportForm form) {
078            this.form = form;
079        }
080    
081        /** 
082         * Creates a file attachment that's based upon a real file.
083         * 
084         * @param id The parameter ID. Usually something like 
085         * {@literal "form_data[att_two]"}.
086         * @param file Path to the file that's going to be attached.
087         * 
088         * @return {@code POST}-able file attachment using the name and contents of
089         * {@code file}.
090         */
091        private static FilePart buildRealFilePart(final String id, final String file) {
092            return new FilePart(id, new PartSource() {
093                public InputStream createInputStream() {
094                    try {
095                        return IOUtil.getInputStream(file);
096                    } catch (Exception e) {
097                        throw new WrapperException("Reading file: "+file, e);
098                    }
099                }
100                public String getFileName() {
101                    return new File(file).getName();
102                }
103                public long getLength() {
104                    return new File(file).length();
105                }
106            });
107        }
108    
109        /**
110         * Creates a file attachment that isn't based upon an actual file. Useful 
111         * for something like the {@literal "extra"} attachment where you collect
112         * a bunch of data but don't want to deal with creating a temporary file.
113         * 
114         * @param id Parameter ID. Typically something like 
115         * {@literal "form_data[att_extra]"}.
116         * @param file Fake name of the file. Can be whatever you like.
117         * @param data The actual data to place inside the attachment.
118         * 
119         * @return {@code POST}-able file attachment using a spoofed filename!
120         */
121        private static FilePart buildFakeFilePart(final String id, final String file, final byte[] data) {
122            return new FilePart(id, new PartSource() {
123                public InputStream createInputStream() {
124                    return new ByteArrayInputStream(data);
125                }
126                public String getFileName() {
127                    return file;
128                }
129                public long getLength() {
130                    return data.length;
131                }
132            });
133        }
134    
135        /**
136         * Attempts to {@code POST} to {@code url} using the information from 
137         * {@code form}.
138         * 
139         * @param url URL that'll accept the {@code POST}. Typically 
140         * {@link #requestUrl}.
141         * @param form The {@link SupportForm} that contains the data to use in the
142         * support request.
143         * 
144         * @return Big honkin' object that contains the support request.
145         */
146        private static PostMethod buildPostMethod(String url, SupportForm form) {
147            PostMethod method = new PostMethod(url);
148    
149            List<Part> parts = new ArrayList<Part>();
150            parts.add(new StringPart("form_data[fromName]", form.getUser()));
151            parts.add(new StringPart("form_data[email]", form.getEmail()));
152            parts.add(new StringPart("form_data[organization]", form.getOrganization()));
153            parts.add(new StringPart("form_data[subject]", form.getSubject()));
154            parts.add(new StringPart("form_data[description]", form.getDescription()));
155            parts.add(new StringPart("form_data[submit]", ""));
156            parts.add(new StringPart("form_data[p_version]", "p_version=ignored"));
157            parts.add(new StringPart("form_data[opsys]", "opsys=ignored"));
158            parts.add(new StringPart("form_data[hardware]", "hardware=ignored"));
159            parts.add(new StringPart("form_data[cc_user]", Boolean.toString(form.getSendCopy())));
160    
161            // attach the files the user has explicitly attached.
162            if (form.hasAttachmentOne()) {
163                parts.add(buildRealFilePart("form_data[att_two]", form.getAttachmentOne()));
164            }
165            if (form.hasAttachmentTwo()) {
166                parts.add(buildRealFilePart("form_data[att_three]", form.getAttachmentTwo()));
167            }
168            // if the user wants, attach an XML bundle of the state
169            if (form.canBundleState() && form.getSendBundle()) {
170                parts.add(buildFakeFilePart("form_data[att_state]", form.getBundledStateName(), form.getBundledState()));
171            }
172    
173            // attach system properties
174            parts.add(buildFakeFilePart("form_data[att_extra]", form.getExtraStateName(), form.getExtraState()));
175    
176            if (form.canSendLog()) {
177                parts.add(buildRealFilePart("form_data[att_log]", form.getLogPath()));
178            }
179    
180            Part[] arr = parts.toArray(new Part[0]);
181            MultipartRequestEntity mpr = new MultipartRequestEntity(arr, method.getParams());
182            method.setRequestEntity(mpr);
183            return method;
184        }
185    
186        protected String compute() {
187            // logic ripped from the IDV's HttpFormEntry#doPost(List, String)
188            try {
189                while ((tryCount++ < POST_ATTEMPTS) && !isCancelled()) {
190                    method = buildPostMethod(validFormUrl, form);
191                    HttpClient client = new HttpClient();
192                    client.executeMethod(method);
193                    if (method.getStatusCode() >= 300 && method.getStatusCode() <= 399) {
194                        Header location = method.getResponseHeader("location");
195                        if (location == null) {
196                            return "Error: No 'location' given on the redirect";
197                        }
198                        validFormUrl = location.getValue();
199                        if (method.getStatusCode() == 301) {
200                            logger.warn("form post has been permanently moved to: {}", validFormUrl);
201                        }
202                        continue;
203                    }
204                    break;
205                }
206                return IOUtil.readContents(method.getResponseBodyAsStream());
207            } catch (Exception e) {
208                throw new WrapperException("doing post", e);
209            }
210        }
211    
212        @Override protected void onCompletion(String result, Throwable exception, boolean cancelled) {
213            logger.trace("result={} exception={} cancelled={}", new Object[] { result, exception, cancelled });
214            if (cancelled) {
215                return;
216            }
217    
218            if (exception == null) {
219                form.showSuccess();
220            } else {
221                form.showFailure(exception.getMessage());
222            }
223        }
224    
225    }