/* * 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.core.io; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.net.URI; import java.util.ArrayList; import java.util.Collection; import java.util.HashSet; import java.util.List; import java.util.Set; import javax.xml.namespace.QName; import org.eclipse.core.runtime.content.IContentType; import org.eclipse.core.runtime.content.IContentTypeManager; import org.w3c.dom.Element; import com.google.common.base.Preconditions; import de.fhg.igd.eclipse.util.extension.ExtensionObjectFactoryCollection; import de.fhg.igd.eclipse.util.extension.FactoryFilter; import de.fhg.igd.slf4jplus.ALogger; import de.fhg.igd.slf4jplus.ALoggerFactory; import eu.esdihumboldt.hale.common.core.HalePlatform; import eu.esdihumboldt.hale.common.core.io.extension.ComplexValueDefinition; import eu.esdihumboldt.hale.common.core.io.extension.ComplexValueExtension; import eu.esdihumboldt.hale.common.core.io.extension.IOProviderDescriptor; import eu.esdihumboldt.hale.common.core.io.extension.IOProviderExtension; import eu.esdihumboldt.hale.common.core.io.supplier.LookupStreamResource; import eu.esdihumboldt.util.Pair; import eu.esdihumboldt.util.io.InputSupplier; import eu.esdihumboldt.util.resource.Resources; /** * Hale I/O utilities * * @author Simon Templer * @partner 01 / Fraunhofer Institute for Computer Graphics Research * @since 2.5 */ public abstract class HaleIO { private static final ALogger log = ALoggerFactory.getLogger(HaleIO.class); /** * Namespace for HALE core complex value type elements. */ public static final String NS_HALE_CORE = "http://www.esdi-humboldt.eu/hale/core"; /** * Tests whether a InputStream to the given URI can be opened. <br> * In case of a file it instead tests File.isFile and File.canRead() because * it is a lot faster. * * @param uri the URI to test * @param allowResource allow resolving through {@link Resources} * @return true, if a InputStream to the URI could be opened. */ public static boolean testStream(URI uri, boolean allowResource) { if ("file".equalsIgnoreCase(uri.getScheme())) { File file = new File(uri); if (file.isFile() && file.canRead()) return true; return false; } // try resolving through local resources if (allowResource && Resources.tryResolve(uri, null) != null) { return true; } // could be further enhanced to check for example for http response // codes like 404. try { uri.toURL().openConnection().getInputStream().close(); } catch (Exception e) { return false; } return true; } /** * Filter I/O provider factories by content type * * @param factories the I/O provider factories * @param contentType the content type factories must support * @return provider factories that support the given content type */ public static List<IOProviderDescriptor> filterFactories( Collection<IOProviderDescriptor> factories, IContentType contentType) { List<IOProviderDescriptor> result = new ArrayList<IOProviderDescriptor>(); for (IOProviderDescriptor factory : factories) { Set<IContentType> supportedTypes = factory.getSupportedTypes(); // check if contentType is supported for (IContentType test : supportedTypes) { if (isCompatibleContentType(test, contentType)) { result.add(factory); break; } } } return result; } /** * Filter I/O provider factories by configuration content type * * @param factories the I/O provider factories * @param configurationContentType the configuration content type the * factories must support * @return provider factories that support the given configuration content * type */ public static List<IOProviderDescriptor> filterFactoriesByConfigurationType( Collection<IOProviderDescriptor> factories, IContentType configurationContentType) { List<IOProviderDescriptor> result = new ArrayList<IOProviderDescriptor>(); for (IOProviderDescriptor factory : factories) { Set<IContentType> supportedTypes = factory.getConfigurationTypes(); // check if contentType is supported for (IContentType test : supportedTypes) { if (isCompatibleContentType(test, configurationContentType)) { result.add(factory); break; } } } return result; } /** * Find the content types that match the given file name and/or input. * * NOTE: The implementation should try to restrict the result to one content * type and only use the input supplier if absolutely needed. * * @param types the types to match * @param in the input supplier to use for testing, may be <code>null</code> * if the file name is not <code>null</code> * @param filename the file name, may be <code>null</code> if the input * supplier is not <code>null</code> * @return the matched content types */ public static List<IContentType> findContentTypesFor(Collection<IContentType> types, InputSupplier<? extends InputStream> in, String filename) { Preconditions.checkArgument(filename != null || in != null, "At least one of input supplier and file name must not be null"); List<IContentType> results = new ArrayList<IContentType>(); if (filename != null && !filename.isEmpty()) { // test file extension String lowerFile = filename.toLowerCase(); for (IContentType type : types) { String[] extensions = type.getFileSpecs(IContentType.FILE_EXTENSION_SPEC); boolean match = false; for (int i = 0; i < extensions.length && !match; i++) { if (lowerFile.endsWith("." + extensions[i].toLowerCase())) { match = true; } } if (match) { results.add(type); } } } if ((results.isEmpty() || results.size() > 1) && in != null) { // remember previous results List<IContentType> extensionResults = null; if (!results.isEmpty()) { extensionResults = new ArrayList<>(results); } // clear results because only an ambiguous result was found results.clear(); // use input stream to make a better test IContentTypeManager ctm = HalePlatform.getContentTypeManager(); try { InputStream is = in.getInput(); /* * IContentTypeManager.findContentTypes seems to return all kind * of content types that match in any way, but ordered by * relevance - so if all but the allowed types are removed, the * remaining types may be very irrelevant and not a match that * actually was determined based on the input stream. * * Thus findContentTypesFor should not be used or only relied * upon the single best match that is returned. It is now only * used as fall-back if there is no result for * findContentTypeFor. */ // instead use findContentTypeFor IContentType candidate = ctm.findContentTypeFor(is, null); if (types.contains(candidate)) { results.add(candidate); } is.close(); } catch (IOException e) { log.warn("Could not read input to determine content type", e); } if (results.isEmpty()) { /* * Fall-back to findContentTypesFor if there was no valid result * for findContentTypeFor. */ try (InputStream is = in.getInput()) { IContentType[] candidates = ctm.findContentTypesFor(is, filename); for (IContentType candidate : candidates) { if (types.contains(candidate)) { results.add(candidate); } } } catch (IOException e) { log.warn("Could not read input to determine content type", e); } } if (results.isEmpty() && extensionResults != null) { /* * If there was no valid result for the stream check, but for * the extension checks, use those results. This may happen for * instance for generic XML files, that have no specific root * element. */ results.addAll(extensionResults); } } return results; } /** * Get the I/O provider factories of a certain type * * @param providerType the provider type, usually an interface * @return the factories currently registered in the system */ public static <P extends IOProvider> Collection<IOProviderDescriptor> getProviderFactories( final Class<P> providerType) { return IOProviderExtension.getInstance() .getFactories(new FactoryFilter<IOProvider, IOProviderDescriptor>() { @Override public boolean acceptFactory(IOProviderDescriptor descriptor) { return descriptor != null && descriptor.getProviderType() != null && providerType.isAssignableFrom(descriptor.getProviderType()); } @Override public boolean acceptCollection( ExtensionObjectFactoryCollection<IOProvider, IOProviderDescriptor> collection) { return true; } }); } /** * Find an I/O provider factory * * @param * <P> * the provider interface type * * @param providerType the provider type, usually an interface * @param contentType the content type the provider must match, may be * <code>null</code> if providerId is set * @param providerId the id of the provider to use, may be <code>null</code> * if contentType is set * @return the I/O provider factory or <code>null</code> if no matching I/O * provider factory is found */ public static <P extends IOProvider> IOProviderDescriptor findIOProviderFactory( Class<P> providerType, IContentType contentType, String providerId) { Preconditions.checkArgument(contentType != null || providerId != null); Collection<IOProviderDescriptor> factories = getProviderFactories(providerType); if (contentType != null) { factories = filterFactories(factories, contentType); } IOProviderDescriptor result = null; if (providerId != null) { for (IOProviderDescriptor factory : factories) { if (factory.getIdentifier().equals(providerId)) { result = factory; break; } } } else { // TODO choose priority based? if (!factories.isEmpty()) { result = factories.iterator().next(); } } return result; } /** * Creates an I/O provider instance * * @param * <P> * the provider interface type * * @param providerType the provider type, usually an interface * @param contentType the content type the provider must match, may be * <code>null</code> if providerId is set * @param providerId the id of the provider to use, may be <code>null</code> * if contentType is set * @return the I/O provider preconfigured with the content type if it was * given or <code>null</code> if no matching I/O provider is found */ @SuppressWarnings("unchecked") public static <P extends IOProvider> P createIOProvider(Class<P> providerType, IContentType contentType, String providerId) { IOProviderDescriptor factory = findIOProviderFactory(providerType, contentType, providerId); P result; try { result = (P) ((factory == null) ? (null) : (factory.createExtensionObject())); } catch (Exception e) { throw new RuntimeException("Could not create I/O provider", e); } if (result != null && contentType != null) { result.setContentType(contentType); } return result; } /** * Find the content type for the given input * * @param * <P> * the provider interface type * * @param providerType the provider type, usually an interface * @param in the input supplier to use for testing, may be <code>null</code> * if the file name is not <code>null</code> * @param filename the file name, may be <code>null</code> if the input * supplier is not <code>null</code> * @return the content type or <code>null</code> if no matching content type * is found */ public static <P extends IOProvider> IContentType findContentType(Class<P> providerType, InputSupplier<? extends InputStream> in, String filename) { Collection<IOProviderDescriptor> providers = getProviderFactories(providerType); // collect supported content types Set<IContentType> supportedTypes = new HashSet<IContentType>(); for (IOProviderDescriptor factory : providers) { supportedTypes.addAll(factory.getSupportedTypes()); } // find matching content type List<IContentType> types = findContentTypesFor(supportedTypes, in, filename); if (types == null || types.isEmpty()) { return null; } // TODO choose? return types.iterator().next(); } /** * Find an I/O provider instance for the given input * * @param * <P> * the provider interface type * * @param providerType the provider type, usually an interface * @param in the input supplier to use for testing, may be <code>null</code> * if the file name is not <code>null</code> * @param filename the file name, may be <code>null</code> if the input * supplier is not <code>null</code> * @return the I/O provider or <code>null</code> if no matching I/O provider * is found */ public static <P extends IOProvider> P findIOProvider(Class<P> providerType, InputSupplier<? extends InputStream> in, String filename) { IContentType contentType = findContentType(providerType, in, filename); if (contentType == null) { return null; } return HaleIO.createIOProvider(providerType, contentType, null); } /** * Find an I/O provider instance for the given input * * @param <T> the provider interface type * * @param providerType the provider type, usually an interface * @param in the input supplier to use for testing, may be <code>null</code> * if the file name is not <code>null</code> * @param filename the file name, may be <code>null</code> if the input * supplier is not <code>null</code> * @return a pair with the I/O provider and the corresponding identifier, * both are <code>null</code> if no matching I/O provider was found */ @SuppressWarnings("unchecked") public static <T extends IOProvider> Pair<T, String> findIOProviderAndId(Class<T> providerType, InputSupplier<? extends InputStream> in, String filename) { T reader = null; String providerId = null; IContentType contentType = HaleIO.findContentType(providerType, in, filename); if (contentType != null) { IOProviderDescriptor factory = HaleIO.findIOProviderFactory(providerType, contentType, null); try { reader = (T) factory.createExtensionObject(); providerId = factory.getIdentifier(); } catch (Exception e) { throw new RuntimeException("Could not create I/O provider", e); } if (reader != null) { reader.setContentType(contentType); } } return new Pair<T, String>(reader, providerId); } /** * Automatically find an import provider to load a resource that is * available through an input stream that can only be read once. * * @param type the import provider type * @param in the input stream * @return the import provider or <code>null</code> if none was found */ public static <T extends ImportProvider> T findImportProvider(Class<T> type, InputStream in) { LookupStreamResource res = new LookupStreamResource(in, null, 64 * 1024); T provider = HaleIO.findIOProvider(type, res.getLookupSupplier(), null); if (provider != null) { provider.setSource(res.getInputSupplier()); return provider; } return null; } /** * Test if the given value content type is compatible with the given parent * content type * * @param parentType the parent content type * @param valueType the value content type * @return if the value content type is compatible with the parent content * type */ public static boolean isCompatibleContentType(IContentType parentType, IContentType valueType) { return valueType.isKindOf(parentType); } /** * Get the value of a complex property represented as a DOM element. * * @param element the DOM element * @param context the context object, may be <code>null</code> * @return the complex value converted through the * {@link ComplexValueExtension}, or the original element */ public static Object getComplexValue(Element element, Object context) { QName name; if (element.getNamespaceURI() != null && !element.getNamespaceURI().isEmpty()) { name = new QName(element.getNamespaceURI(), element.getLocalName()); } else { name = new QName(element.getTagName()); // .getLocalName()); } ComplexValueDefinition cvt = ComplexValueExtension.getInstance().getDefinition(name); if (cvt != null) { // create and return the complex parameter value return cvt.fromDOM(element, cvt.getContextType().isInstance(context) ? context : null); } // the element itself is the complex value return element; } /** * Get the value of a complex property represented as a DOM element. * * @param element the DOM element * @param expectedType the expected parameter type, this must be either * {@link String}, DOM {@link Element} or a complex value type * defined in the {@link ComplexValueExtension} * @param context the context object, may be <code>null</code> * @return the complex value or <code>null</code> if it could not be created * from the element */ @SuppressWarnings("unchecked") public static <T> T getComplexValue(Element element, Class<T> expectedType, Object context) { if (element == null) { return null; } QName name; if (element.getNamespaceURI() != null && !element.getNamespaceURI().isEmpty()) { name = new QName(element.getNamespaceURI(), element.getLocalName()); } else { String ln = element.getTagName(); // .getLocalName(); name = new QName(ln); } ComplexValueDefinition cvt = ComplexValueExtension.getInstance().getDefinition(name); Object value = null; if (cvt != null) { try { value = cvt.fromDOM(element, cvt.getContextType().isInstance(context) ? context : null); } catch (Exception e) { throw new IllegalStateException("Failed to load complex value from DOM", e); } } if (value != null && expectedType.isAssignableFrom(value.getClass())) { return (T) value; } // maybe the element itself is OK if (expectedType.isAssignableFrom(element.getClass())) { return (T) element; } if (expectedType.isAssignableFrom(String.class)) { // FIXME use legacy complex value if possible } return null; } /** * Get the representation of a complex value as a DOM element. Uses the * {@link ComplexValueExtension}. * * @param value the complex value * @return the DOM representation * @throws IllegalStateException if the value is neither a DOM element nor * can be converted to one using the * {@link ComplexValueExtension} */ public static Element getComplexElement(Object value) { if (value instanceof Element) { // as is return (Element) value; } ComplexValueDefinition cvd = ComplexValueExtension.getInstance() .getDefinition(value.getClass()); if (cvd != null) { return cvd.toDOM(value); } throw new IllegalStateException("No definition for complex parameter value found"); } // /** // * Get the file extensions for the given content type // * // * @param contentType the content type // * @param prefix the prefix to add before the extensions, e.g. "." or "*.", // * may be <code>null</code> // * @return the file extensions or <code>null</code> // */ // public static String[] getFileExtensions(ContentType contentType, // String prefix) { // SortedSet<String> exts = new TreeSet<String>(); // // ContentTypeService cts = HalePlatform.getService(ContentTypeService.class); // String[] typeExts = cts.getFileExtensions(contentType); // if (typeExts != null) { // for (String typeExt : typeExts) { // if (prefix == null) { // exts.add(typeExt); // } // else { // exts.add(prefix + typeExt); // } // } // } // // if (exts.isEmpty()) { // return null; // } // else { // return exts.toArray(new String[exts.size()]); // } // } // /** // * Get all file extensions for the given content types // * // * @param contentTypes the content types // * @param prefix the prefix to add before the extensions, e.g. "." or "*.", // * may be <code>null</code> // * @return the file extensions or <code>null</code> // */ // public static String[] getFileExtensions(Iterable<ContentType> contentTypes, // String prefix) { // SortedSet<String> exts = new TreeSet<String>(); // // ContentTypeService cts = HalePlatform.getService(ContentTypeService.class); // for (ContentType contentType : contentTypes) { // String[] typeExts = cts.getFileExtensions(contentType); // if (typeExts != null) { // for (String typeExt : typeExts) { // if (prefix == null) { // exts.add(typeExt); // } // else { // exts.add(prefix + typeExt); // } // } // } // } // // if (exts.isEmpty()) { // return null; // } // else { // return exts.toArray(new String[exts.size()]); // } // } }