/* * Copyright (c) 2016 Data Harmonisation Panel * * All rights reserved. This program and the accompanying materials are made * available under the terms of the GNU Lesser General Public License as * published by the Free Software Foundation, either version 3 of the License, * or (at your option) any later version. * * You should have received a copy of the GNU Lesser General Public License * along with this distribution. If not, see <http://www.gnu.org/licenses/>. * * Contributors: * Data Harmonisation Panel <http://www.dhpanel.eu> */ package eu.esdihumboldt.hale.common.align.model.impl.mdexpl; import java.io.IOException; import java.io.InputStreamReader; import java.io.Reader; import java.net.URL; import java.nio.charset.StandardCharsets; import java.util.HashMap; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Optional; import java.util.ResourceBundle; import java.util.Set; import java.util.stream.Collectors; import javax.annotation.Nullable; import org.pegdown.Extensions; import org.pegdown.PegDownProcessor; import com.google.common.collect.ListMultimap; import de.fhg.igd.slf4jplus.ALogger; import de.fhg.igd.slf4jplus.ALoggerFactory; import eu.esdihumboldt.hale.common.align.extension.function.FunctionDefinition; import eu.esdihumboldt.hale.common.align.extension.function.FunctionUtil; import eu.esdihumboldt.hale.common.align.extension.function.ParameterDefinition; import eu.esdihumboldt.hale.common.align.model.Cell; import eu.esdihumboldt.hale.common.align.model.Entity; import eu.esdihumboldt.hale.common.align.model.impl.AbstractCellExplanation; import eu.esdihumboldt.hale.common.core.service.ServiceProvider; import groovy.text.GStringTemplateEngine; import groovy.text.Template; import groovy.text.TemplateEngine; /** * Markdown based cell explanation. * * @author Simon Templer */ public abstract class MarkdownCellExplanation extends AbstractCellExplanation { private static final ALogger log = ALoggerFactory.getLogger(MarkdownCellExplanation.class); private final PegDownProcessor pegdown = new PegDownProcessor(Extensions.AUTOLINKS | // Extensions.HARDWRAPS | // Extensions.SMARTYPANTS | // Extensions.TABLES); private final TemplateEngine engine = new GStringTemplateEngine(); private final Map<Locale, Optional<Template>> templateCache = new HashMap<>(); /** * Get the explanation template for a given locale. * * @param clazz the class to retrieve the template for * @param locale the locale * @return the loaded template as string, if available */ public Optional<Template> getTemplate(Class<?> clazz, Locale locale) { return templateCache.computeIfAbsent(locale, cl -> loadTemplate(clazz, cl)); } @Override public Iterable<Locale> getSupportedLocales() { try { return AbstractCellExplanation.findLocales(getDefaultMessageClass(), getDefaultMessageClass().getSimpleName(), "md", getDefaultLocale()); } catch (IOException e) { log.error("Error determining supported locales for explanation", e); return null; } } /** * @return the template engine */ protected TemplateEngine getEngine() { return engine; } /** * Load an explanation template. The default implementation locates * localized Markdown files located next to the class. * * @param clazz the explanation class * @param locale the locale * @return the loaded template, if available */ protected Optional<Template> loadTemplate(Class<?> clazz, Locale locale) { return findResource(clazz, clazz.getSimpleName(), "md", locale).flatMap(url -> { try (Reader reader = new InputStreamReader(url.openStream(), StandardCharsets.UTF_8)) { return Optional.ofNullable(engine.createTemplate(reader)); } catch (Exception e) { log.error("Could not read cell explanation template", e); return Optional.empty(); } }); } private Optional<URL> findResource(Class<?> clazz, String baseName, String suffix, Locale locale) { ResourceBundle.Control control = ResourceBundle.Control .getNoFallbackControl(ResourceBundle.Control.FORMAT_DEFAULT); List<Locale> candidateLocales = control.getCandidateLocales(baseName, locale); for (Locale specificLocale : candidateLocales) { String bundleName = control.toBundleName(baseName, specificLocale); String resourceName = control.toResourceName(bundleName, suffix); URL url = clazz.getResource(resourceName); if (url != null) { return Optional.of(url); } } return Optional.empty(); } @Override protected String getExplanation(Cell cell, boolean html, ServiceProvider provider, Locale locale) { Optional<Template> maybeTemplate = getTemplate(getDefaultMessageClass(), locale); if (maybeTemplate.isPresent()) { try { Template template = maybeTemplate.get(); // process template String explanation = template.make(createBinding(cell, html, provider, locale)) .toString(); if (html) { explanation = pegdown.markdownToHtml(explanation); } return explanation; } catch (Exception e) { log.error("Error generating cell explanation for function " + cell.getTransformationIdentifier(), e); return null; } } else { return null; } } private Map<String, Object> createBinding(Cell cell, boolean html, ServiceProvider provider, Locale locale) { Map<String, Object> binding = new HashMap<>(); FunctionDefinition<? extends ParameterDefinition> function = loadFunction( cell.getTransformationIdentifier(), provider); // parameters binding.put("_params", new ParameterBinding(cell, function)); // entities addEntityBindings(binding, function.getSource(), cell.getSource(), "_source", html, locale); addEntityBindings(binding, function.getTarget(), cell.getTarget(), "_target", html, locale); // customization customizeBinding(binding, cell, html, provider, locale); return binding; } /** * Load the function definition associated to the cell to be explained. * * @param functionId the function identifier * @param provider the service provider, if available * @return the function definition or <code>null</code> */ @Nullable protected FunctionDefinition<? extends ParameterDefinition> loadFunction(String functionId, @Nullable ServiceProvider provider) { return FunctionUtil.getFunction(functionId, provider); } /** * Customize the binding provided to the template. * * @param binding the binding * @param cell the mapping cell for which the explanation is created * @param html if HTML content should be produced * @param provider the service provider * @param locale the content locale */ protected void customizeBinding(Map<String, Object> binding, Cell cell, boolean html, ServiceProvider provider, Locale locale) { // override me } private void addEntityBindings(Map<String, Object> binding, Set<? extends ParameterDefinition> definitions, ListMultimap<String, ? extends Entity> entities, String defaultName, boolean html, Locale locale) { if (!definitions.isEmpty()) { if (definitions.size() == 1) { // single entity ParameterDefinition def = definitions.iterator().next(); // _defaultName always maps to single entity addEntityBindingValue(defaultName, def, entities.get(def.getName()), html, binding, locale); // in addition also the name if it is present String name = def.getName(); if (name != null) { addEntityBindingValue(name, def, entities.get(name), html, binding, locale); } } else { for (ParameterDefinition def : definitions) { // add each entity based on its name, the default name is // used for the null entity String name = def.getName(); if (name != null) { addEntityBindingValue(name, def, entities.get(name), html, binding, locale); } else { // null entity -> default name addEntityBindingValue(defaultName, def, entities.get(name), html, binding, locale); } } } } } private void addEntityBindingValue(String bindingName, ParameterDefinition definition, List<? extends Entity> entities, boolean html, Map<String, Object> binding, Locale locale) { final Object entityBinding; if (definition.getMaxOccurrence() == 1) { // single entity if (entities.isEmpty()) { // not present entityBinding = null; } else { entityBinding = formatEntity(entities.get(0), html, false, locale); } } else { // entity list entityBinding = entities.stream().map(entity -> { return formatEntity(entity, html, true, locale); }).collect(Collectors.toList()); } binding.put(bindingName, entityBinding); } }