blob: 493e1c592533993265f0f66a3b6ee9a750521060 [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;
Stefan Beller6307b3d2014-11-13 17:53:03 -080023import java.util.ArrayList;
24import java.util.Collections;
Dave Borowitz9de65952012-08-13 16:09:45 -070025import java.util.List;
26import java.util.Map;
27import java.util.regex.Matcher;
28import java.util.regex.Pattern;
Nodir Turakulov56a226e2015-08-28 09:49:31 -070029import java.util.regex.PatternSyntaxException;
Dave Borowitz9de65952012-08-13 16:09:45 -070030import javax.servlet.http.HttpServletRequest;
Dave Borowitz3b744b12016-08-19 16:11:10 -040031import org.eclipse.jgit.lib.Config;
32import org.slf4j.Logger;
33import org.slf4j.LoggerFactory;
Dave Borowitz9de65952012-08-13 16:09:45 -070034
35/** Linkifier for blocks of text such as commit message descriptions. */
36public class Linkifier {
Stefan Beller6307b3d2014-11-13 17:53:03 -080037 private static final Logger log = LoggerFactory.getLogger(Linkifier.class);
38
39 private static final String COMMENTLINK = "commentlink";
40 private static final Pattern HTTP_URL_PATTERN;
41 private static final Pattern CHANGE_ID_PATTERN;
Dave Borowitz9de65952012-08-13 16:09:45 -070042
43 static {
44 // HTTP URL regex adapted from com.google.gwtexpui.safehtml.client.SafeHtml.
Han-Wen Nienhuysc0200f62016-05-02 17:34:51 +020045 String part = "(?:" + "[a-zA-Z0-9$_.+!*',%;:@=?#/~<>-]" + "|&(?!lt;|gt;)" + ")";
46 String httpUrl = "https?://" + part + "{2,}" + "(?:[(]" + part + "*" + "[)])*" + part + "*";
Stefan Beller6307b3d2014-11-13 17:53:03 -080047 HTTP_URL_PATTERN = Pattern.compile(httpUrl);
48 CHANGE_ID_PATTERN = Pattern.compile("(\\bI[0-9a-f]{8,40}\\b)");
Dave Borowitz9de65952012-08-13 16:09:45 -070049 }
50
51 private final GitilesUrls urls;
Stefan Beller6307b3d2014-11-13 17:53:03 -080052 private final List<CommentLinkInfo> commentLinks;
53 private final Pattern allPattern;
Dave Borowitz9de65952012-08-13 16:09:45 -070054
Stefan Beller6307b3d2014-11-13 17:53:03 -080055 public Linkifier(GitilesUrls urls, Config config) {
Dave Borowitz9de65952012-08-13 16:09:45 -070056 this.urls = checkNotNull(urls, "urls");
Stefan Beller6307b3d2014-11-13 17:53:03 -080057
58 List<CommentLinkInfo> list = new ArrayList<>();
59 list.add(new CommentLinkInfo(HTTP_URL_PATTERN, "$0"));
Stefan Beller6307b3d2014-11-13 17:53:03 -080060
61 List<String> patterns = new ArrayList<>();
Stefan Beller6307b3d2014-11-13 17:53:03 -080062 patterns.add(HTTP_URL_PATTERN.pattern());
63 patterns.add(CHANGE_ID_PATTERN.pattern());
64
Nodir Turakulov56a226e2015-08-28 09:49:31 -070065 for (String subsection : config.getSubsections(COMMENTLINK)) {
Stefan Beller6307b3d2014-11-13 17:53:03 -080066 String match = config.getString("commentlink", subsection, "match");
67 String link = config.getString("commentlink", subsection, "link");
68 String html = config.getString("commentlink", subsection, "html");
69 if (html != null) {
Han-Wen Nienhuysc0200f62016-05-02 17:34:51 +020070 log.warn(
71 "Beware: html in commentlinks is unsupported in gitiles; "
72 + "Did you copy it from a gerrit config?");
Stefan Beller6307b3d2014-11-13 17:53:03 -080073 }
74 if (Strings.isNullOrEmpty(match)) {
75 log.warn("invalid commentlink.%s.match", subsection);
76 continue;
77 }
Nodir Turakulov56a226e2015-08-28 09:49:31 -070078 Pattern pattern;
79 try {
80 pattern = Pattern.compile(match);
Han-Wen Nienhuysc0200f62016-05-02 17:34:51 +020081 } catch (PatternSyntaxException ex) {
David Pursehouse0a7cae72017-02-01 13:41:53 +090082 String msg = "invalid commentlink." + subsection + ".match";
83 if (log.isDebugEnabled()) {
84 log.debug(msg, ex);
85 } else {
86 log.warn(msg + ": " + ex.getMessage());
87 }
Nodir Turakulov56a226e2015-08-28 09:49:31 -070088 continue;
89 }
Stefan Beller6307b3d2014-11-13 17:53:03 -080090 if (Strings.isNullOrEmpty(link)) {
91 log.warn("invalid commentlink.%s.link", subsection);
92 continue;
93 }
Stefan Beller6307b3d2014-11-13 17:53:03 -080094 list.add(new CommentLinkInfo(pattern, link));
95 patterns.add(match);
96 }
97 this.commentLinks = Collections.unmodifiableList(list);
98 allPattern = Pattern.compile(Joiner.on('|').join(patterns));
Dave Borowitz9de65952012-08-13 16:09:45 -070099 }
100
101 public List<Map<String, String>> linkify(HttpServletRequest req, String message) {
Stefan Beller6307b3d2014-11-13 17:53:03 -0800102
103 List<CommentLinkInfo> operationalCommentLinks = new ArrayList<>(commentLinks);
104 // Because we're relying on 'req' as a dynamic parameter, we need to construct
105 // the CommentLinkInfo for ChangeIds on the fly.
Dave Borowitz9de65952012-08-13 16:09:45 -0700106 String baseGerritUrl = urls.getBaseGerritUrl(req);
Stefan Beller6307b3d2014-11-13 17:53:03 -0800107
108 if (baseGerritUrl != null) {
David Pursehouseccaa85d2017-05-30 10:47:27 +0900109 CommentLinkInfo changeIds = new CommentLinkInfo(CHANGE_ID_PATTERN, baseGerritUrl + "#/q/$0");
Stefan Beller6307b3d2014-11-13 17:53:03 -0800110 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}