diff --git a/src/main/java/org/olat/core/commons/services/help/spi/ConfluenceLinkSPI.java b/src/main/java/org/olat/core/commons/services/help/spi/ConfluenceLinkSPI.java index 4b5754ba5a6182b5460910237ab32c5b148d85c7..188180a813a3535a6446fb221c503645a55512bc 100644 --- a/src/main/java/org/olat/core/commons/services/help/spi/ConfluenceLinkSPI.java +++ b/src/main/java/org/olat/core/commons/services/help/spi/ConfluenceLinkSPI.java @@ -19,8 +19,17 @@ */ package org.olat.core.commons.services.help.spi; +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.admin.user.tools.UserTool; import org.olat.core.commons.services.help.HelpLinkSPI; import org.olat.core.gui.UserRequest; @@ -29,6 +38,10 @@ import org.olat.core.gui.components.link.ExternalLink; import org.olat.core.gui.components.velocity.VelocityContainer; import org.olat.core.gui.control.WindowControl; 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; import org.springframework.stereotype.Service; /** @@ -37,12 +50,21 @@ import org.springframework.stereotype.Service; * 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 * */ @Service("ooConfluenceLinkHelp") public class ConfluenceLinkSPI implements HelpLinkSPI { - + private static final OLog logger = Tracing.createLoggerFor(ConfluenceLinkSPI.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>(); + + public static final Locale EN_Locale = new Locale("en"); + public static final Locale DE_Locale = new Locale("de"); + @Override public UserTool getHelpUserTool(WindowControl wControl) { return new ConfluenceUserTool(); @@ -52,21 +74,43 @@ public class ConfluenceLinkSPI implements HelpLinkSPI { public String getURL(Locale locale, String page) { StringBuilder sb = new StringBuilder(64); sb.append("https://confluence.openolat.org/display"); - String version = Settings.getVersion(); - sb.append(generateSpace(version, locale)); + 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 - String realPage = page.substring(0,anchorPos); - String anchor = page.substring(anchorPos+1); - // confluence has some super-fancy way to addressing pages with anchors - sb.append(realPage.replace(" ", "%20")); - sb.append("#").append(realPage.replace(" ", "")).append("-").append(anchor); - + // 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.replace(" ", "%20")); + // Page without anchor + sb.append(page.replaceAll(" ", "%20")); } } return sb.toString(); @@ -84,17 +128,17 @@ public class ConfluenceLinkSPI implements HelpLinkSPI { protected String generateSpace(String version, Locale locale) { StringBuilder sb = new StringBuilder(); sb.append("/OO"); - + int firstPointIndex = version.indexOf('.'); - if(firstPointIndex > 0) { + 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()) { + 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++) { + for (int i = 0; i < subVersionArr.length && Character.isDigit(subVersionArr[i]); i++) { sb.append(subVersionArr[i]); } } else { @@ -102,22 +146,22 @@ public class ConfluenceLinkSPI implements HelpLinkSPI { } } else { char[] versionArr = version.toCharArray(); - for(int i=0; i<versionArr.length && Character.isDigit(versionArr[i]); i++) { + for (int i = 0; i < versionArr.length && Character.isDigit(versionArr[i]); i++) { sb.append(versionArr[i]); } - //add minor version + // add minor version sb.append("0"); } - - if(locale.getLanguage().equals(new Locale("de").getLanguage())) { + + if (locale.getLanguage().equals(DE_Locale.getLanguage())) { sb.append("DE/"); } else { sb.append("EN/"); } - + return sb.toString(); } - + public class ConfluenceUserTool implements UserTool { @Override @@ -139,7 +183,8 @@ public class ConfluenceLinkSPI implements HelpLinkSPI { } @Override - public Component getHelpPageLink(UserRequest ureq, String title, String tooltip, String iconCSS, String elementCSS, String page) { + public Component getHelpPageLink(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); @@ -149,5 +194,74 @@ public class ConfluenceLinkSPI implements HelpLinkSPI { 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 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 { + 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("\\+", " "); + // 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; + } } diff --git a/src/main/java/org/olat/core/helpers/Settings.java b/src/main/java/org/olat/core/helpers/Settings.java index 55eef9d59b6e460a970037a1cff65badb498cb99..e674c3a62dafff8c8c0893a7a54832b9f066af8f 100644 --- a/src/main/java/org/olat/core/helpers/Settings.java +++ b/src/main/java/org/olat/core/helpers/Settings.java @@ -102,7 +102,7 @@ public class Settings implements Initializable, Destroyable, GenericEventListene // fxdiff: only set build id from build date if none is provided in olat.local.properties! private static void setBuildIdFromBuildDate() { SimpleDateFormat formatter = new SimpleDateFormat("yyyyMMdd"); - buildIdentifier = formatter.format(buildDate); + buildIdentifier = formatter.format(getBuildDate()); } // fxdiff: only set build date diff --git a/src/test/java/org/olat/core/commons/services/help/spi/ConfluenceLinkSPITest.java b/src/test/java/org/olat/core/commons/services/help/spi/ConfluenceLinkSPITest.java index 71ab0687a7f67f11c5c03a37cb98f64025540fa0..e68772670e17335bf330d0af7c0f1e1ce0077efc 100644 --- a/src/test/java/org/olat/core/commons/services/help/spi/ConfluenceLinkSPITest.java +++ b/src/test/java/org/olat/core/commons/services/help/spi/ConfluenceLinkSPITest.java @@ -23,6 +23,7 @@ import java.util.Locale; import org.junit.Assert; import org.junit.Test; +import org.olat.core.helpers.SettingsTest; /** * @@ -52,4 +53,53 @@ public class ConfluenceLinkSPITest { Assert.assertNotNull(url4); Assert.assertTrue(url4.startsWith("/OO110EN/")); } + + @Test + public void getUrl() { + // init settings to set version, required by ConfluenceLinkSPI + SettingsTest.createHttpDefaultPortSettings(); + + ConfluenceLinkSPI linkSPI = new ConfluenceLinkSPI(); + //Data%20Management#DataManagement-qb_import + // Standard Case in English + String url1 = linkSPI.getURL(Locale.ENGLISH, "Data Management"); + Assert.assertNotNull(url1); + Assert.assertTrue(url1.endsWith("Data%20Management")); + + // Special handing for anchors in confluence + String url2 = linkSPI.getURL(Locale.ENGLISH, "Data Management#qb_import"); + Assert.assertNotNull(url2); + Assert.assertTrue(url2.endsWith("Data%20Management#DataManagement-qb_import")); + } + + @Test + public void getTranslatedUrl() { + // init settings to set version, required by ConfluenceLinkSPI + SettingsTest.createHttpDefaultPortSettings(); + + ConfluenceLinkSPI linkSPI = new ConfluenceLinkSPI(); + // Standard Case in German - same as in english + String url1 = linkSPI.getURL(Locale.GERMAN, "Data Management"); + Assert.assertNotNull(url1); + Assert.assertTrue(url1.endsWith("Data%20Management")); + + // Special handing for anchors in confluence + // Here some magic is needed since the CustomWare Redirection Plugin + // plugin we use in Confluence can not redirec links with anchors. The + // anchor is deleted. + // We have to translate this here + // First time it won't return the translated link as it does the translation asynchronously in a separate thread to not block the UI + String url2 = linkSPI.getURL(Locale.GERMAN, "Data Management#qb_import"); + Assert.assertNotNull(url2); + Assert.assertTrue(url2.endsWith("Data%20Management#DataManagement-qb_import")); + // Wait 5secs and try it again, should be translated now + try { + Thread.sleep(5000); + } catch (InterruptedException e) { + e.printStackTrace(); + } + url2 = linkSPI.getURL(Locale.GERMAN, "Data Management#qb_import"); + Assert.assertNotNull(url2); + Assert.assertTrue(url2.endsWith("Handhabung%20der%20Daten#HandhabungderDaten-qb_import")); + } } diff --git a/src/test/java/org/olat/core/helpers/SettingsTest.java b/src/test/java/org/olat/core/helpers/SettingsTest.java index 847ff686e12ec71bcfc14064b4444f462ea24867..f75469f297b12e2bc015b2ed0babbd761dc40876 100644 --- a/src/test/java/org/olat/core/helpers/SettingsTest.java +++ b/src/test/java/org/olat/core/helpers/SettingsTest.java @@ -140,27 +140,27 @@ public class SettingsTest { assertEquals("other :port appended.",expectedValue, serverUriScheme); } - private Settings createHttpDefaultPortSettings(){ + public static Settings createHttpDefaultPortSettings(){ Settings settings = createCommonSettingsForPortTests(0, SettingsTest.httpDefaultPort); return settings; } - private Settings createHttpOtherPortSettings(){ + public static Settings createHttpOtherPortSettings(){ Settings settings = createCommonSettingsForPortTests(0, SettingsTest.httpOtherPort); return settings; } - private Settings createHttpsDefaultPortSettings(){ + public static Settings createHttpsDefaultPortSettings(){ Settings settings = createCommonSettingsForPortTests(SettingsTest.httpsDefaultPort, 0); return settings; } - private Settings createHttpsOtherPortSettings(){ + public static Settings createHttpsOtherPortSettings(){ Settings settings = createCommonSettingsForPortTests(SettingsTest.httpsOtherPort, 0); return settings; } - private Settings createCommonSettingsForPortTests(int securePort, int insecurePort){ + public static Settings createCommonSettingsForPortTests(int securePort, int insecurePort){ Settings settings = new Settings(); PersistedProperties persistedPropertiesHttp = new PersistedProperties(new DummyListener()); Properties defaultPropertiesHttp = new Properties(); @@ -170,6 +170,12 @@ public class SettingsTest { settings.setServerSecurePort(securePort); settings.setServerInsecurePort(insecurePort); settings.setServerDomainName(SettingsTest.serverFqnd);//${server.domainname} + if (settings.getVersion() == null) { + // used by ConfluenceLinkSPITest + settings.setVersion("10.4"); + } + // used by ConfluenceLinkSPITest + settings.setApplicationName("OpenOLAT-jUnit-runner"); WebappHelper.setServletContextPath(SettingsTest.contextPath); return settings; }