001: package org.kohsuke.javanet.approval;
002:
003: import com.meterware.httpunit.WebConversation;
004: import dalma.Fiber;
005: import dalma.TimeUnit;
006: import dalma.Workflow;
007: import dalma.endpoints.email.MimeMessageEx;
008: import org.kohsuke.jnt.JNProject;
009: import org.kohsuke.jnt.JavaNet;
010: import org.kohsuke.jnt.MHTMLBuilder;
011: import org.kohsuke.jnt.ProcessingException;
012: import org.xml.sax.SAXException;
013: import org.dom4j.DocumentException;
014:
015: import javax.mail.Message;
016: import javax.mail.Message.RecipientType;
017: import javax.mail.MessagingException;
018: import javax.mail.internet.AddressException;
019: import javax.mail.internet.InternetAddress;
020: import javax.mail.internet.MimeBodyPart;
021: import javax.mail.internet.MimeMessage;
022: import javax.mail.internet.MimeMultipart;
023: import java.io.ByteArrayInputStream;
024: import java.io.ByteArrayOutputStream;
025: import java.io.IOException;
026: import java.io.PrintWriter;
027: import java.io.StringWriter;
028: import java.net.URL;
029: import java.util.Collection;
030: import java.util.HashMap;
031: import java.util.Map;
032: import java.util.TreeMap;
033: import java.util.logging.Level;
034: import java.util.regex.Matcher;
035: import java.util.regex.Pattern;
036:
037: /**
038: * Workflow implementation.
039: */
040: public class ConversationImpl extends Workflow {
041: private Main main;
042: private final String project;
043: private final String user;
044: private final String description;
045:
046: private final boolean testing = false;
047:
048: /**
049: * If non-null, connected connection.
050: */
051: private transient JavaNet connection;
052:
053: /**
054: * The community for which this project is currently considered.
055: *
056: * <p>
057: * If null, that means no community is considered, thus it's
058: * up to the domain admin.
059: */
060: private String community = null;
061:
062: /**
063: * If the project owner designated a community to which
064: */
065: private String preferredCommunity;
066: private OReillyProject op;
067:
068: public ConversationImpl(Main main, String project, String user,
069: String description) {
070: this .main = main;
071: this .project = project;
072: this .user = user;
073: this .description = description;
074: }
075:
076: public void setTitle(String title) {
077: getLogger().info("New title: " + title);
078: super .setTitle(title);
079: }
080:
081: @Override
082: public void run() {
083: try {
084: getLogger().info("Started processing " + project);
085: setTitle("Bootstrapping " + project);
086:
087: op = new OReillyProject(connect().getProject(project));
088: preferredCommunity = getPreferredCommunity(); // check the preferred community
089:
090: // main loop
091: while (true) {
092: if (community == null)
093: setTitle("Considering " + project);
094: else
095: setTitle("Considering " + project + " for "
096: + community + " community");
097:
098: MimeMessage msg = createInfoEmail();
099:
100: // loop until the current responsible person
101: // decides which community to route to.
102: while (true) {
103: MimeMessageEx r = main.email.waitForReply(msg);
104: Matcher m = r.findMainContent(COMMUNITY_NAME);
105: if (m != null) {
106: try {
107: String newCommunity = m.group()
108: .toLowerCase();
109: routeTo(newCommunity);
110: break; // successfully routed
111: } catch (RetryException e) {
112: msg = r
113: .replyWithError(
114: e.getMessage()
115: + "\n\nCan you please tell me what to do again?",
116: true);
117: getLogger().log(Level.INFO, "Failed", e);
118: continue;
119: } catch (ProcessingException e) {
120: // retry java.net error up to 6 hours for every hour
121: Fiber.again(1, TimeUnit.HOURS, 6);
122:
123: msg = r
124: .replyWithError(
125: e
126: + "\n\nCan you please tell me what to do again?",
127: true);
128: getLogger().log(Level.INFO, "Failed", e);
129: continue;
130: }
131: }
132:
133: msg = r
134: .replyWithError(
135: "I'm sorry, but I didn't understand what community you want it to go.\n"
136: + "Please decide which community to route this program to,\n"
137: + "and type it in the reply to this e-mail",
138: true);
139: }
140: }
141: } catch (Exception e) {
142: getLogger().log(Level.SEVERE, e.getMessage(), e);
143: }
144: }
145:
146: private String getPreferredCommunity() throws ProcessingException,
147: DocumentException, IOException, SAXException {
148: JNProject com = op.getCommunity(connect());
149: if (com == null)
150: return "(none)";
151: return com.getName();
152: }
153:
154: /**
155: * Route a project to the specified community.
156: *
157: * <p>
158: * If the community is "accept", it means accepting to
159: * the {@link #community current community}.
160: *
161: * <p>
162: * If the community is "return", then it means
163: * it goes back to the domain admin.
164: */
165: private void routeTo(String newCommunity) throws RetryException,
166: ProcessingException, IOException, MessagingException,
167: SAXException {
168: JavaNet con = connect();
169: JNProject p = con.getProject(project);
170:
171: if (newCommunity.equals("approve") && community != null) {
172: getLogger().info(
173: "Approving " + project + " to " + community);
174: p.approve();
175: JNProject com = con.getProject(community + "-incubator");
176: p.setParent(com);
177:
178: {// send welcome message
179: CommunityConfig config = new CommunityConfig(connect()
180: .getProject(community));
181: MimeMessage msg = config.createWelcomeMessage(
182: main.email.getSession(), createMacroMap());
183: if (msg != null) {
184: msg.addRecipient(RecipientType.TO,
185: new InternetAddress(p.getOwnerAlias()));
186: main.email.send(msg);
187: }
188: }
189:
190: op.approve(con.getProject(community));
191:
192: setTitle("Accepted " + project + " to " + community);
193: Fiber.exit();
194: }
195:
196: if (newCommunity.equals("return") && community != null) {
197: getLogger().info(
198: "Sending " + project + " back to the domain admin");
199: community = null;
200: p.setParent(con.getProject("cm-inbox"));
201: return;
202: }
203:
204: if (newCommunity.equals("disapprove")) {
205: getLogger().info("Disapproving " + project);
206: p.disapprove(Util.replace(getClass().getResourceAsStream(
207: "insufficientInfo.txt"), createMacroMap()));
208: op.disapprove(connect());
209:
210: setTitle("Disapproved " + project);
211: Fiber.exit();
212: }
213:
214: // otherwise...
215: getLogger().info(
216: "Moving " + project + " to the inbox of "
217: + newCommunity);
218:
219: JNProject com = con.getProject(newCommunity + "-inbox");
220: if (!com.exists()) {
221: throw new RetryException(com.getName() + " doesn't exist");
222: }
223: op.update(connect(), "Dispatching to " + newCommunity);
224:
225: community = newCommunity;
226:
227: p.setParent(com);
228: }
229:
230: private JavaNet connect() throws ProcessingException {
231: if (connection == null)
232: connection = JavaNet.connect(main.accountFile);
233: return connection;
234: }
235:
236: private MimeMessage createInfoEmail() throws MessagingException,
237: IOException, SAXException, ProcessingException {
238: MimeMessage msg = new MimeMessageEx(main.email.getSession());
239: msg.setSubject("[New Project] " + project);
240: MimeMultipart payload = new MimeMultipart();
241: msg.setContent(payload);
242:
243: {// main text part
244: StringWriter buf = new StringWriter();
245: PrintWriter out = new PrintWriter(buf);
246: out.println(user + " has started the " + project
247: + " project");
248: out.println();
249: out.println(Util.replace(getClass().getResourceAsStream(
250: community != null ? "communityLead.txt"
251: : "domainAdmin.txt"), createMacroMap()));
252: out.println("---------------");
253: out.println("Project description:");
254: out.println(description);
255: out.println();
256:
257: if (!testing) {
258: // list the projects this guy belongs to
259: out.println("---------------");
260: int count = connect().getUser(user).getProjects()
261: .size();
262: out.println("This user has " + count + " project(s).");
263: }
264:
265: out.println("---------------");
266: out
267: .println("See attachments for more information about this");
268:
269: MimeBodyPart main = new MimeBodyPart();
270: main.setContent(buf.toString(), "text/plain");
271: payload.addBodyPart(main);
272: }
273:
274: if (!testing) {
275: JavaNet con = connect();
276: // add web page capture of this new project
277: payload.addBodyPart(createMHTML(con.getConversation(), con
278: .getProject(project).getURL()));
279:
280: // add Google search of this user in java.net
281: payload.addBodyPart(createMHTML(con.getConversation(),
282: new URL(
283: "http://www.google.com/search?q=site%3Ajava.net+"
284: + user)));
285:
286: // add Google search of this project in the whole world
287: payload
288: .addBodyPart(createMHTML(con.getConversation(),
289: new URL("http://www.google.com/search?q="
290: + project)));
291: }
292:
293: if (community == null)
294: msg.setRecipients(Message.RecipientType.TO,
295: main.domainAdmin);
296: else {
297: // route to the community
298: CommunityConfig config = new CommunityConfig(connect()
299: .getProject(community));
300: for (String address : config.getNewProjectNotifications()) {
301: msg.addRecipient(Message.RecipientType.TO,
302: new InternetAddress(address));
303: }
304: }
305:
306: return msg;
307: }
308:
309: /**
310: * Creates a dictionary for macro substitution.
311: */
312: private Map<String, String> createMacroMap() {
313: Map<String, String> r = new HashMap<String, String>();
314: r.put("project", project);
315: r.put("community", community);
316: r.put("preferredCommunity", preferredCommunity);
317: return r;
318: }
319:
320: MimeBodyPart createMHTML(WebConversation conv, URL url)
321: throws IOException, SAXException, MessagingException {
322: ByteArrayOutputStream baos = new ByteArrayOutputStream();
323: MHTMLBuilder.produce(conv, url, baos);
324: MimeBodyPart part = new MimeBodyPart(new ByteArrayInputStream(
325: baos.toByteArray()));
326: part.setDisposition("attachment; filename=" + url);
327: return part;
328: }
329:
330: /**
331: * Creates a map from projects to their communities.
332: */
333: Map<JNProject, JNProject> createCommunityList(
334: Collection<JNProject> projects) throws ProcessingException {
335: Map<JNProject, JNProject> r = new TreeMap<JNProject, JNProject>();
336: for (JNProject p : projects) {
337: r.put(p, p.getOwnerCommunity());
338: }
339: return r;
340: }
341:
342: InternetAddress getCommunityLeads() {
343: try {
344: return new InternetAddress("leads@" + community
345: + ".dev.java.net");
346: } catch (AddressException e) {
347: throw new AssertionError(e);
348: }
349: }
350:
351: private static final Pattern COMMUNITY_NAME = Pattern
352: .compile("[A-z0-9a-z_\\-]+");
353: }
|