/** * OLAT - Online Learning and Training<br> * http://www.olat.org * <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 * <p> * http://www.apache.org/licenses/LICENSE-2.0 * <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> * Copyright (c) since 2004 at Multimedia- & E-Learning Services (MELS),<br> * University of Zurich, Switzerland. * <hr> * <a href="http://www.openolat.org"> * OpenOLAT - Online Learning and Training</a><br> * This file has been modified by the OpenOLAT community. Changes are licensed * under the Apache 2.0 license as the original file. * <p> */ package org.olat.core.gui.render.velocity; import java.io.Closeable; import java.io.IOException; import java.util.Collection; import java.util.Date; import java.util.HashMap; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.UUID; import org.apache.commons.lang.StringEscapeUtils; import org.olat.core.CoreSpringFactory; import org.olat.core.commons.services.help.HelpModule; import org.olat.core.gui.components.Component; import org.olat.core.gui.components.form.flexible.FormItem; import org.olat.core.gui.components.form.flexible.impl.NameValuePair; import org.olat.core.gui.components.velocity.VelocityContainer; import org.olat.core.gui.control.winmgr.AJAXFlags; import org.olat.core.gui.render.Renderer; import org.olat.core.gui.render.StringOutput; import org.olat.core.gui.render.StringOutputPool; import org.olat.core.gui.translator.PackageTranslator; import org.olat.core.gui.translator.Translator; import org.olat.core.gui.util.CSSHelper; import org.olat.core.helpers.Settings; import org.olat.core.util.ArrayHelper; import org.olat.core.util.CodeHelper; import org.olat.core.util.Formatter; import org.olat.core.util.StringHelper; import org.olat.core.util.WebappHelper; import org.olat.core.util.filter.Filter; import org.olat.core.util.filter.FilterFactory; import org.olat.core.util.filter.impl.OWASPAntiSamyXSSFilter; import org.olat.core.util.i18n.I18nManager; import org.olat.core.util.i18n.I18nModule; /** * @author Felix Jost */ public class VelocityRenderDecorator implements Closeable { private VelocityComponent vc; private Renderer renderer; private final boolean isIframePostEnabled; private StringOutput target; private HelpModule helpModule; /** * @param renderer * @param vc */ public VelocityRenderDecorator(Renderer renderer, VelocityComponent vc, StringOutput target) { this.renderer = renderer; this.vc = vc; this.target = target; this.isIframePostEnabled = renderer.getGlobalSettings().getAjaxFlags().isIframePostEnabled(); this.helpModule = CoreSpringFactory.getImpl(HelpModule.class); } @Override public void close() throws IOException { vc = null; target = null; renderer = null; } /** * * @param prefix e.g. abc for "abc647547326" and so on * @return an prefixed id (e.g. f23748932) which is unique in the current render tree. * */ public StringOutput getId(String prefix) { StringOutput sb = new StringOutput(16); sb.append("o_s").append(prefix).append(vc.getDispatchID()); return sb; } public static String getId(String prefix, VelocityContainer vc) { StringOutput sb = StringOutputPool.allocStringBuilder(24); sb.append("o_s").append(prefix).append(vc.getDispatchID()); return StringOutputPool.freePop(sb); } public String getUniqueId() { return Long.toString(CodeHelper.getRAMUniqueID()); } /** * * @return the componentid (e.g.) o_c32645732 */ public StringOutput getCId() { StringOutput sb = new StringOutput(16); sb.append("o_c").append(vc.getDispatchID()); return sb; } public String getUuid() { return UUID.randomUUID().toString().replace("-", ""); } /** * * @param command * @return e.g. /olat/auth/1:2:3:cid:com/ */ public StringOutput commandURIbg(String command) { StringOutput sb = new StringOutput(100); renderer.getUrlBuilder().buildURI(sb, new String[] { VelocityContainer.COMMAND_ID }, new String[] { command }, isIframePostEnabled? AJAXFlags.MODE_TOBGIFRAME : AJAXFlags.MODE_NORMAL); return sb; } /** * @param command * @return */ public StringOutput commandURI(String command) { StringOutput sb = new StringOutput(100); renderer.getUrlBuilder().buildURI(sb, new String[] { VelocityContainer.COMMAND_ID }, new String[] { command }); return sb; } public String hrefAndOnclick(String command, boolean dirtyCheck, boolean pushState) { renderer.getUrlBuilder().buildHrefAndOnclick(target, null, isIframePostEnabled, dirtyCheck, pushState, new NameValuePair(VelocityContainer.COMMAND_ID, command)); return ""; } public String hrefAndOnclick(String command, boolean dirtyCheck, boolean pushState, String key, String value) { renderer.getUrlBuilder().buildHrefAndOnclick(target, null, isIframePostEnabled, dirtyCheck, pushState, new NameValuePair(VelocityContainer.COMMAND_ID, command), new NameValuePair(key, value)); return ""; } public String hrefAndOnclick(String command, boolean dirtyCheck, boolean pushState, String key1, String value1, String key2, String value2) { renderer.getUrlBuilder().buildHrefAndOnclick(target, null, isIframePostEnabled, dirtyCheck, pushState, new NameValuePair(VelocityContainer.COMMAND_ID, command), new NameValuePair(key1, value1), new NameValuePair(key2, value2)); return ""; } /** * Creates a java script fragment to execute a ajax background request. * * @param command * @return */ public String javaScriptCommand(String command) { renderer.getUrlBuilder().buildXHREvent(target, null, false, false, new NameValuePair(VelocityContainer.COMMAND_ID, command)); return ""; } public String javaScriptCommand(String command, String key, String value) { renderer.getUrlBuilder().buildXHREvent(target, null, false, false, new NameValuePair(VelocityContainer.COMMAND_ID, command), new NameValuePair(key, value)); return ""; } public String javaScriptCommand(String command, String key1, String value1, String key2, String value2) { renderer.getUrlBuilder().buildXHREvent(target, null, false, false, new NameValuePair(VelocityContainer.COMMAND_ID, command), new NameValuePair(key1, value1), new NameValuePair(key2, value2)); return ""; } /** * Creates the start of a java script fragment to execute a background request. It's * up to you to close the javascript call. * * @param command * @return */ public String openJavaScriptCommand(String command) { renderer.getUrlBuilder().openXHREvent(target, null, false, false, new NameValuePair(VelocityContainer.COMMAND_ID, command)); return ""; } public String openNoResponseJavaScriptCommand(String command) { renderer.getUrlBuilder().openXHRNoResponseEvent(target, null, new NameValuePair(VelocityContainer.COMMAND_ID, command)); return ""; } /** * * @param command * @return */ public String backgroundCommand(String command) { renderer.getUrlBuilder().getXHRNoResponseEvent(target, null, new NameValuePair(VelocityContainer.COMMAND_ID, command)); return ""; } public String backgroundCommand(String command, String key, String value) { renderer.getUrlBuilder().getXHRNoResponseEvent(target, null, new NameValuePair(VelocityContainer.COMMAND_ID, command), new NameValuePair(key, value)); return ""; } public String openBackgroundCommand(String command) { renderer.getUrlBuilder().openXHRNoResponseEvent(target, null, new NameValuePair(VelocityContainer.COMMAND_ID, command)); return ""; } /** * Use it to create the action for a handmade form in a velocity template, * e.g. '<form method="post" action="$r.formURIgb("viewswitch")">' * @param command * @return */ public StringOutput formURIbg(String command) { StringOutput sb = new StringOutput(100); renderer.getUrlBuilder().buildURI(sb, new String[] { VelocityContainer.COMMAND_ID }, new String[] { command }, isIframePostEnabled? AJAXFlags.MODE_TOBGIFRAME : AJAXFlags.MODE_NORMAL); return sb; } /** * Use it to create the forced non-ajax action for a handmade form in a velocity template, * e.g. '<form method="post" action="$r.formURIgb("viewswitch")">' * @param command * @return */ public StringOutput formURI(String command) { StringOutput sb = new StringOutput(100); renderer.getUrlBuilder().buildURI(sb, new String[] { VelocityContainer.COMMAND_ID }, new String[] { command }); return sb; } /** * * @param command * @return */ public StringOutput commandURI(String command, String paramKey, String paramValue) { StringOutput sb = new StringOutput(100); renderer.getUrlBuilder().buildURI(sb, new String[] { VelocityContainer.COMMAND_ID, paramKey }, new String[] { command, paramValue }); return sb; } /** * * @param command * @return */ public StringOutput commandURIbg(String command, String paramKey, String paramValue) { StringOutput sb = new StringOutput(100); renderer.getUrlBuilder().buildURI(sb, new String[] { VelocityContainer.COMMAND_ID, paramKey }, new String[] { command, paramValue }, isIframePostEnabled? AJAXFlags.MODE_TOBGIFRAME : AJAXFlags.MODE_NORMAL); return sb; } /** * should be called within the main .html template after the <head>tag. gets * some js/css/onLoad code from the component to render/work correctly. * * @return */ public StringOutput renderBodyOnLoadJSFunctionCall() { StringOutput sb = new StringOutput(100); renderer.renderBodyOnLoadJSFunctionCall(sb, vc); return sb; } /** * should be called within the main .html template after the <head>tag. gets * some js/css/onLoad code from the component to render/work correctly. * * @return */ public StringOutput renderHeaderIncludes() { StringOutput sb = new StringOutput(100); renderer.renderHeaderIncludes(sb, vc); return sb; } /** * Note: use only rarely - e.g. for static redirects to login screen or to a * special dispatcher. Renders a uri which is mounted to the webapp/ directory * of your webapplication. * <p> * For static references (e.g. images which cannot be delivered using css): * use renderStaticURI instead! */ public StringOutput relLink(String URI) { StringOutput sb = new StringOutput(100); Renderer.renderNormalURI(sb, URI); return sb; } /** * * e.g. "images/somethingicannotdowithcss.jpg" -> /olat/raw/61x/images/somethingicannotdowithcss.jpg" * with /olat/raw/61x/ mounted to webapp/static directory of your webapp * * @param URI * @return */ public StringOutput staticLink(String URI) { StringOutput sb = new StringOutput(100); Renderer.renderStaticURI(sb, URI); return sb; } public StringOutput mathJaxCdn() { StringOutput sb = new StringOutput(100); sb.append(WebappHelper.getMathJaxCdn()); return sb; } public StringOutput contextPath() { StringOutput sb = new StringOutput(100); sb.append(Settings.getServerContextPath()); return sb; } /** * e.g. "/olat/" * @return */ public String relWinLink() { return renderer.getUriPrefix(); } /** * @param componentName * @return */ public StringOutput render(String componentName) { return doRender(componentName, null); } /** * Convenience method, render by component name. * * @param component * @return */ public StringOutput render(Component component) { if(component == null) return new StringOutput(1); return doRender(component.getComponentName(), null); } /** * Convenience method, render by component name. * * @param component * @return */ public StringOutput render(FormItem item) { if(item == null) return new StringOutput(1); return doRender(item.getComponent().getComponentName(), null); } public StringOutput render(FormItem item, String arg1) { if(item == null) return new StringOutput(1); return doRender(item.getComponent().getComponentName(), new String[]{ arg1 }); } /** * Create a link wrapped with some markup to render a nice help button. The * link points to the corresponding page in the manual. * * @param page Help page name * @return */ public StringOutput contextHelpWithWrapper(String page) { StringOutput sb = new StringOutput(192); if (helpModule.isHelpEnabled()) { Locale locale = renderer.getTranslator().getLocale(); String url = helpModule.getHelpProvider().getURL(locale, page); if(url != null) { String title = StringEscapeUtils.escapeHtml(renderer.getTranslator().translate("help.button")); sb.append("<span class=\"o_chelp_wrapper\">") .append("<a href=\"").append(url) .append("\" class=\"o_chelp\" target=\"_blank\" title=\"").append(title).append("\"><i class='o_icon o_icon_help'></i> ") .append(renderer.getTranslator().translate("help")) .append("</a></span>"); } } return sb; } /** * Create a js command to open a specific page in the manual. * * @param page Help page name * @return */ public StringOutput contextHelpJSCommand(String page) { StringOutput sb = new StringOutput(100); if (helpModule.isHelpEnabled()) { Locale locale = renderer.getTranslator().getLocale(); String url = helpModule.getHelpProvider().getURL(locale, page); sb.append("contextHelpWindow('").append(url).append("')"); } return sb; } /** * Create a link to a specific page in the manual. The link points to the * corresponding page in the manual. * * @param page * Help page name */ public StringOutput contextHelpLink(String page) { StringOutput sb = new StringOutput(100); if (helpModule.isHelpEnabled()) { String url = helpModule.getHelpProvider().getURL(renderer.getTranslator().getLocale(), page); sb.append(url); } return sb; } /** * @param componentName * @param arg1 * @return */ public StringOutput render(String componentName, String arg1) { return doRender(componentName, new String[] { arg1 }); } /** * @param componentName * @param arg1 * @param arg2 * @return */ public StringOutput render(String componentName, String arg1, String arg2) { return doRender(componentName, new String[] { arg1, arg2 }); } /** * Parametrised translator * * @param key The translation key * @param arg1 The argument list as a string array * @return */ public String translate(String key, String[] arg1) { Translator trans = renderer.getTranslator(); if (trans == null) return "{Translator is null: key_to_translate=" + key + "}"; String res = trans.translate(key, arg1); if (res == null) return "?? key not found to translate: key_to_translate=" + key + " / trans info:" + trans + "}"; return res; } /** * Wrapper to make it possible to use the parametrised translator from within * the velocity container * * @param key The translation key * @param arg1 The argument list as a list of strings * @return the translated string */ public String translate(String key, List<String> arg1) { return translate(key, arg1.toArray(new String[arg1.size()])); } /** * Wrapper to make it possible to use the parametrised translator from within * the velocity container * * @param key The translation key * @param arg1 The argument sd string * @return the translated string */ public String translate(String key, String arg1) { return translate(key, new String[] {arg1}); } public String translate(String key, String arg1, String arg2) { return translate(key, new String[] {arg1, arg2}); } public String translate(String key, String arg1, String arg2, String arg3) { return translate(key, new String[] {arg1, arg2, arg3}); } public String translate(String key, Integer arg1) { return translate(key, new String[] { (arg1 == null ? "" : arg1.toString()) }); } public String translate(String key, Integer arg1, Integer arg2) { return translate(key, new String[] { (arg1 == null ? "" : arg1.toString()), (arg2 == null ? "" : arg2.toString()) }); } /** * Method to translate a key that comes from another package. This should be * used rarely. When a key is used withing multiple packages is is usually * better to use a fallback translator or to move the key to the default * packages. * <p> * Used in context help system * @param bundleName the package name, e.g. 'org.olat.core' * @param key the key, e.g. 'my.key' * @param args optional arguments, null if not used * @return */ public String translateWithPackage(String bundleName, String key, String[] args) { Translator pageTrans = renderer.getTranslator(); if (pageTrans == null) return "{Translator is null: key_to_translate=" + key + "}"; Locale locale = pageTrans.getLocale(); Translator tempTrans = new PackageTranslator(bundleName, locale); String result = tempTrans.translate(key, args); if (result == null) { return "{Invalid bundle name: " + bundleName + " and key: " + key + "}"; } return result; } /** * Method to translate a key that comes from another package. This should be * used rarely. When a key is used withing multiple packages is is usually * better to use a fallback translator or to move the key to the default * packages. * @param bundleName the package name, e.g. 'org.olat.core' * @param key the key, e.g. 'my.key' * @return */ public String translateWithPackage(String bundleName, String key) { return translateWithPackage(bundleName, key, null); } /**escapes " entities in \" * @param in the string to convert * @deprecated please use escapeHtml. * @return the escaped string */ public String escapeDoubleQuotes(String in) { return Formatter.escapeDoubleQuotes(in).toString(); } /** * Escapes the characters in a String for JavaScript use. */ public String escapeJavaScript(String str) { return StringHelper.escapeJavaScript(str); } /** * Escapes the characters in a String using HTML entities. * @param str * @return */ public String escapeHtml(String str) throws IOException { if(str == null) { return ""; } return StringHelper.escapeHtml(str); } public String xssScan(String str) { if(str == null) { return ""; } OWASPAntiSamyXSSFilter filter = new OWASPAntiSamyXSSFilter(); return filter.filter(str); } public String filterHtml(String str) { if(str == null) { return ""; } return FilterFactory.getHtmlTagsFilter().filter(str); } /** * @param key * @return */ public String translate(String key) { Translator trans = renderer.getTranslator(); if (trans == null) return "{Translator is null: key_to_translate=" + key + "}"; String res = trans.translate(key); if (res == null) return "?? key not found to translate: key_to_translate=" + key + " / trans info:" + trans + "}"; return res; } /** * Translates and escapesHTML. * It assumes that the HTML attribute value should be enclosed in double quotes. * @param key * @return */ public String translateInAttribute(String key) { return StringEscapeUtils.escapeHtml(translate(key)); } /** * @return current language code as found in (current)Locale.toString() method */ public String getLanguageCode() { Locale currentLocale = I18nManager.getInstance().getCurrentThreadLocale(); return currentLocale.toString(); } /** * * renders the component. * if the component cannot be found, there is no error, but an empty String is returned. Not recommended to use normally, but rather use @see render(String componentName) * * @param componentName * @return */ public StringOutput renderForce(String componentName) { Component source = renderer.findComponent(componentName); StringOutput sb; if (source == null) { sb = new StringOutput(1); } else if (target == null) { sb = new StringOutput(10000); renderer.render(source, sb, null); } else { renderer.render(source, target, null); } return new StringOutput(1); } private StringOutput doRender(String componentName, String[] args) { Component source = renderer.findComponent(componentName); StringOutput sb; if (source == null) { sb = new StringOutput(128); sb.append(">>>>>>>>>>>>>>>>>>>>>>>>>> component " + componentName + " could not be found to be rendered!"); } else if (target == null) { sb = new StringOutput(10000); renderer.render(source, sb, args); } else { sb = new StringOutput(1); renderer.render(source, target, args); } return sb; } public boolean isNull(Object obj) { return obj == null; } public boolean isNotNull(Object obj) { return obj != null; } public boolean isEmpty(Object obj) { boolean empty; if(obj == null) { empty = true; } else if(obj instanceof String) { empty = !StringHelper.containsNonWhitespace((String)obj) || "<p></p>".equals(obj); } else if(obj instanceof Collection) { empty = ((Collection<?>)obj).isEmpty(); } else if(obj instanceof Map) { empty = ((Map<?,?>)obj).isEmpty(); } else { empty = false; } return empty; } public boolean isNotEmpty(Object obj) { boolean notEmpty; if(obj == null) { notEmpty = false; } else if(obj instanceof String) { notEmpty = StringHelper.containsNonWhitespace((String)obj) && !"<p></p>".equals(obj); } else if(obj instanceof Collection) { notEmpty = !((Collection<?>)obj).isEmpty(); } else if(obj instanceof Map) { notEmpty = !((Map<?,?>)obj).isEmpty(); } else { notEmpty = true; } return notEmpty; } public int parseInt(String text) { try { if(StringHelper.containsNonWhitespace(text)) { return Integer.parseInt(text); } return -1; } catch (NumberFormatException e) { return -1; } } /** * @param componentName * @return true if the component with name componentName is a child of the current container. Used to "if" the render * instruction "$r.render(componentName)" if it is not known beforehand whether the component is there or not. */ public boolean available(String componentName) { Component source = renderer.findComponent(componentName); return (source != null); } /** * @param componentName * @return true if the component with name componentName is a child of the current container and if this * component is visible */ public boolean visible(String componentName) { Component source = renderer.findComponent(componentName); return (source != null && source.isVisible()); } public boolean visible(Component component) { return (component != null && component.isVisible()); } public boolean visible(FormItem item) { if(item == null) return false; return visible(item.getComponent()); } /** * @param componentName * @return true if the component with name componentName is a child of the current container and if this * component is visible and enabled */ public boolean enabled(String componentName) { Component source = renderer.findComponent(componentName); return (source != null && source.isVisible() && source.isEnabled()); } /** * Return the component * @param componentName * @return */ public Component getComponent(String componentName) { Component source = renderer.findComponent(componentName); return source; } /** * returns an object from the context of velocity * * @param key * @return */ public Object get(String key) { return vc.getContext().get(key); } public boolean absent(String key) { return !vc.getContext().containsKey(key); } public boolean notNull(Object obj) { return obj != null; } /** * Formats the given date in a short format, e.g. 05.12.2015 or 12/05/2015 * * @param date the date * @return a String with the formatted date */ public String formatDate(Date date) { if(date == null) return ""; Formatter f = Formatter.getInstance(renderer.getTranslator().getLocale()); return f.formatDate(date); } /** * Formats the given date in a medium sized format, e.g. 12. Dezember 2015 or December 12, 2015 * * @param date the date * @return a String with the formatted date */ public String formatDateLong(Date date) { if(date == null) return ""; Formatter f = Formatter.getInstance(renderer.getTranslator().getLocale()); return f.formatDateLong(date); } /** * Formats the given date in a medium size with date and time, e.g. 05.12.2015 14:35 * * @param date the date * @return a String with the formatted date and time */ public String formatDateAndTime(Date date) { if(date == null) return ""; Formatter f = Formatter.getInstance(renderer.getTranslator().getLocale()); return f.formatDateAndTime(date); } /** * Formats the given date in a long size with date and time, e.g. Tuesday, * 10. September 2015, 3:48 PM * * @param date * the date * @return a String with the formatted date and time */ public String formatDateAndTimeLong(Date date) { if(date == null) return ""; Formatter f = Formatter.getInstance(renderer.getTranslator().getLocale()); return f.formatDateAndTimeLong(date); } /** * formats the given time period so it is friendly to read * * @param d the date * @return a String with the formatted time */ public String formatTime(Date date) { Formatter f = Formatter.getInstance(renderer.getTranslator().getLocale()); return f.formatTime(date); } /** * format a duration (in milliseconds) * @param durationInMillis * @return */ public String formatDurationInMillis(long durationInMillis) { return Formatter.formatDuration(durationInMillis); } public String formatBytes(long bytes) { return Formatter.formatBytes(bytes); } /** * Wrapp given html code with a wrapper an add code to transform latex * formulas to nice visual characters on the client side. The latex formulas * must be within an HTML element that has the class 'math' attached. * * @param htmlFragment A html element that might contain an element that has a * class 'math' with latex formulas * @return */ public static String formatLatexFormulas(String htmlFragment) { return Formatter.formatLatexFormulas(htmlFragment); } /** * Search in given text fragment for URL's and surround them with clickable * HTML link objects. * * @param textFragment * @return text with clickable links */ public static String formatURLsAsLinks(String textFragment) { return Formatter.formatURLsAsLinks(textFragment); } /** * Strips all HTML tags from the source string. * * @param source * Source * @return Source without HTML tags. */ public static String filterHTMLTags(String source) { Filter htmlTagsFilter = FilterFactory.getHtmlTagsFilter(); return htmlTagsFilter.filter(source); } /** * Get the icon css class that represents the filetype based on the file name * @param filename * @return The css class for the file or a default css class */ public static String getFiletypeIconCss(String filename) { return CSSHelper.createFiletypeIconCssClassFor(filename); } /** * Returns true when debug mode is configured, false otherwhise * @return */ public boolean isDebuging() { return Settings.isDebuging(); } public String getVersion() { return Settings.getVersion(); } public Languages getLanguages() { I18nManager i18nMgr = I18nManager.getInstance(); Collection<String> enabledKeysSet = I18nModule.getEnabledLanguageKeys(); Map<String, String> langNames = new HashMap<String, String>(); Map<String, String> langTranslators = new HashMap<String, String>(); String[] enabledKeys = ArrayHelper.toArray(enabledKeysSet); String[] names = new String[enabledKeys.length]; for (int i = 0; i < enabledKeys.length; i++) { String key = enabledKeys[i]; String langName = i18nMgr.getLanguageInEnglish(key, I18nModule.isOverlayEnabled()); langNames.put(key, langName); names[i] = langName; String author = i18nMgr.getLanguageAuthor(key); langTranslators.put(key, author); } ArrayHelper.sort(enabledKeys, names, true, true, true); return new Languages(enabledKeys, langNames, langTranslators); } public static class Languages { private final String[] enabledKeys; private final Map<String, String> langNames; private final Map<String, String> langTranslators; public Languages(String[] enabledKeys, Map<String, String> langNames, Map<String, String> langTranslators) { this.enabledKeys = enabledKeys; this.langNames = langNames; this.langTranslators = langTranslators; } public String[] getEnabledKeys() { return enabledKeys; } public Map<String, String> getLangNames() { return langNames; } public Map<String, String> getLangTranslators() { return langTranslators; } } }