Paul Martin
2016-04-27 c2188a840bc4153ae92112b04b2e06a90d3944aa
commit | author | age
5e3521 1 /*
JM 2  * Copyright 2014 gitblit.com.
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *     http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 package com.gitblit.tickets;
17
18 import java.io.IOException;
19 import java.io.InputStream;
20 import java.text.DateFormat;
21 import java.text.MessageFormat;
22 import java.text.SimpleDateFormat;
23 import java.util.ArrayList;
24 import java.util.Arrays;
25 import java.util.Collections;
26 import java.util.HashMap;
27 import java.util.HashSet;
28 import java.util.List;
29 import java.util.Map;
30 import java.util.Set;
31 import java.util.TreeMap;
32 import java.util.TreeSet;
33 import java.util.regex.Matcher;
34 import java.util.regex.Pattern;
35
36 import org.apache.commons.io.IOUtils;
37 import org.apache.log4j.Logger;
38 import org.eclipse.jgit.diff.DiffEntry.ChangeType;
39 import org.eclipse.jgit.lib.Repository;
40 import org.eclipse.jgit.revwalk.RevCommit;
41 import org.slf4j.LoggerFactory;
42
43 import com.gitblit.Constants;
44 import com.gitblit.IStoredSettings;
45 import com.gitblit.Keys;
46 import com.gitblit.git.PatchsetCommand;
47 import com.gitblit.manager.INotificationManager;
48 import com.gitblit.manager.IRepositoryManager;
49 import com.gitblit.manager.IRuntimeManager;
50 import com.gitblit.manager.IUserManager;
51 import com.gitblit.models.Mailing;
52 import com.gitblit.models.PathModel.PathChangeModel;
53 import com.gitblit.models.RepositoryModel;
54 import com.gitblit.models.TicketModel;
55 import com.gitblit.models.TicketModel.Change;
56 import com.gitblit.models.TicketModel.Field;
57 import com.gitblit.models.TicketModel.Patchset;
58 import com.gitblit.models.TicketModel.Review;
59 import com.gitblit.models.TicketModel.Status;
60 import com.gitblit.models.UserModel;
61 import com.gitblit.utils.ArrayUtils;
62 import com.gitblit.utils.DiffUtils;
63 import com.gitblit.utils.DiffUtils.DiffStat;
64 import com.gitblit.utils.JGitUtils;
65 import com.gitblit.utils.MarkdownUtils;
66 import com.gitblit.utils.StringUtils;
67
68 /**
69  * Formats and queues ticket/patch notifications for dispatch to the
70  * mail executor upon completion of a push or a ticket update.  Messages are
71  * created as Markdown and then transformed to html.
72  *
73  * @author James Moger
74  *
75  */
76 public class TicketNotifier {
77
78     protected final Map<Long, Mailing> queue = new TreeMap<Long, Mailing>();
79
80     private final String SOFT_BRK = "\n";
81
82     private final String HARD_BRK = "\n\n";
83
84     private final String HR = "----\n\n";
85
86     private final IStoredSettings settings;
87
88     private final INotificationManager notificationManager;
89
90     private final IUserManager userManager;
91
92     private final IRepositoryManager repositoryManager;
93
94     private final ITicketService ticketService;
95
96     private final String addPattern = "<span style=\"color:darkgreen;\">+{0}</span>";
97     private final String delPattern = "<span style=\"color:darkred;\">-{0}</span>";
98
99     public TicketNotifier(
100             IRuntimeManager runtimeManager,
101             INotificationManager notificationManager,
102             IUserManager userManager,
103             IRepositoryManager repositoryManager,
104             ITicketService ticketService) {
105
106         this.settings = runtimeManager.getSettings();
107         this.notificationManager = notificationManager;
108         this.userManager = userManager;
109         this.repositoryManager = repositoryManager;
110         this.ticketService = ticketService;
111     }
112
113     public void sendAll() {
114         for (Mailing mail : queue.values()) {
115             notificationManager.send(mail);
116         }
117     }
118
119     public void sendMailing(TicketModel ticket) {
120         queueMailing(ticket);
121         sendAll();
122     }
123
124     /**
125      * Queues an update notification.
126      *
127      * @param ticket
128      * @return a notification object used for testing
129      */
130     public Mailing queueMailing(TicketModel ticket) {
131         try {
132             // format notification message
133             String markdown = formatLastChange(ticket);
134
135             StringBuilder html = new StringBuilder();
136             html.append("<head>");
137             html.append(readStyle());
dac2de 138             html.append(readViewTicketAction(ticket));
5e3521 139             html.append("</head>");
JM 140             html.append("<body>");
141             html.append(MarkdownUtils.transformGFM(settings, markdown, ticket.repository));
142             html.append("</body>");
143
144             Mailing mailing = Mailing.newHtml();
145             mailing.from = getUserModel(ticket.updatedBy == null ? ticket.createdBy : ticket.updatedBy).getDisplayName();
146             mailing.subject = getSubject(ticket);
147             mailing.content = html.toString();
148             mailing.id = "ticket." + ticket.number + "." + StringUtils.getSHA1(ticket.repository + ticket.number);
149
150             setRecipients(ticket, mailing);
151             queue.put(ticket.number, mailing);
152
153             return mailing;
154         } catch (Exception e) {
155             Logger.getLogger(getClass()).error("failed to queue mailing for #" + ticket.number, e);
156         }
157         return null;
158     }
159
160     protected String getSubject(TicketModel ticket) {
161         Change lastChange = ticket.changes.get(ticket.changes.size() - 1);
162         boolean newTicket = lastChange.isStatusChange() && ticket.changes.size() == 1;
163         String re = newTicket ? "" : "Re: ";
164         String subject = MessageFormat.format("{0}[{1}] {2} (#{3,number,0})",
165                 re, StringUtils.stripDotGit(ticket.repository), ticket.title, ticket.number);
166         return subject;
167     }
168
169     protected String formatLastChange(TicketModel ticket) {
170         Change lastChange = ticket.changes.get(ticket.changes.size() - 1);
171         UserModel user = getUserModel(lastChange.author);
172
173         // define the fields we do NOT want to see in an email notification
174         Set<TicketModel.Field> fieldExclusions = new HashSet<TicketModel.Field>();
175         fieldExclusions.addAll(Arrays.asList(Field.watchers, Field.voters));
176
177         StringBuilder sb = new StringBuilder();
1626c6 178         boolean newTicket = lastChange.isStatusChange() && Status.New == lastChange.getStatus();
5e3521 179         boolean isFastForward = true;
JM 180         List<RevCommit> commits = null;
181         DiffStat diffstat = null;
182
183         String pattern;
1626c6 184         if (lastChange.hasPatchset()) {
5e3521 185             // patchset uploaded
JM 186             Patchset patchset = lastChange.patchset;
187             String base = "";
188             // determine the changed paths
189             Repository repo = null;
190             try {
191                 repo = repositoryManager.getRepository(ticket.repository);
192                 if (patchset.isFF() && (patchset.rev > 1)) {
193                     // fast-forward update, just show the new data
194                     isFastForward = true;
195                     Patchset prev = ticket.getPatchset(patchset.number, patchset.rev - 1);
196                     base = prev.tip;
197                 } else {
198                     // proposal OR non-fast-forward update
199                     isFastForward = false;
200                     base = patchset.base;
201                 }
202
203                 diffstat = DiffUtils.getDiffStat(repo, base, patchset.tip);
204                 commits = JGitUtils.getRevLog(repo, base, patchset.tip);
205             } catch (Exception e) {
206                 Logger.getLogger(getClass()).error("failed to get changed paths", e);
207             } finally {
b37af4 208                 if (repo != null) {
JM 209                     repo.close();
210                 }
5e3521 211             }
JM 212
213             String compareUrl = ticketService.getCompareUrl(ticket, base, patchset.tip);
1626c6 214
JM 215             if (newTicket) {
216                 // new proposal
217                 pattern = "**{0}** is proposing a change.";
218                 sb.append(MessageFormat.format(pattern, user.getDisplayName()));
8d11fa 219                 fieldExclusions.add(Field.status);
JM 220                 fieldExclusions.add(Field.title);
221                 fieldExclusions.add(Field.body);
5e3521 222             } else {
1626c6 223                 // describe the patchset
JM 224                 if (patchset.isFF()) {
225                     pattern = "**{0}** added {1} {2} to patchset {3}.";
226                     sb.append(MessageFormat.format(pattern, user.getDisplayName(), patchset.added, patchset.added == 1 ? "commit" : "commits", patchset.number));
227                 } else {
228                     pattern = "**{0}** uploaded patchset {1}. *({2})*";
229                     sb.append(MessageFormat.format(pattern, user.getDisplayName(), patchset.number, patchset.type.toString().toUpperCase()));
230                 }
5e3521 231             }
JM 232             sb.append(HARD_BRK);
1626c6 233
5e3521 234             sb.append(MessageFormat.format("{0} {1}, {2} {3}, <span style=\"color:darkgreen;\">+{4} insertions</span>, <span style=\"color:darkred;\">-{5} deletions</span> from {6}. [compare]({7})",
JM 235                     commits.size(), commits.size() == 1 ? "commit" : "commits",
236                     diffstat.paths.size(),
237                     diffstat.paths.size() == 1 ? "file" : "files",
238                     diffstat.getInsertions(),
239                     diffstat.getDeletions(),
240                     isFastForward ? "previous revision" : "merge base",
241                     compareUrl));
242
243             // note commit additions on a rebase,if any
244             switch (lastChange.patchset.type) {
245             case Rebase:
246                 if (lastChange.patchset.added > 0) {
247                     sb.append(SOFT_BRK);
248                     sb.append(MessageFormat.format("{0} {1} added.", lastChange.patchset.added, lastChange.patchset.added == 1 ? "commit" : "commits"));
249                 }
250                 break;
251             default:
252                 break;
253             }
254             sb.append(HARD_BRK);
1626c6 255         } else if (lastChange.isStatusChange()) {
JM 256             if (newTicket) {
257                 fieldExclusions.add(Field.status);
258                 fieldExclusions.add(Field.title);
259                 fieldExclusions.add(Field.body);
260                 pattern = "**{0}** created this ticket.";
261                 sb.append(MessageFormat.format(pattern, user.getDisplayName()));
262             } else if (lastChange.hasField(Field.mergeSha)) {
263                 // closed by merged
264                 pattern = "**{0}** closed this ticket by merging {1} to {2}.";
265
266                 // identify patchset that closed the ticket
267                 String merged = ticket.mergeSha;
268                 for (Patchset patchset : ticket.getPatchsets()) {
269                     if (patchset.tip.equals(ticket.mergeSha)) {
270                         merged = patchset.toString();
271                         break;
272                     }
273                 }
274                 sb.append(MessageFormat.format(pattern, user.getDisplayName(), merged, ticket.mergeTo));
275             } else {
276                 // workflow status change by user
277                 pattern = "**{0}** changed the status of this ticket to **{1}**.";
278                 sb.append(MessageFormat.format(pattern, user.getDisplayName(), lastChange.getStatus().toString().toUpperCase()));
279             }
280             sb.append(HARD_BRK);
5e3521 281         } else if (lastChange.hasReview()) {
JM 282             // review
283             Review review = lastChange.review;
284             pattern = "**{0}** has reviewed patchset {1,number,0} revision {2,number,0}.";
285             sb.append(MessageFormat.format(pattern, user.getDisplayName(), review.patchset, review.rev));
286             sb.append(HARD_BRK);
287
288             String d = settings.getString(Keys.web.datestampShortFormat, "yyyy-MM-dd");
289             String t = settings.getString(Keys.web.timeFormat, "HH:mm");
290             DateFormat df = new SimpleDateFormat(d + " " + t);
291             List<Change> reviews = ticket.getReviews(ticket.getPatchset(review.patchset, review.rev));
292             sb.append("| Date | Reviewer      | Score | Description  |\n");
293             sb.append("| :--- | :------------ | :---: | :----------- |\n");
294             for (Change change : reviews) {
295                 String name = change.author;
296                 UserModel u = userManager.getUserModel(change.author);
297                 if (u != null) {
298                     name = u.getDisplayName();
299                 }
300                 String score;
301                 switch (change.review.score) {
302                 case approved:
303                     score = MessageFormat.format(addPattern, change.review.score.getValue());
304                     break;
305                 case vetoed:
306                     score = MessageFormat.format(delPattern, Math.abs(change.review.score.getValue()));
307                     break;
308                 default:
309                     score = "" + change.review.score.getValue();
310                 }
311                 String date = df.format(change.date);
312                 sb.append(String.format("| %1$s | %2$s | %3$s | %4$s |\n",
313                         date, name, score, change.review.score.toString()));
314             }
315             sb.append(HARD_BRK);
316         } else if (lastChange.hasComment()) {
317             // comment update
318             sb.append(MessageFormat.format("**{0}** commented on this ticket.", user.getDisplayName()));
319             sb.append(HARD_BRK);
c2188a 320         } else if (lastChange.hasReference()) {
PM 321             // reference update
322             String type = "?";
323
324             switch (lastChange.reference.getSourceType()) {
325                 case Commit: { type = "commit"; } break;
326                 case Ticket: { type = "ticket"; } break;
327                 default: { } break;
328             }
329                 
330             sb.append(MessageFormat.format("**{0}** referenced this ticket in {1} {2}", type, lastChange.toString())); 
331             sb.append(HARD_BRK);
332             
5e3521 333         } else {
JM 334             // general update
335             pattern = "**{0}** has updated this ticket.";
336             sb.append(MessageFormat.format(pattern, user.getDisplayName()));
337             sb.append(HARD_BRK);
338         }
339
340         // ticket link
341         sb.append(MessageFormat.format("[view ticket {0,number,0}]({1})",
342                 ticket.number, ticketService.getTicketUrl(ticket)));
343         sb.append(HARD_BRK);
344
345         if (newTicket) {
346             // ticket title
347             sb.append(MessageFormat.format("### {0}", ticket.title));
348             sb.append(HARD_BRK);
349
350             // ticket description, on state change
351             if (StringUtils.isEmpty(ticket.body)) {
352                 sb.append("<span style=\"color: #888;\">no description entered</span>");
353             } else {
354                 sb.append(ticket.body);
355             }
356             sb.append(HARD_BRK);
357             sb.append(HR);
358         }
359
360         // field changes
361         if (lastChange.hasFieldChanges()) {
362             Map<Field, String> filtered = new HashMap<Field, String>();
363             for (Map.Entry<Field, String> fc : lastChange.fields.entrySet()) {
364                 if (!fieldExclusions.contains(fc.getKey())) {
365                     // field is included
366                     filtered.put(fc.getKey(), fc.getValue());
367                 }
368             }
369
370             // sort by field ordinal
371             List<Field> fields = new ArrayList<Field>(filtered.keySet());
372             Collections.sort(fields);
373
374             if (filtered.size() > 0) {
375                 sb.append(HARD_BRK);
376                 sb.append("| Field Changes               ||\n");
377                 sb.append("| ------------: | :----------- |\n");
378                 for (Field field : fields) {
379                     String value;
380                     if (filtered.get(field) == null) {
381                         value = "";
382                     } else {
383                         value = filtered.get(field).replace("\r\n", "<br/>").replace("\n", "<br/>").replace("|", "&#124;");
384                     }
385                     sb.append(String.format("| **%1$s:** | %2$s |\n", field.name(), value));
386                 }
387                 sb.append(HARD_BRK);
388             }
389         }
390
391         // new comment
392         if (lastChange.hasComment()) {
393             sb.append(HR);
394             sb.append(lastChange.comment.text);
395             sb.append(HARD_BRK);
396         }
397
398         // insert the patchset details and review instructions
399         if (lastChange.hasPatchset() && ticket.isOpen()) {
400             if (commits != null && commits.size() > 0) {
401                 // append the commit list
402                 String title = isFastForward ? "Commits added to previous patchset revision" : "All commits in patchset";
403                 sb.append(MessageFormat.format("| {0} |||\n", title));
404                 sb.append("| SHA | Author | Title |\n");
405                 sb.append("| :-- | :----- | :---- |\n");
406                 for (RevCommit commit : commits) {
407                     sb.append(MessageFormat.format("| {0} | {1} | {2} |\n",
408                             commit.getName(), commit.getAuthorIdent().getName(),
409                             StringUtils.trimString(commit.getShortMessage(), Constants.LEN_SHORTLOG).replace("|", "&#124;")));
410                 }
411                 sb.append(HARD_BRK);
412             }
413
414             if (diffstat != null) {
415                 // append the changed path list
416                 String title = isFastForward ? "Files changed since previous patchset revision" : "All files changed in patchset";
417                 sb.append(MessageFormat.format("| {0} |||\n", title));
418                 sb.append("| :-- | :----------- | :-: |\n");
419                 for (PathChangeModel path : diffstat.paths) {
420                     String add = MessageFormat.format(addPattern, path.insertions);
421                     String del = MessageFormat.format(delPattern, path.deletions);
422                     String diff = null;
423                     switch (path.changeType) {
424                     case ADD:
425                         diff = add;
426                         break;
427                     case DELETE:
428                         diff = del;
429                         break;
430                     case MODIFY:
431                         if (path.insertions > 0 && path.deletions > 0) {
432                             // insertions & deletions
433                             diff = add + "/" + del;
434                         } else if (path.insertions > 0) {
435                             // just insertions
436                             diff = add;
437                         } else {
438                             // just deletions
439                             diff = del;
440                         }
441                         break;
442                     default:
443                         diff = path.changeType.name();
444                         break;
445                     }
446                     sb.append(MessageFormat.format("| {0} | {1} | {2} |\n",
447                             getChangeType(path.changeType), path.name, diff));
448                 }
449                 sb.append(HARD_BRK);
450             }
451
452             sb.append(formatPatchsetInstructions(ticket, lastChange.patchset));
453         }
454
455         return sb.toString();
456     }
457
458     protected String getChangeType(ChangeType type) {
459         String style = null;
460         switch (type) {
461             case ADD:
462                 style = "color:darkgreen;";
463                 break;
464             case COPY:
465                 style = "";
466                 break;
467             case DELETE:
468                 style = "color:darkred;";
469                 break;
470             case MODIFY:
471                 style = "";
472                 break;
473             case RENAME:
474                 style = "";
475                 break;
476             default:
477                 break;
478         }
479         String code = type.name().toUpperCase().substring(0, 1);
480         if (style == null) {
481             return code;
482         } else {
483             return MessageFormat.format("<strong><span style=\"{0}padding:2px;margin:2px;border:1px solid #ddd;\">{1}</span></strong>", style, code);
484         }
485     }
486
487     /**
488      * Generates patchset review instructions for command-line git
489      *
490      * @param patchset
491      * @return instructions
492      */
493     protected String formatPatchsetInstructions(TicketModel ticket, Patchset patchset) {
494         String canonicalUrl = settings.getString(Keys.web.canonicalUrl, "https://localhost:8443");
495         String repositoryUrl = canonicalUrl + Constants.R_PATH + ticket.repository;
496
497         String ticketBranch = Repository.shortenRefName(PatchsetCommand.getTicketBranch(ticket.number));
498         String patchsetBranch  = PatchsetCommand.getPatchsetBranch(ticket.number, patchset.number);
499         String reviewBranch = PatchsetCommand.getReviewBranch(ticket.number);
500
501         String instructions = readResource("commands.md");
502         instructions = instructions.replace("${ticketId}", "" + ticket.number);
503         instructions = instructions.replace("${patchset}", "" + patchset.number);
504         instructions = instructions.replace("${repositoryUrl}", repositoryUrl);
505         instructions = instructions.replace("${ticketRef}", ticketBranch);
506         instructions = instructions.replace("${patchsetRef}", patchsetBranch);
507         instructions = instructions.replace("${reviewBranch}", reviewBranch);
328346 508         instructions = instructions.replace("${ticketBranch}", ticketBranch);
5e3521 509
JM 510         return instructions;
511     }
512
513     /**
514      * Gets the usermodel for the username.  Creates a temp model, if required.
515      *
516      * @param username
517      * @return a usermodel
518      */
519     protected UserModel getUserModel(String username) {
520         UserModel user = userManager.getUserModel(username);
521         if (user == null) {
522             // create a temporary user model (for unit tests)
523             user = new UserModel(username);
524         }
525         return user;
526     }
527
528     /**
529      * Set the proper recipients for a ticket.
530      *
531      * @param ticket
532      * @param mailing
533      */
534     protected void setRecipients(TicketModel ticket, Mailing mailing) {
535         RepositoryModel repository = repositoryManager.getRepositoryModel(ticket.repository);
536
537         //
538         // Direct TO recipients
4261c1 539         // reporter & responsible
5e3521 540         //
4261c1 541         Set<String> tos = new TreeSet<String>();
JM 542         tos.add(ticket.createdBy);
543         if (!StringUtils.isEmpty(ticket.responsible)) {
544             tos.add(ticket.responsible);
545         }
546
5e3521 547         Set<String> toAddresses = new TreeSet<String>();
4261c1 548         for (String name : tos) {
5e3521 549             UserModel user = userManager.getUserModel(name);
d68553 550             if (user != null && !user.disabled) {
5e3521 551                 if (!StringUtils.isEmpty(user.emailAddress)) {
JM 552                     if (user.canView(repository)) {
553                         toAddresses.add(user.emailAddress);
554                     } else {
555                         LoggerFactory.getLogger(getClass()).warn(
556                                 MessageFormat.format("ticket {0}-{1,number,0}: {2} can not receive notification",
557                                         repository.name, ticket.number, user.username));
558                     }
559                 }
560             }
561         }
562
563         //
564         // CC recipients
565         //
566         Set<String> ccs = new TreeSet<String>();
567
4261c1 568         // repository owners
JM 569         if (!ArrayUtils.isEmpty(repository.owners)) {
afbaeb 570             ccs.addAll(repository.owners);
4261c1 571         }
JM 572
5e3521 573         // cc users mentioned in last comment
JM 574         Change lastChange = ticket.changes.get(ticket.changes.size() - 1);
575         if (lastChange.hasComment()) {
576             Pattern p = Pattern.compile("\\s@([A-Za-z0-9-_]+)");
577             Matcher m = p.matcher(lastChange.comment.text);
578             while (m.find()) {
579                 String username = m.group();
580                 ccs.add(username);
581             }
582         }
583
584         // cc users who are watching the ticket
585         ccs.addAll(ticket.getWatchers());
586
587         // TODO cc users who are watching the repository
588
589         Set<String> ccAddresses = new TreeSet<String>();
590         for (String name : ccs) {
591             UserModel user = userManager.getUserModel(name);
d68553 592             if (user != null && !user.disabled) {
5e3521 593                 if (!StringUtils.isEmpty(user.emailAddress)) {
JM 594                     if (user.canView(repository)) {
595                         ccAddresses.add(user.emailAddress);
596                     } else {
597                         LoggerFactory.getLogger(getClass()).warn(
598                                 MessageFormat.format("ticket {0}-{1,number,0}: {2} can not receive notification",
599                                         repository.name, ticket.number, user.username));
600                     }
601                 }
602             }
603         }
604
605         // cc repository mailing list addresses
606         if (!ArrayUtils.isEmpty(repository.mailingLists)) {
607             ccAddresses.addAll(repository.mailingLists);
608         }
609         ccAddresses.addAll(settings.getStrings(Keys.mail.mailingLists));
610
afbaeb 611         // respect the author's email preference
JM 612         UserModel lastAuthor = userManager.getUserModel(lastChange.author);
dac2de 613         if (lastAuthor != null && !lastAuthor.getPreferences().isEmailMeOnMyTicketChanges()) {
afbaeb 614             toAddresses.remove(lastAuthor.emailAddress);
JM 615             ccAddresses.remove(lastAuthor.emailAddress);
616         }
617
618         mailing.setRecipients(toAddresses);
5e3521 619         mailing.setCCs(ccAddresses);
JM 620     }
621
622     protected String readStyle() {
623         StringBuilder sb = new StringBuilder();
624         sb.append("<style>\n");
625         sb.append(readResource("email.css"));
626         sb.append("</style>\n");
627         return sb.toString();
628     }
629
dac2de 630     protected String readViewTicketAction(TicketModel ticket) {
JM 631         String action = readResource("viewTicket.html");
632         action = action.replace("${url}", ticketService.getTicketUrl(ticket));
633         return action;
634     }
635
5e3521 636     protected String readResource(String resource) {
JM 637         StringBuilder sb = new StringBuilder();
638         InputStream is = null;
639         try {
640             is = getClass().getResourceAsStream(resource);
641             List<String> lines = IOUtils.readLines(is);
642             for (String line : lines) {
643                 sb.append(line).append('\n');
644             }
645         } catch (IOException e) {
646
647         } finally {
648             if (is != null) {
649                 try {
650                     is.close();
651                 } catch (IOException e) {
652                 }
653             }
654         }
655         return sb.toString();
656     }
657 }