blob: 1a49a41683a944ecd15bce44d036b1f174d20c81 [file] [log] [blame]
Dave Borowitz9de65952012-08-13 16:09:45 -07001// Copyright 2012 Google Inc. All Rights Reserved.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7// http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15package com.google.gitiles;
16
17import static com.google.common.base.Preconditions.checkNotNull;
18
19import com.google.common.base.Joiner;
Stefan Beller6307b3d2014-11-13 17:53:03 -080020import com.google.common.base.Strings;
Dave Borowitz9de65952012-08-13 16:09:45 -070021import com.google.common.collect.ImmutableMap;
22import com.google.common.collect.Lists;
23
Stefan Beller6307b3d2014-11-13 17:53:03 -080024import org.eclipse.jgit.lib.Config;
25import org.slf4j.Logger;
26import org.slf4j.LoggerFactory;
27
28import java.util.ArrayList;
29import java.util.Collections;
Dave Borowitz9de65952012-08-13 16:09:45 -070030import java.util.List;
31import java.util.Map;
Stefan Beller6307b3d2014-11-13 17:53:03 -080032import java.util.Set;
Dave Borowitz9de65952012-08-13 16:09:45 -070033import java.util.regex.Matcher;
34import java.util.regex.Pattern;
35
36import javax.servlet.http.HttpServletRequest;
37
38/** Linkifier for blocks of text such as commit message descriptions. */
39public class Linkifier {
Stefan Beller6307b3d2014-11-13 17:53:03 -080040 private static final Logger log = LoggerFactory.getLogger(Linkifier.class);
41
42 private static final String COMMENTLINK = "commentlink";
43 private static final Pattern HTTP_URL_PATTERN;
44 private static final Pattern CHANGE_ID_PATTERN;
Dave Borowitz9de65952012-08-13 16:09:45 -070045
46 static {
47 // HTTP URL regex adapted from com.google.gwtexpui.safehtml.client.SafeHtml.
Dave Borowitz5871ac82013-03-21 12:14:28 -070048 String part = "(?:" +
49 "[a-zA-Z0-9$_.+!*',%;:@=?#/~<>-]" +
50 "|&(?!lt;|gt;)" +
51 ")";
Dave Borowitz9de65952012-08-13 16:09:45 -070052 String httpUrl = "https?://" +
53 part + "{2,}" +
54 "(?:[(]" + part + "*" + "[)])*" +
55 part + "*";
Stefan Beller6307b3d2014-11-13 17:53:03 -080056 HTTP_URL_PATTERN = Pattern.compile(httpUrl);
57 CHANGE_ID_PATTERN = Pattern.compile("(\\bI[0-9a-f]{8,40}\\b)");
Dave Borowitz9de65952012-08-13 16:09:45 -070058 }
59
60 private final GitilesUrls urls;
Stefan Beller6307b3d2014-11-13 17:53:03 -080061 private final List<CommentLinkInfo> commentLinks;
62 private final Pattern allPattern;
Dave Borowitz9de65952012-08-13 16:09:45 -070063
Stefan Beller6307b3d2014-11-13 17:53:03 -080064 public Linkifier(GitilesUrls urls, Config config) {
Dave Borowitz9de65952012-08-13 16:09:45 -070065 this.urls = checkNotNull(urls, "urls");
Stefan Beller6307b3d2014-11-13 17:53:03 -080066
67 List<CommentLinkInfo> list = new ArrayList<>();
68 list.add(new CommentLinkInfo(HTTP_URL_PATTERN, "$0"));
69 Set<String> subsections = config.getSubsections(COMMENTLINK);
70
71 List<String> patterns = new ArrayList<>();
72
73 patterns.add(HTTP_URL_PATTERN.pattern());
74 patterns.add(CHANGE_ID_PATTERN.pattern());
75
76 for (String subsection : subsections) {
77 String match = config.getString("commentlink", subsection, "match");
78 String link = config.getString("commentlink", subsection, "link");
79 String html = config.getString("commentlink", subsection, "html");
80 if (html != null) {
81 log.warn("Beware: html in commentlinks is unsupported in gitiles; "
82 + "Did you copy it from a gerrit config?");
83 }
84 if (Strings.isNullOrEmpty(match)) {
85 log.warn("invalid commentlink.%s.match", subsection);
86 continue;
87 }
88 if (Strings.isNullOrEmpty(link)) {
89 log.warn("invalid commentlink.%s.link", subsection);
90 continue;
91 }
92 Pattern pattern = Pattern.compile(match);
93 list.add(new CommentLinkInfo(pattern, link));
94 patterns.add(match);
95 }
96 this.commentLinks = Collections.unmodifiableList(list);
97 allPattern = Pattern.compile(Joiner.on('|').join(patterns));
Dave Borowitz9de65952012-08-13 16:09:45 -070098 }
99
100 public List<Map<String, String>> linkify(HttpServletRequest req, String message) {
Stefan Beller6307b3d2014-11-13 17:53:03 -0800101
102 List<CommentLinkInfo> operationalCommentLinks = new ArrayList<>(commentLinks);
103 // Because we're relying on 'req' as a dynamic parameter, we need to construct
104 // the CommentLinkInfo for ChangeIds on the fly.
Dave Borowitz9de65952012-08-13 16:09:45 -0700105 String baseGerritUrl = urls.getBaseGerritUrl(req);
Stefan Beller6307b3d2014-11-13 17:53:03 -0800106
107 if (baseGerritUrl != null) {
108 CommentLinkInfo changeIds =
109 new CommentLinkInfo(CHANGE_ID_PATTERN, baseGerritUrl + "#/q/$0,n,z");
110 operationalCommentLinks.add(changeIds);
111 }
112
Dave Borowitz9de65952012-08-13 16:09:45 -0700113 List<Map<String, String>> parsed = Lists.newArrayList();
Stefan Beller6307b3d2014-11-13 17:53:03 -0800114 parsed.add(ImmutableMap.of("text", message));
115
116 for (int index = 0; index < parsed.size(); index++) {
117 if (parsed.get(index).get("url") != null) {
118 continue;
119 }
120 Matcher m = allPattern.matcher(parsed.get(index).get("text"));
121 if (!m.find()) {
122 continue;
123 }
124
125 for (CommentLinkInfo cli : operationalCommentLinks) {
126 // No need to apply more rules if this is already a link.
127 if (parsed.get(index).get("url") != null) {
128 break;
Dave Borowitz9de65952012-08-13 16:09:45 -0700129 }
Stefan Beller6307b3d2014-11-13 17:53:03 -0800130 String text = parsed.get(index).get("text");
131 parsed.remove(index);
132 parsed.addAll(index, cli.linkify(text));
Dave Borowitz9de65952012-08-13 16:09:45 -0700133 }
Dave Borowitz9de65952012-08-13 16:09:45 -0700134 }
Dave Borowitz9de65952012-08-13 16:09:45 -0700135 return parsed;
136 }
Dave Borowitz9de65952012-08-13 16:09:45 -0700137}