| // Copyright 2012 Google Inc. All Rights Reserved. | |
| // | |
| // Licensed under the Apache License, Version 2.0 (the "License"); | |
| // you may not use this file except in compliance with the License. | |
| // You may obtain a copy of the License at | |
| // | |
| // http://www.apache.org/licenses/LICENSE-2.0 | |
| // | |
| // Unless required by applicable law or agreed to in writing, software | |
| // distributed under the License is distributed on an "AS IS" BASIS, | |
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
| // See the License for the specific language governing permissions and | |
| // limitations under the License. | |
| package com.google.gitiles; | |
| import static com.google.common.base.Preconditions.checkNotNull; | |
| import com.google.common.base.Joiner; | |
| import com.google.common.base.Strings; | |
| import com.google.common.collect.ImmutableMap; | |
| import com.google.common.collect.Lists; | |
| import java.util.ArrayList; | |
| import java.util.Collections; | |
| import java.util.List; | |
| import java.util.Map; | |
| import java.util.regex.Matcher; | |
| import java.util.regex.Pattern; | |
| import java.util.regex.PatternSyntaxException; | |
| import javax.servlet.http.HttpServletRequest; | |
| import org.eclipse.jgit.lib.Config; | |
| import org.slf4j.Logger; | |
| import org.slf4j.LoggerFactory; | |
| /** Linkifier for blocks of text such as commit message descriptions. */ | |
| public class Linkifier { | |
| private static final Logger log = LoggerFactory.getLogger(Linkifier.class); | |
| private static final String COMMENTLINK = "commentlink"; | |
| private static final Pattern HTTP_URL_PATTERN; | |
| private static final Pattern CHANGE_ID_PATTERN; | |
| static { | |
| // HTTP URL regex adapted from com.google.gwtexpui.safehtml.client.SafeHtml. | |
| String part = "(?:" + "[a-zA-Z0-9$_.+!*',%;:@=?#/~<>-]" + "|&(?!lt;|gt;)" + ")"; | |
| String httpUrl = "https?://" + part + "{2,}" + "(?:[(]" + part + "*" + "[)])*" + part + "*"; | |
| HTTP_URL_PATTERN = Pattern.compile(httpUrl); | |
| CHANGE_ID_PATTERN = Pattern.compile("(\\bI[0-9a-f]{8,40}\\b)"); | |
| } | |
| private final GitilesUrls urls; | |
| private final List<CommentLinkInfo> commentLinks; | |
| private final Pattern allPattern; | |
| public Linkifier(GitilesUrls urls, Config config) { | |
| this.urls = checkNotNull(urls, "urls"); | |
| List<CommentLinkInfo> list = new ArrayList<>(); | |
| list.add(new CommentLinkInfo(HTTP_URL_PATTERN, "$0")); | |
| List<String> patterns = new ArrayList<>(); | |
| patterns.add(HTTP_URL_PATTERN.pattern()); | |
| patterns.add(CHANGE_ID_PATTERN.pattern()); | |
| for (String subsection : config.getSubsections(COMMENTLINK)) { | |
| String match = config.getString(COMMENTLINK, subsection, "match"); | |
| String link = config.getString(COMMENTLINK, subsection, "link"); | |
| String html = config.getString(COMMENTLINK, subsection, "html"); | |
| if (html != null) { | |
| log.warn( | |
| "Beware: html in commentlinks is unsupported in gitiles; " | |
| + "Did you copy it from a gerrit config?"); | |
| } | |
| if (Strings.isNullOrEmpty(match)) { | |
| log.warn("invalid commentlink.{}.match", subsection); | |
| continue; | |
| } | |
| Pattern pattern; | |
| try { | |
| pattern = Pattern.compile(match); | |
| } catch (PatternSyntaxException ex) { | |
| String msg = "invalid commentlink." + subsection + ".match"; | |
| if (log.isDebugEnabled()) { | |
| log.debug(msg, ex); | |
| } else { | |
| log.warn("{}: {}", msg, ex.getMessage()); | |
| } | |
| continue; | |
| } | |
| if (Strings.isNullOrEmpty(link)) { | |
| log.warn("invalid commentlink.{}.link", subsection); | |
| continue; | |
| } | |
| list.add(new CommentLinkInfo(pattern, link)); | |
| patterns.add(match); | |
| } | |
| this.commentLinks = Collections.unmodifiableList(list); | |
| allPattern = Pattern.compile(Joiner.on('|').join(patterns)); | |
| } | |
| public List<Map<String, String>> linkify(HttpServletRequest req, String message) { | |
| List<CommentLinkInfo> operationalCommentLinks = new ArrayList<>(commentLinks); | |
| // Because we're relying on 'req' as a dynamic parameter, we need to construct | |
| // the CommentLinkInfo for ChangeIds on the fly. | |
| String baseGerritUrl = urls.getBaseGerritUrl(req); | |
| if (baseGerritUrl != null) { | |
| CommentLinkInfo changeIds = new CommentLinkInfo(CHANGE_ID_PATTERN, baseGerritUrl + "#/q/$0"); | |
| operationalCommentLinks.add(changeIds); | |
| } | |
| List<Map<String, String>> parsed = Lists.newArrayList(); | |
| parsed.add(ImmutableMap.of("text", message)); | |
| for (int index = 0; index < parsed.size(); index++) { | |
| if (parsed.get(index).get("url") != null) { | |
| continue; | |
| } | |
| Matcher m = allPattern.matcher(parsed.get(index).get("text")); | |
| if (!m.find()) { | |
| continue; | |
| } | |
| for (CommentLinkInfo cli : operationalCommentLinks) { | |
| // No need to apply more rules if this is already a link. | |
| if (parsed.get(index).get("url") != null) { | |
| break; | |
| } | |
| String text = parsed.get(index).get("text"); | |
| parsed.remove(index); | |
| parsed.addAll(index, cli.linkify(text)); | |
| } | |
| } | |
| return parsed; | |
| } | |
| } |