Java tutorial
//============================================================================== //=== Copyright (C) 2001-2008 Food and Agriculture Organization of the //=== United Nations (FAO-UN), United Nations World Food Programme (WFP) //=== and United Nations Environment Programme (UNEP) //=== //=== This program is free software; you can redistribute it and/or modify //=== it under the terms of the GNU General Public License as published by //=== the Free Software Foundation; either version 2 of the License, or (at //=== your option) any later version. //=== //=== This program is distributed in the hope that it will be useful, but //=== WITHOUT ANY WARRANTY; without even the implied warranty of //=== MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU //=== General Public License for more details. //=== //=== You should have received a copy of the GNU General Public License //=== along with this program; if not, write to the Free Software //=== Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA //=== //=== Contact: Jeroen Ticheler - FAO - Viale delle Terme di Caracalla 2, //=== Rome - Italy. email: geonetwork@osgeo.org //============================================================================== package org.fao.geonet.api.records.formatters; import static com.google.common.io.Files.getNameWithoutExtension; import static org.fao.geonet.api.ApiParams.API_PARAM_RECORD_UUID; import static org.fao.geonet.api.records.formatters.FormatterConstants.SCHEMA_PLUGIN_FORMATTER_DIR; import static org.springframework.data.jpa.domain.Specifications.where; import java.io.IOException; import java.net.URI; import java.net.URISyntaxException; import java.nio.file.DirectoryStream; import java.nio.file.FileVisitResult; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.SimpleFileVisitor; import java.nio.file.attribute.BasicFileAttributes; import java.util.Iterator; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Set; import java.util.WeakHashMap; import java.util.concurrent.Callable; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import com.google.common.annotations.VisibleForTesting; import com.google.common.collect.Maps; import com.google.common.collect.Sets; import com.google.common.io.ByteStreams; import io.swagger.annotations.Api; import io.swagger.annotations.ApiOperation; import io.swagger.annotations.ApiParam; import jeeves.server.context.ServiceContext; import jeeves.server.dispatchers.ServiceManager; import jeeves.xlink.Processor; import org.apache.http.client.methods.HttpGet; import org.apache.http.client.methods.HttpUriRequest; import org.fao.geonet.ApplicationContextHolder; import org.fao.geonet.Constants; import org.fao.geonet.SystemInfo; import org.fao.geonet.api.API; import org.fao.geonet.api.ApiUtils; import org.fao.geonet.api.records.formatters.cache.CacheConfig; import org.fao.geonet.api.records.formatters.cache.ChangeDateValidator; import org.fao.geonet.api.records.formatters.cache.FormatterCache; import org.fao.geonet.api.records.formatters.cache.Key; import org.fao.geonet.api.records.formatters.cache.NoCacheValidator; import org.fao.geonet.api.records.formatters.cache.StoreInfoAndDataLoadResult; import org.fao.geonet.api.records.formatters.cache.Validator; import org.fao.geonet.api.records.formatters.groovy.ParamValue; import org.fao.geonet.api.tools.i18n.LanguageUtils; import org.fao.geonet.constants.Geonet; import org.fao.geonet.domain.AbstractMetadata; import org.fao.geonet.domain.ISODate; import org.fao.geonet.domain.Metadata; import org.fao.geonet.domain.MetadataType; import org.fao.geonet.domain.OperationAllowed; import org.fao.geonet.domain.Pair; import org.fao.geonet.domain.ReservedOperation; import org.fao.geonet.kernel.AccessManager; import org.fao.geonet.kernel.DataManager; import org.fao.geonet.kernel.GeonetworkDataDirectory; import org.fao.geonet.kernel.SchemaManager; import org.fao.geonet.kernel.XmlSerializer; import org.fao.geonet.kernel.datamanager.IMetadataUtils; import org.fao.geonet.kernel.search.SearchManager; import org.fao.geonet.kernel.setting.SettingManager; import org.fao.geonet.languages.IsoLanguagesMapper; import org.fao.geonet.lib.Lib; import org.fao.geonet.repository.OperationAllowedRepository; import org.fao.geonet.repository.specification.OperationAllowedSpecs; import org.fao.geonet.util.XslUtil; import org.fao.geonet.utils.GeonetHttpRequestFactory; import org.fao.geonet.utils.IO; import org.fao.geonet.utils.Log; import org.fao.geonet.utils.Xml; import org.jdom.Element; import org.jdom.JDOMException; import org.jdom.Namespace; import org.json.JSONException; import org.json.JSONObject; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationEvent; import org.springframework.context.ApplicationListener; import org.springframework.context.annotation.Lazy; import org.springframework.data.jpa.domain.Specification; import org.springframework.http.HttpEntity; import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; import org.springframework.http.client.ClientHttpResponse; import org.springframework.stereotype.Controller; import org.springframework.util.Assert; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestHeader; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.context.request.NativeWebRequest; import org.springframework.web.context.request.WebRequest; import org.xhtmlrenderer.pdf.ITextRenderer; import springfox.documentation.annotations.ApiIgnore; import com.google.common.annotations.VisibleForTesting; import com.google.common.collect.Maps; import com.google.common.collect.Sets; import com.google.common.io.ByteStreams; import io.swagger.annotations.Api; import io.swagger.annotations.ApiOperation; import io.swagger.annotations.ApiParam; import jeeves.server.context.ServiceContext; import jeeves.server.dispatchers.ServiceManager; import springfox.documentation.annotations.ApiIgnore; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.net.URI; import java.net.URISyntaxException; import java.nio.file.DirectoryStream; import java.nio.file.FileVisitResult; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.SimpleFileVisitor; import java.nio.file.attribute.BasicFileAttributes; import java.util.Iterator; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Set; import java.util.WeakHashMap; import java.util.concurrent.Callable; import static com.google.common.io.Files.getNameWithoutExtension; import static org.fao.geonet.api.ApiParams.API_PARAM_RECORD_UUID; import static org.fao.geonet.api.records.formatters.FormatterConstants.SCHEMA_PLUGIN_FORMATTER_DIR; import static org.springframework.data.jpa.domain.Specifications.where; /** * Allows a user to display a metadata with a particular formatters * * @author jeichar */ @Api(value = "records", tags = "records", description = "Metadata record operations") @Controller("recordFormatter") @Lazy public class FormatterApi extends AbstractFormatService implements ApplicationListener { private static final Set<String> ALLOWED_PARAMETERS = Sets.newHashSet("id", "uuid", "xsl", "skippopularity", "hide_withheld"); @Autowired LanguageUtils languageUtils; /** * Map (canonical path to formatter dir -> Element containing all xml files in Formatter * bundle's loc directory) */ private WeakHashMap<String, Element> pluginLocs = new WeakHashMap<>(); private Map<Path, Boolean> isFormatterInSchemaPluginMap = Maps.newHashMap(); /** * We will copy all formatter files to the data directory so that the formatters should always * compile in data directory without administrators manually keeping all the formatter * directories up-to-date. */ @Override public void onApplicationEvent(ApplicationEvent event) { if (event instanceof GeonetworkDataDirectory.GeonetworkDataDirectoryInitializedEvent) { GeonetworkDataDirectory.GeonetworkDataDirectoryInitializedEvent dataDirEvent = (GeonetworkDataDirectory.GeonetworkDataDirectoryInitializedEvent) event; final String webappPath = "WEB-INF/data/data/" + SCHEMA_PLUGIN_FORMATTER_DIR; final GeonetworkDataDirectory geonetworkDataDirectory = dataDirEvent.getSource(); final Path fromDir = geonetworkDataDirectory.getWebappDir().resolve(webappPath); final Path toDir = geonetworkDataDirectory.getFormatterDir(); try { copyNewerFilesToDataDir(fromDir, toDir); SchemaManager schemaManager = dataDirEvent.getApplicationContext().getBean(SchemaManager.class); final Set<String> schemas = schemaManager.getSchemas(); for (String schema : schemas) { final String webappSchemaPath = "WEB-INF/data/config/schema_plugins/" + schema + "/" + SCHEMA_PLUGIN_FORMATTER_DIR; final Path webappSchemaDir = geonetworkDataDirectory.getWebappDir().resolve(webappSchemaPath); final Path dataDirSchemaFormatterDir = schemaManager.getSchemaDir(schema) .resolve(SCHEMA_PLUGIN_FORMATTER_DIR); copyNewerFilesToDataDir(webappSchemaDir, dataDirSchemaFormatterDir); } } catch (IOException e) { throw new RuntimeException(e); } } } public void copyNewerFilesToDataDir(final Path fromDir, final Path toDir) throws IOException { if (Files.exists(fromDir)) { Files.walkFileTree(fromDir, new SimpleFileVisitor<Path>() { @Override public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { final Path path = IO.relativeFile(fromDir, file, toDir); if (!file.getFileName().toString().toLowerCase().endsWith(".iml") && (!Files.exists(path) || Files.getLastModifiedTime(path).compareTo(Files.getLastModifiedTime(file)) < 0)) { Files.deleteIfExists(path); IO.copyDirectoryOrFile(file, path, false); } return super.visitFile(file, attrs); } }); } } @RequestMapping(value = { "/api/records/{metadataUuid}/formatters/{formatterId}", "/api/" + API.VERSION_0_1 + "/records/{metadataUuid}/formatters/{formatterId}" }, method = RequestMethod.GET, produces = { MediaType.TEXT_HTML_VALUE, MediaType.APPLICATION_XHTML_XML_VALUE, "application/pdf", MediaType.ALL_VALUE // TODO: PDF }) @ApiOperation(value = "Get a formatted metadata record", nickname = "getRecordFormattedBy") @ResponseBody public void getRecordFormattedBy( @ApiParam(value = "Formatter type to use.") @RequestHeader(value = HttpHeaders.ACCEPT, defaultValue = MediaType.TEXT_HTML_VALUE) String acceptHeader, @PathVariable(value = "formatterId") final String formatterId, @ApiParam(value = API_PARAM_RECORD_UUID, required = true) @PathVariable String metadataUuid, @RequestParam(value = "width", defaultValue = "_100") final FormatterWidth width, @RequestParam(value = "mdpath", required = false) final String mdPath, @RequestParam(value = "output", required = false) FormatType formatType, @ApiIgnore final NativeWebRequest request, final HttpServletRequest servletRequest) throws Exception { ApplicationContext applicationContext = ApplicationContextHolder.get(); Locale locale = languageUtils.parseAcceptLanguage(servletRequest.getLocales()); // TODO : // if text/html > xsl_view // if application/pdf > xsl_view and PDF output // if application/x-gn-<formatterId>+(xml|html|pdf|text) // Force PDF ouutput when URL parameter is set. // This is useful when making GET link to PDF which // can not use headers. if (MediaType.ALL_VALUE.equals(acceptHeader)) { acceptHeader = MediaType.TEXT_HTML_VALUE; } if (formatType == null) { formatType = FormatType.find(acceptHeader); } if (formatType == null) { formatType = FormatType.xml; } final String language = LanguageUtils.locale2gnCode(locale.getISO3Language()); final ServiceContext context = createServiceContext(language, formatType, request.getNativeRequest(HttpServletRequest.class)); AbstractMetadata metadata = ApiUtils.canViewRecord(metadataUuid, servletRequest); Boolean hideWithheld = true; // final boolean hideWithheld = Boolean.TRUE.equals(hide_withheld) || // !context.getBean(AccessManager.class).canEdit(context, resolvedId); Key key = new Key(metadata.getId(), language, formatType, formatterId, hideWithheld, width); final boolean skipPopularityBool = false; ISODate changeDate = metadata.getDataInfo().getChangeDate(); Validator validator; if (changeDate != null) { final long changeDateAsTime = changeDate.toDate().getTime(); long roundedChangeDate = changeDateAsTime / 1000 * 1000; if (request.checkNotModified(language, roundedChangeDate) && context.getBean(CacheConfig.class).allowCaching(key)) { if (!skipPopularityBool) { context.getBean(DataManager.class).increasePopularity(context, String.valueOf(metadata.getId())); } return; } validator = new ChangeDateValidator(changeDateAsTime); } else { validator = new NoCacheValidator(); } final FormatMetadata formatMetadata = new FormatMetadata(context, key, request); byte[] bytes; if (hasNonStandardParameters(request)) { // the http headers can cause a formatter to output custom output due to the parameters. // because it is not known how the parameters may affect the output then we have two choices // 1. make a unique cache for each configuration of parameters // 2. don't cache anything that has extra parameters beyond the standard parameters used to // create the key // #1 has a major flaw because an attacker could simply make new requests always changing the parameters // and completely swamp the cache. So we go with #2. The formatters are pretty fast so it is a fine solution bytes = formatMetadata.call().data; } else { bytes = context.getBean(FormatterCache.class).get(key, validator, formatMetadata, false); } if (bytes != null) { if (!skipPopularityBool) { context.getBean(DataManager.class).increasePopularity(context, String.valueOf(metadata.getId())); } writeOutResponse(context, metadataUuid, locale.getISO3Language(), request.getNativeResponse(HttpServletResponse.class), formatType, bytes); } } /** * @param lang ui language * @param type output type, Must be one of {@link org.fao.geonet.api.records.formatters.FormatType} * @param xslid the id of the formatter * @param metadata the xml to format (either metadata or url must be defined) * @param url a url to call and format either metadata or url must be defined) * @param schema the schema of the xml retrieved from the url or of the metadata xml * @param width the approximate size of the element that the formatter output will be * embedded in compared to the full device width. Allowed options are the enum * values: {@link org.fao.geonet.api.records.formatters.FormatterWidth} The * default is _100 (100% of the screen) * @param mdPath (optional) the xpath to the metadata node if it's not the root node of the * XML */ @RequestMapping(value = "/{lang}/xml.format.{type}") @ResponseBody @Deprecated public void execXml(@PathVariable final String lang, @PathVariable final String type, @RequestParam(value = "xsl", required = false) final String xslid, @RequestParam(value = "metadata", required = false) String metadata, @RequestParam(value = "url", required = false) final String url, @RequestParam(value = "schema") final String schema, @RequestParam(value = "width", defaultValue = "_100") final FormatterWidth width, @RequestParam(value = "mdpath", required = false) final String mdPath, final NativeWebRequest request) throws Exception { if (url == null && metadata == null) { throw new IllegalArgumentException("Either the metadata or url parameter must be declared."); } if (url != null && metadata != null) { throw new IllegalArgumentException("Only one of metadata or url parameter must be declared."); } FormatType formatType = FormatType.valueOf(type.toLowerCase()); final ServiceContext context = createServiceContext(lang, formatType, request.getNativeRequest(HttpServletRequest.class)); if (metadata == null) { metadata = getXmlFromUrl(context, lang, url, request); } Element metadataEl = Xml.loadString(metadata, false); if (mdPath != null) { final List<Namespace> namespaces = context.getBean(SchemaManager.class).getSchema(schema) .getNamespaces(); metadataEl = Xml.selectElement(metadataEl, mdPath, namespaces); metadataEl.detach(); } Metadata metadataInfo = new Metadata(); metadataInfo.setData(metadata).setId(1).setUuid("uuid"); metadataInfo.getDataInfo().setType(MetadataType.METADATA).setRoot(metadataEl.getQualifiedName()) .setSchemaId(schema); Pair<FormatterImpl, FormatterParams> result = createFormatterAndParams(lang, formatType, xslid, width, request, context, metadataEl, metadataInfo); final String formattedMetadata = result.one().format(result.two()); byte[] bytes = formattedMetadata.getBytes(Constants.CHARSET); writeOutResponse(context, "", lang, request.getNativeResponse(HttpServletResponse.class), formatType, bytes); } /** * This service will read directly from the cache and return the value. If it is not in the * cache then a 404 will be returned. * * This is a service to use if there is process to keep the cache at least periodically * up-to-date and if maximum performance is required. */ @RequestMapping(value = "/{lang}/md.format.public.{type}") public HttpEntity<byte[]> getCachedPublicMetadata(@PathVariable final String lang, @PathVariable final String type, @RequestParam(required = false) final String id, @RequestParam(value = "uuid", required = false) final String uuid, @RequestParam(value = "xsl", required = false) final String xslid) throws Exception { final FormatType formatType = FormatType.valueOf(type.toLowerCase()); FormatterCache formatterCache = ApplicationContextHolder.get().getBean(FormatterCache.class); String resolvedId = resolveId(id, uuid); Key key = new Key(Integer.parseInt(resolvedId), lang, formatType, xslid, true, FormatterWidth._100); byte[] bytes = formatterCache.getPublished(key); if (bytes != null) { return new HttpEntity<>(bytes); } return null; } /** * Run the a formatter against a metadata. * * @param lang ui language * @param type output type, Must be one of {@link org.fao.geonet.api.records.formatters.FormatType} * @param id the id, uuid or fileIdentifier of the metadata * @param xslid the id of the formatter * @param skipPopularity if true then don't increment popularity * @param hide_withheld if true hideWithheld (private) elements even if the current user would * normally have access to them. * @param width the approximate size of the element that the formatter output will be * embedded in compared to the full device width. Allowed options are the * enum values: {@link org.fao.geonet.api.records.formatters.FormatterWidth} * The default is _100 (100% of the screen) */ @RequestMapping(value = "/{lang}/md.format.{type}") @ResponseBody public void exec(@PathVariable final String lang, @PathVariable final String type, @RequestParam(value = "id", required = false) final String id, @RequestParam(value = "uuid", required = false) final String uuid, @RequestParam(value = "xsl", required = false) final String xslid, @RequestParam(defaultValue = "n") final String skipPopularity, @RequestParam(value = "hide_withheld", required = false) final Boolean hide_withheld, @RequestParam(value = "width", defaultValue = "_100") final FormatterWidth width, final NativeWebRequest request) throws Exception { final FormatType formatType = FormatType.valueOf(type.toLowerCase()); String resolvedId = resolveId(id, uuid); ServiceContext context = createServiceContext(lang, formatType, request.getNativeRequest(HttpServletRequest.class)); Lib.resource.checkPrivilege(context, resolvedId, ReservedOperation.view); final boolean hideWithheld = Boolean.TRUE.equals(hide_withheld) || !context.getBean(AccessManager.class).canEdit(context, resolvedId); Key key = new Key(Integer.parseInt(resolvedId), lang, formatType, xslid, hideWithheld, width); final boolean skipPopularityBool = new ParamValue(skipPopularity).toBool(); ISODate changeDate = context.getBean(SearchManager.class).getDocChangeDate(resolvedId); Validator validator; if (changeDate != null) { final long changeDateAsTime = changeDate.toDate().getTime(); long roundedChangeDate = changeDateAsTime / 1000 * 1000; if (request.checkNotModified(roundedChangeDate) && context.getBean(CacheConfig.class).allowCaching(key)) { if (!skipPopularityBool) { context.getBean(DataManager.class).increasePopularity(context, resolvedId); } return; } validator = new ChangeDateValidator(changeDateAsTime); } else { validator = new NoCacheValidator(); } final FormatMetadata formatMetadata = new FormatMetadata(context, key, request); byte[] bytes; if (hasNonStandardParameters(request)) { // the http headers can cause a formatter to output custom output due to the parameters. // because it is not known how the parameters may affect the output then we have two choices // 1. make a unique cache for each configuration of parameters // 2. don't cache anything that has extra parameters beyond the standard parameters used to // create the key // #1 has a major flaw because an attacker could simply make new requests always changing the parameters // and completely swamp the cache. So we go with #2. The formatters are pretty fast so it is a fine solution bytes = formatMetadata.call().data; } else { bytes = context.getBean(FormatterCache.class).get(key, validator, formatMetadata, false); } if (bytes != null) { if (!skipPopularityBool) { context.getBean(DataManager.class).increasePopularity(context, resolvedId); } writeOutResponse(context, resolvedId, lang, request.getNativeResponse(HttpServletResponse.class), formatType, bytes); } } private void writeOutResponse(ServiceContext context, String metadataUuid, String lang, HttpServletResponse response, FormatType formatType, byte[] formattedMetadata) throws Exception { response.setContentType(formatType.contentType); String filename = "metadata." + metadataUuid + formatType; response.addHeader("Content-Disposition", "inline; filename=\"" + filename + "\""); response.setStatus(HttpServletResponse.SC_OK); if (formatType == FormatType.pdf) { writerAsPDF(context, response, formattedMetadata, lang); } else { response.setCharacterEncoding(Constants.ENCODING); response.setContentType("text/html"); response.setContentLength(formattedMetadata.length); response.setHeader("Cache-Control", "no-cache"); response.getOutputStream().write(formattedMetadata); } } private boolean hasNonStandardParameters(NativeWebRequest request) { Iterator<String> iter = request.getParameterNames(); while (iter.hasNext()) { if (!ALLOWED_PARAMETERS.contains(iter.next())) { return true; } } return false; } private String getXmlFromUrl(ServiceContext context, String lang, String url, WebRequest request) throws IOException, URISyntaxException { String adjustedUrl = url; if (!url.startsWith("http")) { adjustedUrl = context.getBean(SettingManager.class).getSiteURL(lang) + url; } else { final URI uri = new URI(url); Set allowedRemoteHosts = context.getApplicationContext().getBean("formatterRemoteFormatAllowedHosts", Set.class); Assert.isTrue(allowedRemoteHosts.contains(uri.getHost()), "xml.format is not allowed to make requests to " + uri.getHost()); } HttpUriRequest getXmlRequest = new HttpGet(adjustedUrl); final Iterator<String> headerNames = request.getHeaderNames(); while (headerNames.hasNext()) { String headerName = headerNames.next(); final String[] headers = request.getHeaderValues(headerName); for (String header : headers) { getXmlRequest.addHeader(headerName, header); } } GeonetHttpRequestFactory requestFactory = context.getBean(GeonetHttpRequestFactory.class); final ClientHttpResponse execute = requestFactory.execute(getXmlRequest); if (execute.getRawStatusCode() != 200) { throw new IllegalArgumentException("Request " + adjustedUrl + " did not succeed. Response Status: " + execute.getStatusCode() + ", status text: " + execute.getStatusText()); } return new String(ByteStreams.toByteArray(execute.getBody()), Constants.CHARSET); } private void writerAsPDF(ServiceContext context, HttpServletResponse response, byte[] bytes, String lang) throws IOException, com.itextpdf.text.DocumentException { final String htmlContent = new String(bytes, Constants.CHARSET); try { XslUtil.setNoScript(); ITextRenderer renderer = new ITextRenderer(); String siteUrl = context.getBean(SettingManager.class).getSiteURL(lang); renderer.getSharedContext().setReplacedElementFactory(new ImageReplacedElementFactory(siteUrl, renderer.getSharedContext().getReplacedElementFactory())); renderer.getSharedContext().setDotsPerPixel(13); renderer.setDocumentFromString(htmlContent, siteUrl); renderer.layout(); renderer.createPDF(response.getOutputStream()); } catch (final Exception e) { Log.error(Geonet.FORMATTER, "Error converting formatter output to a file: " + htmlContent, e); throw e; } } @VisibleForTesting Pair<FormatterImpl, FormatterParams> loadMetadataAndCreateFormatterAndParams(ServiceContext context, Key key, final NativeWebRequest request) throws Exception { final Pair<Element, AbstractMetadata> elementMetadataPair = getMetadata(context, key.mdId, key.hideWithheld); Element metadata = elementMetadataPair.one(); AbstractMetadata metadataInfo = elementMetadataPair.two(); return createFormatterAndParams(key.lang, key.formatType, key.formatterId, key.width, request, context, metadata, metadataInfo); } private ServiceContext createServiceContext(String lang, FormatType type, HttpServletRequest request) { final ServiceManager serviceManager = ApplicationContextHolder.get().getBean(ServiceManager.class); return serviceManager.createServiceContext("metadata.formatter" + type, lang, request); } private Pair<FormatterImpl, FormatterParams> createFormatterAndParams(String lang, FormatType type, String xslid, FormatterWidth width, NativeWebRequest request, ServiceContext context, Element metadata, AbstractMetadata metadataInfo) throws Exception { final String schema = metadataInfo.getDataInfo().getSchemaId(); Path schemaDir = null; if (schema != null) { schemaDir = context.getBean(SchemaManager.class).getSchemaDir(schema); } GeonetworkDataDirectory geonetworkDataDirectory = context.getBean(GeonetworkDataDirectory.class); Path formatDir = getAndVerifyFormatDir(geonetworkDataDirectory, "xsl", xslid, schemaDir); ConfigFile config = new ConfigFile(formatDir, true, schemaDir); if (!isCompatibleMetadata(schema, config)) { throw new IllegalArgumentException("The bundle cannot format metadata with the " + schema + " schema"); } FormatterParams fparams = new FormatterParams(); fparams.config = config; fparams.format = this; fparams.webRequest = request; fparams.context = context; fparams.formatDir = formatDir.toRealPath(); fparams.metadata = metadata; fparams.schema = schema; fparams.schemaDir = schemaDir; fparams.formatType = type; fparams.url = context.getBean(SettingManager.class).getSiteURL(lang); fparams.metadataInfo = metadataInfo; fparams.width = width; fparams.formatterInSchemaPlugin = isFormatterInSchemaPlugin(formatDir, schemaDir); Path viewXslFile = formatDir.resolve(FormatterConstants.VIEW_XSL_FILENAME); Path viewGroovyFile = formatDir.resolve(FormatterConstants.VIEW_GROOVY_FILENAME); FormatterImpl formatter; if (Files.exists(viewXslFile)) { fparams.viewFile = viewXslFile.toRealPath(); formatter = context.getBean(XsltFormatter.class); } else if (Files.exists(viewGroovyFile)) { fparams.viewFile = viewGroovyFile.toRealPath(); formatter = context.getBean(GroovyFormatter.class); } else { throw new IllegalArgumentException("The 'xsl' parameter must be a valid id of a formatter"); } return Pair.read(formatter, fparams); } private synchronized boolean isFormatterInSchemaPlugin(Path formatterDir, Path schemaDir) throws IOException { final Path canonicalPath = formatterDir.toRealPath(); Boolean isInSchemaPlugin = this.isFormatterInSchemaPluginMap.get(canonicalPath); if (isInSchemaPlugin == null) { isInSchemaPlugin = false; Path current = formatterDir; while (current.getParent() != null && Files.exists(current.getParent())) { if (current.equals(schemaDir)) { isInSchemaPlugin = true; break; } current = current.getParent(); } this.isFormatterInSchemaPluginMap.put(canonicalPath, isInSchemaPlugin); } return isInSchemaPlugin; } public Pair<Element, AbstractMetadata> getMetadata(ServiceContext context, int id, Boolean hide_withheld) throws Exception { AbstractMetadata md = loadMetadata(context.getBean(IMetadataUtils.class), id); XmlSerializer serializer = context.getBean(XmlSerializer.class); boolean doXLinks = serializer.resolveXLinks(); Element metadata = serializer.removeHiddenElements(false, md, false); if (doXLinks) Processor.processXLink(metadata, context); boolean withholdWithheldElements = hide_withheld != null && hide_withheld; if (XmlSerializer.getThreadLocal(false) != null || withholdWithheldElements) { XmlSerializer.getThreadLocal(true).setForceFilterEditOperation(withholdWithheldElements); } return Pair.read(metadata, md); } private boolean isCompatibleMetadata(String schemaName, ConfigFile config) throws Exception { List<String> applicable = config.listOfApplicableSchemas(); return applicable.contains(schemaName) || applicable.contains("all"); } Element getStrings(Path appPath, String lang) throws IOException, JDOMException { Path baseLoc = appPath.resolve("loc"); Path locDir = findLocDir(lang, baseLoc); if (Files.exists(locDir)) { return Xml.loadFile(locDir.resolve("xml").resolve("strings.xml")); } return new Element("strings"); } /** * Get the localization files from current format plugin. It will load all xml file in the * loc/lang/ directory as children of the returned element. */ public synchronized Element getPluginLocResources(ServiceContext context, Path formatDir, String lang) throws Exception { final Element pluginLocResources = getPluginLocResources(context, formatDir); Element translations = pluginLocResources.getChild(lang); if (translations == null) { if (pluginLocResources.getChildren().isEmpty()) { translations = new Element(lang); } else { translations = (Element) pluginLocResources.getChildren().get(0); } } return translations; } public synchronized Element getPluginLocResources(final ServiceContext context, Path formatDir) throws Exception { final String formatDirPath = formatDir.toString(); Element allLangResources = this.pluginLocs.get(formatDirPath); if (isDevMode(context) || allLangResources == null) { allLangResources = new Element("loc"); Path baseLoc = formatDir.resolve("loc"); if (Files.exists(baseLoc)) { final Element finalAllLangResources = allLangResources; Files.walkFileTree(baseLoc, new SimpleFileVisitor<Path>() { private void addTranslations(String locDirName, Element fileElements) { if (locDirName != null && !locDirName.isEmpty()) { Element resources = finalAllLangResources.getChild(locDirName); if (resources == null) { resources = new Element(locDirName); finalAllLangResources.addContent(resources); } resources.addContent(fileElements); } } @Override public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { if (file.getFileName().toString().toLowerCase().endsWith(".xml")) { try { final Element fileElements = Xml.loadFile(file); final String fileName = getNameWithoutExtension(file.getFileName().toString()); fileElements.setName(fileName); final String locDirName = getNameWithoutExtension( file.getParent().getFileName().toString()); addTranslations(locDirName, fileElements); } catch (JDOMException e) { throw new RuntimeException(e); } } else if (file.getFileName().toString().toLowerCase().endsWith(".json")) { try { final String fileName = getNameWithoutExtension(file.getFileName().toString()); final String[] nameParts = fileName.split("-", 2); IsoLanguagesMapper isoLanguagesMapper = context.getBean(IsoLanguagesMapper.class); String lang = isoLanguagesMapper.iso639_1_to_iso639_2(nameParts[0].toLowerCase(), nameParts[0]); final JSONObject json = new JSONObject( new String(Files.readAllBytes(file), Constants.CHARSET)); Element fileElements = new Element(nameParts[1]); final Iterator keys = json.keys(); while (keys.hasNext()) { String key = (String) keys.next(); fileElements.addContent(new Element(key).setText(json.getString(key))); } addTranslations(lang, fileElements); } catch (JSONException e) { throw new RuntimeException(e); } } return super.visitFile(file, attrs); } }); } this.pluginLocs.put(formatDirPath, allLangResources); } return allLangResources; } private Path findLocDir(String lang, Path baseLoc) throws IOException { Path locDir = baseLoc.resolve(lang); if (!Files.exists(locDir)) { locDir = baseLoc.resolve(Geonet.DEFAULT_LANGUAGE); } if (!Files.exists(locDir) && Files.exists(baseLoc)) { try (DirectoryStream<Path> paths = Files.newDirectoryStream(baseLoc)) { final Iterator<Path> pathIterator = paths.iterator(); if (pathIterator.hasNext()) { locDir = pathIterator.next(); } } } return locDir; } protected boolean isDevMode(ServiceContext context) { return context.getApplicationContext().getBean(SystemInfo.class).isDevMode(); } private class FormatMetadata implements Callable<StoreInfoAndDataLoadResult> { private final Key key; private final NativeWebRequest request; private final ServiceContext serviceContext; public FormatMetadata(ServiceContext context, Key key, NativeWebRequest request) { this.key = key; this.request = request; this.serviceContext = context; } @Override public StoreInfoAndDataLoadResult call() throws Exception { serviceContext.setAsThreadLocal(); Pair<FormatterImpl, FormatterParams> result = loadMetadataAndCreateFormatterAndParams(serviceContext, key, request); FormatterImpl formatter = result.one(); FormatterParams fparams = result.two(); final String formattedMetadata = formatter.format(fparams); byte[] bytes = formattedMetadata.getBytes(Constants.CHARSET); long changeDate = fparams.metadataInfo.getDataInfo().getChangeDate().toDate().getTime(); final Specification<OperationAllowed> isPublished = OperationAllowedSpecs .isPublic(ReservedOperation.view); final Specification<OperationAllowed> hasMdId = OperationAllowedSpecs.hasMetadataId(key.mdId); final OperationAllowed one = serviceContext.getBean(OperationAllowedRepository.class) .findOne(where(hasMdId).and(isPublished)); final boolean isPublishedMd = one != null; Key withheldKey = null; FormatMetadata loadWithheld = null; if (!key.hideWithheld && isPublishedMd) { withheldKey = new Key(key.mdId, key.lang, key.formatType, key.formatterId, true, key.width); loadWithheld = new FormatMetadata(serviceContext, withheldKey, request); } return new StoreInfoAndDataLoadResult(bytes, changeDate, isPublishedMd, withheldKey, loadWithheld); } } }