/** * Copyright (c) 2014-2017 by the respective copyright holders. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html */ package org.eclipse.smarthome.model.core.internal; import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; import java.text.MessageFormat; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.concurrent.CopyOnWriteArrayList; import org.apache.commons.io.IOUtils; import org.apache.commons.lang.StringUtils; import org.eclipse.emf.common.util.URI; import org.eclipse.emf.ecore.EObject; import org.eclipse.emf.ecore.resource.Resource; import org.eclipse.emf.ecore.resource.Resource.Diagnostic; import org.eclipse.emf.ecore.resource.ResourceSet; import org.eclipse.emf.ecore.util.Diagnostician; import org.eclipse.smarthome.model.core.EventType; import org.eclipse.smarthome.model.core.ModelRepository; import org.eclipse.smarthome.model.core.ModelRepositoryChangeListener; import org.eclipse.xtext.resource.SynchronizedXtextResourceSet; import org.eclipse.xtext.resource.XtextResource; import org.eclipse.xtext.resource.XtextResourceSet; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.google.common.base.Function; import com.google.common.base.Predicate; import com.google.common.collect.Iterables; import com.google.common.collect.Lists; /** * @author Oliver Libutzki - Added reloadAllModelsOfType method * @author Simon Kaufmann - added validation of models before loading them * */ public class ModelRepositoryImpl implements ModelRepository { private final Logger logger = LoggerFactory.getLogger(ModelRepositoryImpl.class); private final ResourceSet resourceSet; private final List<ModelRepositoryChangeListener> listeners = new CopyOnWriteArrayList<>(); public ModelRepositoryImpl() { XtextResourceSet xtextResourceSet = new SynchronizedXtextResourceSet(); xtextResourceSet.addLoadOption(XtextResource.OPTION_RESOLVE_ALL, Boolean.TRUE); this.resourceSet = xtextResourceSet; // don't use XMI as a default Resource.Factory.Registry.INSTANCE.getExtensionToFactoryMap().remove("*"); } @Override public EObject getModel(String name) { synchronized (resourceSet) { Resource resource = getResource(name); if (resource != null) { if (resource.getContents().size() > 0) { return resource.getContents().get(0); } else { logger.warn("Configuration model '{}' is either empty or cannot be parsed correctly!", name); resourceSet.getResources().remove(resource); return null; } } else { logger.trace("Configuration model '{}' can not be found", name); return null; } } } @Override public boolean addOrRefreshModel(String name, final InputStream originalInputStream) { Resource resource = null; try { InputStream inputStream = null; if (originalInputStream != null) { byte[] bytes = IOUtils.toByteArray(originalInputStream); String validationResult = validateModel(name, new ByteArrayInputStream(bytes)); if (validationResult != null) { logger.warn("Configuration model '{}' has errors, therefore ignoring it: {}", name, validationResult); removeModel(name); return false; } inputStream = new ByteArrayInputStream(bytes); } resource = getResource(name); if (resource == null) { synchronized (resourceSet) { // try again to retrieve the resource as it might have been created by now resource = getResource(name); if (resource == null) { // seems to be a new file // don't use XMI as a default Resource.Factory.Registry.INSTANCE.getExtensionToFactoryMap().remove("*"); resource = resourceSet.createResource(URI.createURI(name)); if (resource != null) { logger.info("Loading model '{}'", name); Map<String, String> options = new HashMap<String, String>(); options.put(XtextResource.OPTION_ENCODING, "UTF-8"); if (inputStream == null) { logger.warn( "Resource '{}' not found. You have to pass an inputStream to create the resource.", name); return false; } resource.load(inputStream, options); notifyListeners(name, EventType.ADDED); return true; } else { logger.warn("Ignoring file '{}' as we do not have a parser for it.", name); } } } } else { synchronized (resourceSet) { resource.unload(); logger.info("Refreshing model '{}'", name); if (inputStream != null) { resource.load(inputStream, Collections.EMPTY_MAP); } else { resource.load(Collections.EMPTY_MAP); } notifyListeners(name, EventType.MODIFIED); return true; } } } catch (IOException e) { logger.warn("Configuration model '{}' cannot be parsed correctly!", name, e); if (resource != null) { resourceSet.getResources().remove(resource); } } return false; } @Override public boolean removeModel(String name) { Resource resource = getResource(name); if (resource != null) { synchronized (resourceSet) { // do not physically delete it, but remove it from the resource set notifyListeners(name, EventType.REMOVED); resourceSet.getResources().remove(resource); return true; } } else { return false; } } @Override public Iterable<String> getAllModelNamesOfType(final String modelType) { synchronized (resourceSet) { // Make a copy to avoid ConcurrentModificationException List<Resource> resourceListCopy = new ArrayList<Resource>(resourceSet.getResources()); Iterable<Resource> matchingResources = Iterables.filter(resourceListCopy, new Predicate<Resource>() { @Override public boolean apply(Resource input) { if (input != null && input.getURI().lastSegment().contains(".") && input.isLoaded()) { return modelType.equalsIgnoreCase(input.getURI().fileExtension()); } else { return false; } } }); return Lists.newArrayList(Iterables.transform(matchingResources, new Function<Resource, String>() { @Override public String apply(Resource from) { return from.getURI().path(); } })); } } @Override public void reloadAllModelsOfType(final String modelType) { synchronized (resourceSet) { // Make a copy to avoid ConcurrentModificationException List<Resource> resourceListCopy = new ArrayList<Resource>(resourceSet.getResources()); for (Resource resource : resourceListCopy) { if (resource != null && resource.getURI().lastSegment().contains(".") && resource.isLoaded()) { if (modelType.equalsIgnoreCase(resource.getURI().fileExtension())) { XtextResource xtextResource = (XtextResource) resource; // It's not sufficient to discard the derived state. // The quick & dirts solution is to reparse the whole resource. // We trigger this by dummy updating the resource. logger.debug("Refreshing resource '{}'", resource.getURI().lastSegment()); xtextResource.update(1, 0, ""); notifyListeners(resource.getURI().lastSegment(), EventType.MODIFIED); } } } } } @Override public void addModelRepositoryChangeListener(ModelRepositoryChangeListener listener) { listeners.add(listener); } @Override public void removeModelRepositoryChangeListener(ModelRepositoryChangeListener listener) { listeners.remove(listener); } private Resource getResource(String name) { return resourceSet.getResource(URI.createURI(name), false); } /** * Validates the given model. * * There are two "layers" of validation * <ol> * <li> * errors when loading the resource. Usually these are syntax violations which irritate the parser. They will be * returned as a String. * <li> * all kinds of other errors (i.e. violations of validation checks) will only be logged, but not included in the * return value. * </ol> * <p> * Validation will be done on a separate resource, in order to keep the original one intact in case its content * needs to be removed because of syntactical errors. * * @param name * @param inputStream * @return error messages as a String if any syntactical error were found, <code>null</code> otherwise * @throws IOException if there was an error with the given {@link InputStream}, loading the resource from there */ private String validateModel(String name, InputStream inputStream) throws IOException { // use another resource for validation in order to keep the original one for emergency-removal in case of errors Resource resource = resourceSet.createResource(URI.createURI("tmp_" + name)); try { resource.load(inputStream, Collections.EMPTY_MAP); StringBuilder criticalErrors = new StringBuilder(); List<String> warnings = new LinkedList<>(); if (resource != null && !resource.getContents().isEmpty()) { // Check for syntactical errors for (Diagnostic diagnostic : resource.getErrors()) { criticalErrors.append(MessageFormat.format("[{0},{1}]: {2}\n", diagnostic.getLine(), diagnostic.getColumn(), diagnostic.getMessage())); } if (criticalErrors.length() > 0) { return criticalErrors.toString(); } // Check for validation errors, but log them only try { org.eclipse.emf.common.util.Diagnostic diagnostic = Diagnostician.INSTANCE .validate(resource.getContents().get(0)); for (org.eclipse.emf.common.util.Diagnostic d : diagnostic.getChildren()) { warnings.add(d.getMessage()); } if (warnings.size() > 0) { logger.info("Validation issues found in configuration model '{}', using it anyway:\n{}", name, StringUtils.join(warnings, "\n")); } } catch (NullPointerException e) { // see https://github.com/eclipse/smarthome/issues/3335 logger.debug("Validation of '{}' skipped due to internal errors.", name); } } } finally { resourceSet.getResources().remove(resource); } return null; } private void notifyListeners(String name, EventType type) { for (ModelRepositoryChangeListener listener : listeners) { listener.modelChanged(name, type); } } }