/* * Copyright (c) 2012 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: * HUMBOLDT EU Integrated Project #030962 * Data Harmonisation Panel <http://www.dhpanel.eu> */ package eu.esdihumboldt.hale.common.align.model.impl; import java.io.IOException; import java.text.MessageFormat; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Map.Entry; import java.util.ResourceBundle; import java.util.Set; import java.util.stream.Collectors; import javax.annotation.Nullable; import org.apache.commons.lang.StringEscapeUtils; import org.springframework.core.io.support.PathMatchingResourcePatternResolver; import com.google.common.base.Joiner; import de.fhg.igd.slf4jplus.ALogger; import de.fhg.igd.slf4jplus.ALoggerFactory; import eu.esdihumboldt.hale.common.align.model.AlignmentUtil; import eu.esdihumboldt.hale.common.align.model.Cell; import eu.esdihumboldt.hale.common.align.model.CellExplanation; import eu.esdihumboldt.hale.common.align.model.ChildContext; import eu.esdihumboldt.hale.common.align.model.Entity; import eu.esdihumboldt.hale.common.align.model.EntityDefinition; import eu.esdihumboldt.hale.common.core.service.ServiceProvider; /** * Abstract cell explanation implementation. * * @author Simon Templer */ public abstract class AbstractCellExplanation implements CellExplanation { private static final ALogger log = ALoggerFactory.getLogger(AbstractCellExplanation.class); @Override public String getExplanation(Cell cell, ServiceProvider provider, Locale locale) { return getExplanation(cell, false, provider, locale); } @Override public String getExplanationAsHtml(Cell cell, ServiceProvider provider, Locale locale) { return getExplanation(cell, true, provider, locale); } /** * Get the explanation string in the specified format. * * @param cell the cell to create an explanation for * @param html if the format should be HMTL, otherwise the format is just * text * @param provider the service provider, if available * @param locale the locale for the explanation, to be matched if content is * available * @return the explanation or <code>null</code> */ protected abstract String getExplanation(Cell cell, boolean html, @Nullable ServiceProvider provider, Locale locale); /** * Format an entity for inclusion in an explanation. * * @param entity the entity, may be <code>null</code> * @param html if the format should be HMTL, otherwise the format is just * text * @param indexInFront whether index conditions should be in front of the * property name or behind in brackets * @param locale the locale for the explanation, to be matched if content is * available * @return the formatted entity name or <code>null</code> in case of * <code>null</code> input */ protected String formatEntity(Entity entity, boolean html, boolean indexInFront, Locale locale) { if (entity == null) return null; return formatEntity(entity.getDefinition(), html, indexInFront, locale); } /** * Format an entity for inclusion in an explanation. * * @param entityDef the entity definition, may be <code>null</code> * @param html if the format should be HMTL, otherwise the format is just * text * @param indexInFront whether index conditions should be in front of the * property name or behind in brackets * @param locale the locale for the explanation, to be matched if content is * available * @return the formatted entity name or <code>null</code> in case of * <code>null</code> input */ protected String formatEntity(EntityDefinition entityDef, boolean html, boolean indexInFront, Locale locale) { if (entityDef == null) return null; // get name and standard text String name = entityDef.getDefinition().getDisplayName(); String text = quoteName(name, html); // modify text with filter List<ChildContext> path = entityDef.getPropertyPath(); // different output than AlignmentUtil in case of property with index // condition if (path != null && !path.isEmpty() && path.get(path.size() - 1).getIndex() != null) { if (indexInFront) { text = MessageFormat.format(getBaseMessage("index_pre", locale), formatNumber(path.get(path.size() - 1).getIndex() + 1, locale), text); } else { text += " " + MessageFormat.format(getBaseMessage("index_post", locale), formatNumber(path.get(path.size() - 1).getIndex() + 1, locale)); } } else { String filterString = AlignmentUtil.getContextText(entityDef); if (html) { filterString = StringEscapeUtils.escapeHtml(filterString); } if (filterString != null) text += " " + MessageFormat.format(getBaseMessage("filter", locale), quoteText(filterString, html)); } return text; } /** * Returns an entity name without condition strings (e.g. "part1.part2"). * * @param entity the entity * @return the entity name */ protected String getEntityNameWithoutCondition(Entity entity) { EntityDefinition entityDef = entity.getDefinition(); if (entityDef.getPropertyPath() != null && !entityDef.getPropertyPath().isEmpty()) { List<String> names = new ArrayList<String>(); for (ChildContext context : entityDef.getPropertyPath()) { names.add(context.getChild().getName().getLocalPart()); } String longName = Joiner.on('.').join(names); return longName; } else return entityDef.getDefinition().getDisplayName(); } /** * Checks whether the given entity has an index condition. * * @param entity the entity to check * @return true, if the entity has an index condition */ protected boolean hasIndexCondition(Entity entity) { List<ChildContext> path = entity.getDefinition().getPropertyPath(); return path != null && !path.isEmpty() && path.get(path.size() - 1).getIndex() != null; } /** * Quote or otherwise format (in case of HTML) the given text. * * @param text the text, may be <code>null</code> * @param html if the format should be HMTL, otherwise the format is just * text * @return the quoted text or <code>null</code> in case of <code>null</code> * input */ protected String quoteText(String text, boolean html) { if (text == null) return null; if (html) return "<span style=\"font-style: italic;\">" + text + "</span>"; else return "'" + text + "'"; } /** * Quote or otherwise format (in case of HTML) the given value. * * @param value the value to quote, may be <code>null</code> * @param html if the format should be HMTL, otherwise the format is just * text * @return the quoted text or <code>null</code> in case of <code>null</code> * input */ protected String quoteValue(Object value, boolean html) { if (value == null) return null; if (html) return "<code>" + value + "</code>"; else return "`" + value + "`"; } /** * Quote or otherwise format (in case of HTML) the given name (e.g. an * entity or parameter name). * * @param name the name to quote, may be <code>null</code> * @param html if the format should be HMTL, otherwise the format is just * text * @return the quoted text or <code>null</code> in case of <code>null</code> * input */ protected String quoteName(String name, boolean html) { if (name == null) return null; if (html) return "<em>" + name + "</em>"; else return "'" + name + "'"; } private String formatNumber(int number, Locale locale) { switch (number) { case 1: return getBaseMessage("first", locale); case 2: return getBaseMessage("second", locale); case 3: return getBaseMessage("third", locale); case 4: return getBaseMessage("fourth", locale); case 5: return getBaseMessage("fifth", locale); case 6: return getBaseMessage("sixth", locale); default: return (number + 1) + "."; } } /** * Get a message for a specific locale. * * @param key the message key * @param locale the locale * @return the message string */ protected String getMessage(String key, Locale locale) { return getMessage(key, locale, getDefaultMessageClass()); } /** * Get a message for a specific locale. * * @param key the message key * @param locale the locale * @param messageClass the class the messages to retrieve are associated to * @return the message string */ protected String getMessage(String key, Locale locale, Class<?> messageClass) { return ResourceBundle .getBundle(messageClass.getName(), locale, messageClass.getClassLoader(), ResourceBundle.Control .getNoFallbackControl(ResourceBundle.Control.FORMAT_PROPERTIES)) .getString(key); } /** * Get the class used to retrieve messages via * {@link #getMessage(String, Locale)} * * @return the default message class */ protected Class<?> getDefaultMessageClass() { return getClass(); } /** * Get a message for a specific locale that is stored with the * {@link AbstractCellExplanation} explanation base class. * * @param key the message key * @param locale the locale * @return the message string */ private String getBaseMessage(String key, Locale locale) { return ResourceBundle .getBundle(AbstractCellExplanation.class.getName(), locale, AbstractCellExplanation.class.getClassLoader(), ResourceBundle.Control .getNoFallbackControl(ResourceBundle.Control.FORMAT_PROPERTIES)) .getString(key); } @Override public Iterable<Locale> getSupportedLocales() { try { return findLocales(getDefaultMessageClass(), getDefaultMessageClass().getSimpleName(), "properties", getDefaultLocale()); } catch (IOException e) { log.error("Error determining supported locales for explanation", e); return null; } } /** * Get the default locale assumed for resources with an unspecified locale. * * @return the default locale assumed for messages */ protected Locale getDefaultLocale() { return Locale.ENGLISH; } /** * Determine the locales a resource is available for. * * @param clazz the clazz the resource resides next to * @param baseName the base name of the resource * @param suffix the suffix of the resource file, e.g. * <code>properties</code> * @param defaultLocale the default locale to be assumed for an unqualified * resource * @return the set of locales the resource is available for * @throws IOException if an error occurs trying to determine the resource * files */ public static Set<Locale> findLocales(final Class<?> clazz, final String baseName, final String suffix, Locale defaultLocale) throws IOException { PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver( clazz.getClassLoader()); String pkg = clazz.getPackage().getName().replaceAll("\\.", "/"); String pattern = pkg + "/" + baseName + "*." + suffix; return Arrays.stream(resolver.getResources(pattern)).map(resource -> { String fileName = resource.getFilename(); if (fileName != null && fileName.startsWith(baseName)) { fileName = fileName.substring(baseName.length()); if (fileName.endsWith("." + suffix)) { if (fileName.length() == suffix.length() + 1) { // default locale file return defaultLocale; } else { String localeIdent = fileName.substring(0, fileName.length() - suffix.length() - 1); String language = ""; String country = ""; String variant = ""; String[] parts = localeIdent.split("_"); int index = 0; if (parts.length > index && parts[index].isEmpty()) { index++; } if (parts.length > index) { language = parts[index++]; } if (parts.length > index) { country = parts[index++]; } if (parts.length > index) { variant = parts[index++]; } return new Locale(language, country, variant); } } else { log.error("Invalid resource encountered"); return null; } } else { log.error("Invalid resource encountered"); return null; } }).filter(locale -> locale != null).collect(Collectors.toSet()); } /** * Create a string enumerating the given items. * * @param items the collection of items * @param locale the locale * @return the joined string */ protected String enumerateJoin(List<String> items, Locale locale) { StringBuilder result = new StringBuilder(); for (int i = 0; i < items.size(); i++) { result.append(items.get(i)); if (i == items.size() - 2) { result.append(' '); result.append(getBaseMessage("and", locale)); result.append(' '); } else if (i < items.size() - 2) { result.append(", "); } } return result.toString(); } /** * Build a replacement table (HTML only). * * @param varToProperty variable expressions mapped to the entities that * replace them * @param locale the locale * @return the replacement table as string */ protected String buildReplacementTable(Map<String, String> varToProperty, Locale locale) { StringBuilder sb = new StringBuilder(); sb.append("<br /><br />"); sb.append(getBaseMessage("rt_intro", locale)); sb.append("<br />"); sb.append("<table border=\"1\"><tr><th>"); sb.append(getBaseMessage("rt_variable", locale)); sb.append("</th><th>"); sb.append(getBaseMessage("rt_property", locale)); sb.append("</th></tr>"); for (Entry<String, String> entry : varToProperty.entrySet()) { sb.append(String.format("<tr><td>%s</td><td>%s</td></tr>", entry.getKey(), entry.getValue())); } sb.append("</table>"); return sb.toString(); } }