/** * <a href="http://www.openolat.org"> * OpenOLAT - Online Learning and Training</a><br> * <p> * Licensed under the Apache License, Version 2.0 (the "License"); <br> * you may not use this file except in compliance with the License.<br> * You may obtain a copy of the License at the * <a href="http://www.apache.org/licenses/LICENSE-2.0">Apache homepage</a> * <p> * Unless required by applicable law or agreed to in writing,<br> * software distributed under the License is distributed on an "AS IS" BASIS, <br> * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. <br> * See the License for the specific language governing permissions and <br> * limitations under the License. * <p> * Initial code contributed and copyrighted by<br> * frentix GmbH, http://www.frentix.com * <p> */ package org.olat.core.commons.services.help; import java.util.Date; import java.util.Locale; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import org.apache.http.HttpResponse; import org.apache.http.HttpStatus; import org.apache.http.client.methods.HttpGet; import org.apache.http.impl.client.CloseableHttpClient; import org.apache.http.util.EntityUtils; import org.apache.poi.util.IOUtils; import org.olat.core.gui.UserRequest; import org.olat.core.gui.components.Component; import org.olat.core.gui.components.link.ExternalLink; import org.olat.core.helpers.Settings; import org.olat.core.logging.OLog; import org.olat.core.logging.Tracing; import org.olat.core.util.StringHelper; import org.olat.core.util.httpclient.HttpClientFactory; /** * Helper to create openolat confluence help links. * * Help links have the following form:<br/> * https://confluence.openolat.org/display/OO100DE/OpenOLAT+10+Benutzerhandbuch * * Initial date: 07.01.2015<br> * * @author srosse, stephane.rosse@frentix.com, http://www.frentix.com * */ public class ConfluenceHelper { private static final OLog logger = Tracing.createLoggerFor(ConfluenceHelper.class); private static final Map<String, String> spaces = new ConcurrentHashMap<String, String>(); private static final Map<String, String> translatedPages = new ConcurrentHashMap<String, String>(); private static final Map<String, Date> translatTrials = new ConcurrentHashMap<String, Date>(); private static final String confluenceBaseUrl = "https://confluence.openolat.org"; private static final String confluenceDisplayUrl = confluenceBaseUrl + "/display"; private static final String confluencePagesUrl = confluenceBaseUrl + "/pages/"; public static final Locale EN_Locale = new Locale("en"); public static final Locale DE_Locale = new Locale("de"); public static final String getURL(Locale locale, String page) { StringBuilder sb = new StringBuilder(64); sb.append(confluenceDisplayUrl); String space = spaces.get(locale.toString()); if (space == null) { // Generate space only once per language, version does not change at // runtime String version = Settings.getVersion(); space = generateSpace(version, locale); spaces.putIfAbsent(locale.toString(), space); } sb.append(space); if (page != null) { int anchorPos = page.indexOf("#"); if (anchorPos != -1) { // Page with anchor: real page name + anchor String realPage = page.substring(0, anchorPos); String anchor = page.substring(anchorPos + 1); // Special case for non-en spaces: CustomWare Redirection Plugin // can not redirect pages with anchors. We need to fix it here // by fetching the page and lookup the redirect path. Ugly, but // we see no other option here. if (!locale.getLanguage().equals(EN_Locale.getLanguage())) { String redirectedPage = getPageFromAlias(getURL(locale, realPage)); if (redirectedPage != null) { realPage = redirectedPage; } // else realPage part stays the same - anchor won't work but // at least the right page is loading } // Confluence has some super-fancy way to addressing pages with // anchors sb.append(realPage.replaceAll(" ", "%20")); sb.append("#").append(realPage.replaceAll(" ", "")).append("-").append(anchor); } else { // Page without anchor sb.append(page.replaceAll(" ", "%20")); } } return sb.toString(); } /** * Convert 10.0 -> 100<br/> * Convert 10.1.1 -> 101 * * * @param version * @param locale * @return */ protected static final String generateSpace(String version, Locale locale) { StringBuilder sb = new StringBuilder(); sb.append("/OO"); int firstPointIndex = version.indexOf('.'); if (firstPointIndex > 0) { sb.append(version.substring(0, firstPointIndex)); int secondPointIndex = version.indexOf('.', firstPointIndex + 1); if (secondPointIndex > firstPointIndex) { sb.append(version.substring(firstPointIndex + 1, secondPointIndex)); } else if (firstPointIndex + 1 < version.length()) { String subVersion = version.substring(firstPointIndex + 1); char[] subVersionArr = subVersion.toCharArray(); for (int i = 0; i < subVersionArr.length && Character.isDigit(subVersionArr[i]); i++) { sb.append(subVersionArr[i]); } } else { sb.append("0"); } } else { char[] versionArr = version.toCharArray(); for (int i = 0; i < versionArr.length && Character.isDigit(versionArr[i]); i++) { sb.append(versionArr[i]); } // add minor version sb.append("0"); } if (locale.getLanguage().equals(DE_Locale.getLanguage())) { sb.append("DE/"); } else { sb.append("EN/"); } return sb.toString(); } public static final Component createHelpPageLink(UserRequest ureq, String title, String tooltip, String iconCSS, String elementCSS, String page) { ExternalLink helpLink = new ExternalLink("topnav.help." + page); helpLink.setName(title); helpLink.setTooltip(tooltip); helpLink.setIconLeftCSS(iconCSS); helpLink.setElementCssClass(elementCSS); helpLink.setTarget("oohelp"); helpLink.setUrl(getURL(ureq.getLocale(), page)); return helpLink; } /** * Fetch the redirected page name for the given URL. Note that this is * executed asynchronously, meaning that the first time this method is * executed for a certain URL it will return null. As soon as the code could * get the redirection from the confluence server it will return the * redirected page name instead. * * @param aliasUrl * @return The translated page name or NULL if not found */ private static final String getPageFromAlias(String aliasUrl) { if (StringHelper.containsNonWhitespace(aliasUrl)) { String translatedPage = translatedPages.get(aliasUrl); if (translatedPage != null) { return translatedPage; } // Not in cache. Start a background thread to fetch the translated // page from the confluence. Since this can take several seconds, we // exit here with null. Next time the page is loaded the translated // page will be in the cache. // Do this only once per 30 mins per page. Confluence might be down // or another user already trigger the fetch. Date lastTrial = translatTrials.get(aliasUrl); Date now = new Date(); if (lastTrial == null || lastTrial.getTime() < (now.getTime() - (1800 * 1000))) { translatTrials.put(aliasUrl, now); new Thread() { public void run() { CloseableHttpClient httpClient = HttpClientFactory.getHttpClientInstance(false); try { // Phase 1: lookup alias redirect HttpGet httpMethod = new HttpGet(aliasUrl); httpMethod.setHeader("User-Agent", Settings.getFullVersionInfo()); HttpResponse response = httpClient.execute(httpMethod); int httpStatusCode = response.getStatusLine().getStatusCode(); // Looking at the HTTP status code tells us whether a // user with the given MSN name exists. if (httpStatusCode == HttpStatus.SC_OK) { String body = EntityUtils.toString(response.getEntity()); // Page contains a javascript redirect call, extract // redirect location int locationPos = body.indexOf("location.replace('"); if (locationPos == -1) { return; } int endPos = body.indexOf("'", locationPos + 18); if (endPos == -1) { return; } // Remove the path to extract the page name String path = body.substring(locationPos + 18, endPos); String translatedPage = path.substring(path.lastIndexOf("/") + 1); translatedPage = translatedPage.replaceAll("\\+", " "); // Phase 2:Lookup real page name in confluence // if this just a stupid confluence page ID // instead of the page name. This totally breaks // the anchor mechanism which is broken anyway, // we need the real page name. For some reason // confluence does not always adress pages using // the page name, sometimes the page ID is used. // Anchors do not work on such pages. For iso // latin pages this should be fine for most // cases. if (translatedPage.indexOf("viewpage.action?pageId") != -1) { String redirectUrl = confluencePagesUrl + translatedPage; httpMethod = new HttpGet(redirectUrl); httpMethod.setHeader("User-Agent", Settings.getFullVersionInfo()); response = httpClient.execute(httpMethod); httpStatusCode = response.getStatusLine().getStatusCode(); // Looking at the HTTP status code tells us whether a // user with the given MSN name exists. if (httpStatusCode == HttpStatus.SC_OK) { body = EntityUtils.toString(response.getEntity()); // Page contains a javascript redirect call, extract // redirect location int titlePos = body.indexOf("ajs-page-title"); if (titlePos == -1) { return; } endPos = body.indexOf("\"", titlePos + 25); if (endPos == -1) { return; } // Remove the path to extract the page name path = body.substring(titlePos + 25, endPos); translatedPage = path.substring(path.lastIndexOf("/") + 1); translatedPage = translatedPage.replaceAll("\\+", " "); // Check if this just a stupid page ID instead // of the page name. This totally breaks the // anchor stuff, we need the real page name. For // iso latin pages this should be fine for most cases. } } // We're done. Put to cache for next retrieval translatedPages.putIfAbsent(aliasUrl, translatedPage); } } catch (Exception e) { logger.warn("Error while getting help page from EN alias", e); } finally { IOUtils.closeQuietly(httpClient); } } }.start(); } return null; } return null; } }