/* * Copyright 2015 * Ubiquitous Knowledge Processing (UKP) Lab and FG Language Technology * Technische Universität Darmstadt * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package de.tudarmstadt.ukp.clarin.webanno.diag; import java.lang.reflect.Modifier; import java.util.ArrayList; import java.util.List; import java.util.stream.Collectors; import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.exception.ExceptionUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.apache.uima.cas.CAS; import org.reflections.Reflections; import org.springframework.beans.BeansException; import org.springframework.beans.factory.InitializingBean; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextAware; import org.springframework.stereotype.Component; import de.tudarmstadt.ukp.clarin.webanno.diag.checks.Check; import de.tudarmstadt.ukp.clarin.webanno.diag.repairs.Repair; import de.tudarmstadt.ukp.clarin.webanno.model.Project; import de.tudarmstadt.ukp.clarin.webanno.support.SettingsUtil; @Component("casDoctor") public class CasDoctor implements InitializingBean, ApplicationContextAware { private Logger log = LoggerFactory.getLogger(getClass()); @Value(value = "${debug.casDoctor.checks}") private String activeChecks; @Value(value = "${debug.casDoctor.fatal}") private boolean fatalChecks = true; @Value(value = "${debug.casDoctor.repairs}") private String activeRepairs; private ApplicationContext context; private List<Class<? extends Check>> checkClasses = new ArrayList<>(); private List<Class<? extends Repair>> repairClasses = new ArrayList<>(); @Value(value = "${debug.casDoctor.forceReleaseBehavior}") private boolean disableAutoScan = false; public CasDoctor() { // Bean operation } /** * This constructor must only be used for unit tests. */ public CasDoctor(Class<?>... aChecksRepairs) { StringBuilder checks = new StringBuilder(); StringBuilder repairs = new StringBuilder(); for (Class<?> clazz : aChecksRepairs) { boolean isCheck = Check.class.isAssignableFrom(clazz); boolean isRepair = Repair.class.isAssignableFrom(clazz); if (isCheck) { if (checks.length() > 0) { checks.append(','); } checks.append(clazz.getSimpleName()); } if (isRepair) { if (repairs.length() > 0) { repairs.append(','); } repairs.append(clazz.getSimpleName()); } if (!isCheck && !isRepair) { throw new IllegalArgumentException("[" + clazz.getName() + "] is neither a check nor a repair"); } } activeChecks = checks.toString(); fatalChecks = false; activeRepairs = repairs.toString(); // This constructor is only used for tests. In tests we want to control which checks are // used and do not want to auto-scan. disableAutoScan = true; afterPropertiesSet(); } public void setFatalChecks(boolean aFatalChecks) { fatalChecks = aFatalChecks; } public boolean isFatalChecks() { return fatalChecks; } public void repair(Project aProject, CAS aCas) { List<LogMessage> messages = new ArrayList<>(); repair(aProject, aCas, messages); if (log.isWarnEnabled() && !messages.isEmpty()) { messages.forEach(s -> log.warn("{}", s)); } } public boolean isRepairsActive() { return !repairClasses.isEmpty(); } public void repair(Project aProject, CAS aCas, List<LogMessage> aMessages) { // If there are no active repairs, don't do anything if (repairClasses.isEmpty()) { return; } // APPLY REPAIRS long tStart = System.currentTimeMillis(); for (Class<? extends Repair> repairClass : repairClasses) { try { long tStartTask = System.currentTimeMillis(); Repair repair = repairClass.newInstance(); if (context != null) { context.getAutowireCapableBeanFactory().autowireBean(repair); } log.info("CasDoctor repair [" + repairClass.getSimpleName() + "] running..."); repair.repair(aProject, aCas, aMessages); log.info("CasDoctor repair [" + repairClass.getSimpleName() + "] completed in " + (System.currentTimeMillis() - tStartTask) + "ms"); } catch (Exception e) { // aMessages.add(new LogMessage(this, LogLevel.ERROR, "Cannot perform repair [%s]: %s", // repairClass.getSimpleName(), ExceptionUtils.getRootCauseMessage(e))); log.error("Cannot perform repair [" + repairClass.getSimpleName() + "]", e); throw new IllegalStateException("Repair attempt failed - ask system administrator " + "for details."); } } log.info("CasDoctor completed all repairs in " + (System.currentTimeMillis() - tStart) + "ms"); // POST-CONDITION: CAS must be consistent // Ensure that the repairs actually fixed the CAS analyze(aProject, aCas, aMessages, true); } public boolean analyze(Project aProject, CAS aCas) throws CasDoctorException { List<LogMessage> messages = new ArrayList<>(); boolean result = analyze(aProject, aCas, messages); if (log.isDebugEnabled()) { messages.forEach(s -> log.debug("{}", s)); } return result; } public boolean analyze(Project aProject, CAS aCas, List<LogMessage> aMessages) throws CasDoctorException { return analyze(aProject, aCas, aMessages, isFatalChecks()); } public boolean analyze(Project aProject, CAS aCas, List<LogMessage> aMessages, boolean aFatalChecks) throws CasDoctorException { long tStart = System.currentTimeMillis(); boolean ok = true; for (Class<? extends Check> checkClass : checkClasses) { try { long tStartTask = System.currentTimeMillis(); Check check = checkClass.newInstance(); if (context != null) { context.getAutowireCapableBeanFactory().autowireBean(check); } log.debug("CasDoctor analysis [" + checkClass.getSimpleName() + "] running..."); ok &= check.check(aProject, aCas, aMessages); log.debug("CasDoctor analysis [" + checkClass.getSimpleName() + "] completed in " + (System.currentTimeMillis() - tStartTask) + "ms"); } catch (InstantiationException | IllegalAccessException e) { aMessages.add(new LogMessage(this, LogLevel.ERROR, "Cannot instantiate [%s]: %s", checkClass.getSimpleName(), ExceptionUtils.getRootCauseMessage(e))); log.error("Error running check", e); } } if (!ok) { aMessages.forEach(s -> log.error("{}", s)); } if (!ok && aFatalChecks) { throw new CasDoctorException(aMessages); } log.debug("CasDoctor completed all analyses in " + (System.currentTimeMillis() - tStart) + "ms"); return ok; } public void setActiveChecks(String aActiveChecks) { activeChecks = aActiveChecks; } public void setActiveRepairs(String aActiveRepairs) { activeRepairs = aActiveRepairs; } @SuppressWarnings("unchecked") @Override public void afterPropertiesSet() { // If WebAnno is in under development, automatically enable all checks. String version = SettingsUtil.getVersionProperties().getProperty(SettingsUtil.PROP_VERSION); if (!disableAutoScan && ( "unknown".equals(version) || version.contains("-SNAPSHOT") || version.contains("-beta-")) ) { Reflections reflections = new Reflections(Check.class.getPackage().getName()); checkClasses.addAll(reflections.getSubTypesOf(Check.class).stream() .filter(c -> !Modifier.isAbstract(c.getModifiers())) .collect(Collectors.toList())); log.info("Detected SNAPSHOT/beta version - automatically enabling all checks"); } if (StringUtils.isNotBlank(activeChecks)) { for (String check : activeChecks.split(",")) { try { checkClasses.add((Class<? extends Check>) Class.forName(Check.class .getPackage().getName() + "." + check.trim())); } catch (ClassNotFoundException e) { throw new IllegalStateException(e); } } } for (Class<? extends Check> c : checkClasses) { log.info("Check activated: " + c.getSimpleName()); } if (StringUtils.isNotBlank(activeRepairs)) { for (String check : activeRepairs.split(",")) { try { repairClasses.add((Class<? extends Repair>) Class.forName(Repair.class .getPackage().getName() + "." + check.trim())); } catch (ClassNotFoundException e) { throw new IllegalStateException(e); } } } for (Class<? extends Repair> c : repairClasses) { log.info("Repair activated: " + c.getSimpleName()); } } public static enum LogLevel { INFO, ERROR } public static class LogMessage { public final LogLevel level; public final Class<?> source; public final String message; public LogMessage(Object aSource, LogLevel aLevel, String aMessage) { this(aSource, aLevel, "%s", aMessage); } public LogMessage(Object aSource, LogLevel aLevel, String aFormat, Object... aValues) { super(); source = aSource != null ? aSource.getClass() : null; level = aLevel; message = String.format(aFormat, aValues); } @Override public String toString() { return String.format("[%s] %s", source != null ? source.getSimpleName() : "<unknown>", message); } } @Override public void setApplicationContext(ApplicationContext aContext) throws BeansException { context = aContext; } }