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