diff --git a/build.gradle b/build.gradle index bdb3ea941..628bdbab4 100644 --- a/build.gradle +++ b/build.gradle @@ -1,6 +1,6 @@ apply plugin: 'application' -project.version = '5.3' +project.version = '5.3.1' sourceCompatibility = '1.17' [compileJava, compileTestJava]*.options*.encoding = 'UTF-8' @@ -331,3 +331,16 @@ fileTree("$projectDir/private").include('*.gradle').each { file -> test { testLogging.showStandardStreams = true } + +// Utility task to print out head templates +task(printTemplates, dependsOn: 'classes', type: JavaExec) { + description = "Print out head templates with various sizes" + mainClass = "org.audiveris.omr.image.TemplateFactory" + classpath = sourceSets.main.runtimeClasspath + + if (project.hasProperty("cmdLineArgs")) { + if (cmdLineArgs) { + args(cmdLineArgs.split()) + } + } +} diff --git a/docs/_pages/advanced/samples.md b/docs/_pages/advanced/samples.md index 0f5a5cfe4..d6a40d945 100644 --- a/docs/_pages/advanced/samples.md +++ b/docs/_pages/advanced/samples.md @@ -80,7 +80,7 @@ repository or to assign it a correct shape. To work on the **GLOBAL** repository, we use the `Tools | Browse Global Repository...` pulldown menu. It will work on the `samples.zip` and `images.zip` files if any are found in our own `config/train` -folder (see [Folders] section). +folder (see [Train folder](../folders/essential.md#train-folder) section). To work on a **book** repository, we use the `Book | View Book Repository...` pulldown menu. We will be able to pick and merge book repositories into the GLOBAL repository later on, @@ -141,6 +141,3 @@ When we are satisfied with a book repository we can push its content to the glob We do so from the book repository interface, by selecting the pulldown menu `Repository | Push to Global`. - - -[Folders]: /folders/essential.md diff --git a/docs/_pages/assets/images/merged_grand_staff.png b/docs/_pages/assets/images/merged_grand_staff.png new file mode 100644 index 000000000..8a8dbea1c Binary files /dev/null and b/docs/_pages/assets/images/merged_grand_staff.png differ diff --git a/docs/_pages/handbook.md b/docs/_pages/handbook.md index 4e85a338d..3ee53507e 100644 --- a/docs/_pages/handbook.md +++ b/docs/_pages/handbook.md @@ -93,3 +93,6 @@ Features only relevant for an advanced usage of Audiveris. 1. [References](./references.md): Not meant to be read from A to Z, but when a specific item needs to be checked. + +1. [Updates](./updates.md): +History of major software updates. diff --git a/schemas/src/main/java/org/audiveris/schema/MyJavaDocExtractor.java b/schemas/src/main/java/org/audiveris/schema/MyJavaDocExtractor.java index a25899b63..a009c533b 100644 --- a/schemas/src/main/java/org/audiveris/schema/MyJavaDocExtractor.java +++ b/schemas/src/main/java/org/audiveris/schema/MyJavaDocExtractor.java @@ -1,611 +1,638 @@ -//------------------------------------------------------------------------------------------------// -// // -// M y J a v a D o c E x t r a c t o r // -// // -//------------------------------------------------------------------------------------------------// -package org.audiveris.schema; - -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you 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. - */ -import com.thoughtworks.qdox.JavaProjectBuilder; -import com.thoughtworks.qdox.model.JavaAnnotatedElement; -import org.apache.maven.plugin.logging.Log; -import org.codehaus.mojo.jaxb2.schemageneration.postprocessing.javadoc.JavaDocData; -import org.codehaus.mojo.jaxb2.schemageneration.postprocessing.javadoc.SearchableDocumentation; -import org.codehaus.mojo.jaxb2.schemageneration.postprocessing.javadoc.SortableLocation; -import org.codehaus.mojo.jaxb2.schemageneration.postprocessing.javadoc.location.ClassLocation; -import org.codehaus.mojo.jaxb2.schemageneration.postprocessing.javadoc.location.FieldLocation; -import org.codehaus.mojo.jaxb2.schemageneration.postprocessing.javadoc.location.MethodLocation; -import org.codehaus.mojo.jaxb2.schemageneration.postprocessing.javadoc.location.PackageLocation; -import org.codehaus.mojo.jaxb2.shared.FileSystemUtilities; -import org.codehaus.mojo.jaxb2.shared.Validate; - -import com.thoughtworks.qdox.model.JavaAnnotation; -import com.thoughtworks.qdox.model.JavaClass; -import com.thoughtworks.qdox.model.JavaField; -import com.thoughtworks.qdox.model.JavaMethod; -import com.thoughtworks.qdox.model.JavaPackage; -import com.thoughtworks.qdox.model.JavaSource; - -import java.io.File; -import java.io.IOException; -import java.net.URL; -import java.util.Collection; -import java.util.Collections; -import java.util.List; -import java.util.Map; -import java.util.SortedMap; -import java.util.SortedSet; -import java.util.TreeMap; - -import javax.xml.bind.annotation.XmlAttribute; -import javax.xml.bind.annotation.XmlElement; -import javax.xml.bind.annotation.XmlElementWrapper; -import javax.xml.bind.annotation.XmlEnumValue; -import javax.xml.bind.annotation.XmlType; - -/** - * The schemagen tool operates on compiled bytecode, where JavaDoc comments are not present. - * However, the javadoc documentation present in java source files is required within the generated - * XSD to increase usability and produce an XSD which does not loose out on important usage - * information.

- *

- * The JavaDocExtractor is used as a post processor after creating the XSDs within the compilation - * unit, and injects XSD annotations into the appropriate XSD elements or types.

- *

- * HB: This is a modified version of class - * org.codehaus.mojo.jaxb2.schemageneration.postprocessing.javadoc.JavaDocExtractor - * which processes all relevant classes, including the inner classes. - * - * @author Lennart Jörelid, jGuru Europe AB - * @since 2.0 - */ -public class MyJavaDocExtractor -{ - //~ Static fields/initializers ----------------------------------------------------------------- - - /** - * The default value given as the return value from some annotation classes whenever - * the attribute has not been supplied within the codebase. - */ - private static final String DEFAULT_VALUE = "##default"; - - //~ Instance fields ---------------------------------------------------------------------------- - private final JavaProjectBuilder builder; - - private final Log log; - - //~ Constructors ------------------------------------------------------------------------------- - /** - * Creates a NestedJavaDocExtractor wrapping the supplied Maven Log. - * - * @param log A non-null Log. - */ - public MyJavaDocExtractor (final Log log) - { - - // Check sanity - Validate.notNull(log, "log"); - - // Create internal state - this.log = log; - this.builder = new JavaProjectBuilder(); - } - - //~ Methods ------------------------------------------------------------------------------------ - /** - * Assigns the encoding of the underlying {@link JavaProjectBuilder}. - * - * @param encoding The non-empty encoding to be set into the underlying - * {@link JavaProjectBuilder}. - */ - public void setEncoding (final String encoding) - { - this.builder.setEncoding(encoding); - } - - /** - * Adds the supplied sourceCodeFiles for processing by this JavaDocExtractor. - * - * @param sourceCodeFiles The non-null List of source code files to add. - * @return This MyJavaDocExtractor, for call chaining. - * @throws IllegalArgumentException If any of the given sourceCodeFiles could not be read - * properly. - */ - public MyJavaDocExtractor addSourceFiles (final List sourceCodeFiles) - throws IllegalArgumentException - { - - // Check sanity - Validate.notNull(sourceCodeFiles, "addSourceFiles"); - - // Add the files. - for (File current : sourceCodeFiles) { - try { - builder.addSource(current); - } catch (IOException e) { - throw new IllegalArgumentException("Could not add file [" - + FileSystemUtilities.getCanonicalPath( - current) + "]", e); - } - } - - // All done. - return this; - } - - /** - * Adds the supplied sourceCodeFiles for processing by this JavaDocExtractor. - * - * @param sourceCodeURLs The non-null List of source code URLs to add. - * @return This MyJavaDocExtractor, for call chaining. - * @throws IllegalArgumentException If any of the given sourceCodeURLs could not be read - * properly. - */ - public MyJavaDocExtractor addSourceURLs (final List sourceCodeURLs) - throws IllegalArgumentException - { - - // Check sanity - Validate.notNull(sourceCodeURLs, "sourceCodeURLs"); - - // Add the URLs - for (URL current : sourceCodeURLs) { - try { - builder.addSource(current); - } catch (IOException e) { - throw new IllegalArgumentException("Could not add URL [" + current.toString() + "]", - e); - } - } - - // All done - return this; - } - - /** - * Processes all supplied Java source Files and URLs to extract JavaDocData for all - * ClassLocations from which JavaDoc has been collected. - * - * @return A SearchableDocumentation relating SortableLocations and their paths to harvested - * JavaDocData. - */ - public SearchableDocumentation process () - { - - // Start processing. - final SortedMap dataHolder = new TreeMap<>(); - final Collection sources = builder.getSources(); - - if (log.isInfoEnabled()) { - log.info("Processing [" + sources.size() + "] java sources."); - } - - for (JavaSource current : sources) { - - // Add the package-level JavaDoc - final JavaPackage currentPackage = current.getPackage(); - final String packageName = currentPackage.getName(); - addEntry(dataHolder, new PackageLocation(packageName), currentPackage); - - if (log.isDebugEnabled()) { - log.debug("Added package-level JavaDoc for [" + packageName + "]"); - } - - for (JavaClass currentClass : current.getClasses()) { - processClass(currentClass, packageName, dataHolder); - } - } - - // All done. - return new ReadOnlySearchableDocumentation(dataHolder); - } - - private void processClass (JavaClass currentClass, - String packageName, - SortedMap dataHolder) - { - // Add the class-level JavaDoc - final String simpleClassName = currentClass.getName(); - final String classXmlName = getAnnotationAttributeValueFrom( - XmlType.class, "name", currentClass.getAnnotations()); - - final ClassLocation classLocation = new ClassLocation( - packageName, simpleClassName, classXmlName); - - addEntry(dataHolder, classLocation, currentClass); - - if (log.isDebugEnabled()) { - log.debug("Added class-level JavaDoc for [" + classLocation + "]"); - } - - // Fields - for (JavaField currentField : currentClass.getFields()) { - - final List currentFieldAnnotations = currentField - .getAnnotations(); - String annotatedXmlName = null; - - // - // Is this field a collection, annotated with @XmlElementWrapper? - // If so, the documentation should pertain to the corresponding XML Sequence, - // rather than the individual XML elements. - // - if (hasAnnotation(XmlElementWrapper.class, currentFieldAnnotations)) { - - // There are 2 cases here: - // - // 1: The XmlElementWrapper is named. - // ================================== - // @XmlElementWrapper(name = "foobar") - // @XmlElement(name = "aString") - // private List strings; - // - // ==> annotatedXmlName == "foobar" - // - // 2: The XmlElementWrapper is not named. - // ====================================== - // @XmlElementWrapper - // @XmlElement(name = "anInteger") - // private SortedSet integerSet; - // - // ==> annotatedXmlName == "integerSet" - // - annotatedXmlName = getAnnotationAttributeValueFrom( - XmlElementWrapper.class, - "name", - currentFieldAnnotations); - - if (annotatedXmlName == null || annotatedXmlName.equals(DEFAULT_VALUE)) { - annotatedXmlName = currentField.getName(); - } - } - - // Find the XML name if provided within an annotation. - if (annotatedXmlName == null) { - annotatedXmlName = getAnnotationAttributeValueFrom( - XmlElement.class, - "name", - currentFieldAnnotations); - } - - if (annotatedXmlName == null) { - annotatedXmlName = getAnnotationAttributeValueFrom( - XmlAttribute.class, - "name", - currentFieldAnnotations); - } - if (annotatedXmlName == null) { - annotatedXmlName = getAnnotationAttributeValueFrom( - XmlEnumValue.class, - "value", - currentFieldAnnotations); - } - - // Add the field-level JavaDoc - final FieldLocation fieldLocation = new FieldLocation( - packageName, - simpleClassName, - classXmlName, - currentField.getName(), - annotatedXmlName); - - addEntry(dataHolder, fieldLocation, currentField); - - if (log.isDebugEnabled()) { - log.debug("Added field-level JavaDoc for [" + fieldLocation + "]"); - } - } - - // Methods - for (JavaMethod currentMethod : currentClass.getMethods()) { - - final List currentMethodAnnotations = currentMethod - .getAnnotations(); - String annotatedXmlName = null; - - // - // Is this field a collection, annotated with @XmlElementWrapper? - // If so, the documentation should pertain to the corresponding XML Sequence, - // rather than the individual XML elements. - // - if (hasAnnotation(XmlElementWrapper.class, currentMethodAnnotations)) { - - // There are 2 cases here: - // - // 1: The XmlElementWrapper is named. - // ================================== - // @XmlElementWrapper(name = "foobar") - // @XmlElement(name = "aString") - // public List getStrings() { ... }; - // - // ==> annotatedXmlName == "foobar" - // - // 2: The XmlElementWrapper is not named. - // ====================================== - // @XmlElementWrapper - // @XmlElement(name = "anInteger") - // public SortedSet getIntegerSet() { ... }; - // - // ==> annotatedXmlName == "getIntegerSet" - // - annotatedXmlName = getAnnotationAttributeValueFrom( - XmlElementWrapper.class, - "name", - currentMethodAnnotations); - - if (annotatedXmlName == null || annotatedXmlName.equals(DEFAULT_VALUE)) { - annotatedXmlName = currentMethod.getName(); - } - } - - // Find the XML name if provided within an annotation. - if (annotatedXmlName == null) { - annotatedXmlName = getAnnotationAttributeValueFrom( - XmlElement.class, - "name", - currentMethod.getAnnotations()); - } - - if (annotatedXmlName == null) { - annotatedXmlName = getAnnotationAttributeValueFrom( - XmlAttribute.class, - "name", - currentMethod.getAnnotations()); - } - - // Add the method-level JavaDoc - final MethodLocation location = new MethodLocation(packageName, - simpleClassName, - classXmlName, - currentMethod.getName(), - annotatedXmlName, - currentMethod.getParameters()); - addEntry(dataHolder, location, currentMethod); - - if (log.isDebugEnabled()) { - log.debug("Added method-level JavaDoc for [" + location + "]"); - } - } - - // HB: Recursion to inner classes if any - for (JavaClass innerClass : currentClass.getNestedClasses()) { - processClass(innerClass, packageName, dataHolder); - } - } - - /** - * Finds the value of the attribute with the supplied name within the first matching - * JavaAnnotation of - * the given type encountered in the given annotations List. This is typically used for reading - * values of - * annotations such as {@link XmlElement}, {@link XmlAttribute} or {@link XmlEnumValue}. - * - * @param annotations The list of JavaAnnotations to filter from. - * @param annotationType The type of annotation to read attribute values from. - * @param attributeName The name of the attribute the value of which should be returned. - * @return The first matching JavaAnnotation of type annotationType within the given annotations - * List, or {@code null} if none was found. - * @since 2.2 - */ - private static String getAnnotationAttributeValueFrom ( - final Class annotationType, - final String attributeName, - final List annotations) - { - - // QDox uses the fully qualified class name of the annotation for comparison. - // Extract it. - final String fullyQualifiedClassName = annotationType.getName(); - - JavaAnnotation annotation = null; - String toReturn = null; - - if (annotations != null) { - - for (JavaAnnotation current : annotations) { - if (current.getType().isA(fullyQualifiedClassName)) { - annotation = current; - break; - } - } - - if (annotation != null) { - - final Object nameValue = annotation.getNamedParameter(attributeName); - - if (nameValue != null && nameValue instanceof String) { - - toReturn = ((String) nameValue).trim(); - - // Remove initial and trailing " chars, if present. - if (toReturn.startsWith("\"") && toReturn.endsWith("\"")) { - toReturn = (((String) nameValue).trim()).substring(1, toReturn.length() - 1); - } - } - } - } - - // All Done. - return toReturn; - } - - private static boolean hasAnnotation (final Class annotationType, - final List annotations) - { - - if (annotations != null && !annotations.isEmpty() && annotationType != null) { - - final String fullAnnotationClassName = annotationType.getName(); - - for (JavaAnnotation current : annotations) { - if (current.getType().isA(fullAnnotationClassName)) { - return true; - } - } - } - - return false; - } - - // - // Private helpers - // - private void addEntry (final SortedMap map, - final SortableLocation key, - final JavaAnnotatedElement value) - { - - // Check sanity - if (map.containsKey(key)) { - - // Get something to compare with - final JavaDocData existing = map.get(key); - - // Is this an empty package-level documentation? - if (key instanceof PackageLocation) { - - final boolean emptyExisting = existing.getComment() == null || existing.getComment() - .isEmpty(); - final boolean emptyGiven = value.getComment() == null || value.getComment() - .isEmpty(); - - if (emptyGiven) { - if (log.isDebugEnabled()) { - log.debug("Skipping processing empty Package javadoc from [" + key + "]"); - } - return; - } else if (emptyExisting && log.isWarnEnabled()) { - log.warn("Overwriting empty Package javadoc from [" + key + "]"); - } - } else { - final String given = "[" + value.getClass().getName() + "]: " + value.getComment(); - throw new IllegalArgumentException("Not processing duplicate SortableLocation [" - + key + "]. " - + "\n Existing: " + existing - + ".\n Given: [" + given + "]"); - } - } - - // Validate.isTrue(!map.containsKey(key), "Found duplicate SortableLocation [" + key + "] in map. " - // + "Current map keySet: " + map.keySet() + ". Got comment: [" + value.getComment() + "]"); - map.put(key, new JavaDocData(value.getComment(), value.getTags())); - } - - //~ Inner Classes ------------------------------------------------------------------------------ - /** - * Standard read-only SearchableDocumentation implementation. - */ - private class ReadOnlySearchableDocumentation - implements SearchableDocumentation - { - - // Internal state - private final TreeMap keyMap; - - private final SortedMap valueMap; - - ReadOnlySearchableDocumentation (final SortedMap valueMap) - { - - // Create internal state - this.valueMap = valueMap; - - keyMap = new TreeMap<>(); - for (Map.Entry current : valueMap.entrySet()) { - - final SortableLocation key = current.getKey(); - keyMap.put(key.getPath(), key); - } - } - - /** - * {@inheritDoc} - */ - @Override - public SortedSet getPaths () - { - return Collections.unmodifiableSortedSet(keyMap.navigableKeySet()); - } - - /** - * {@inheritDoc} - */ - @Override - public JavaDocData getJavaDoc (final String path) - { - - // Check sanity - Validate.notNull(path, "path"); - - // All done. - final SortableLocation location = getLocation(path); - return (location == null) ? null : valueMap.get(location); - } - - /** - * {@inheritDoc} - */ - @Override - @SuppressWarnings("unchecked") - public T getLocation (final String path) - { - - // Check sanity - Validate.notNull(path, "path"); - - // All done - return (T) keyMap.get(path); - } - - /** - * {@inheritDoc} - */ - @Override - @SuppressWarnings("unchecked") - public SortedMap getAll () - { - return (SortedMap) Collections.unmodifiableSortedMap( - valueMap); - } - - /** - * {@inheritDoc} - */ - @Override - @SuppressWarnings("unchecked") - public SortedMap getAll (final Class type) - { - - // Check sanity - Validate.notNull(type, "type"); - - // Filter the valueMap. - final SortedMap toReturn = new TreeMap<>(); - for (Map.Entry current : valueMap.entrySet()) { - if (type == current.getKey().getClass()) { - toReturn.put((T) current.getKey(), current.getValue()); - } - } - - // All done. - return toReturn; - } - } -} +//------------------------------------------------------------------------------------------------// +// // +// M y J a v a D o c E x t r a c t o r // +// // +//------------------------------------------------------------------------------------------------// +package org.audiveris.schema; + +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ +import org.apache.maven.plugin.logging.Log; +import org.codehaus.mojo.jaxb2.schemageneration.postprocessing.javadoc.JavaDocData; +import org.codehaus.mojo.jaxb2.schemageneration.postprocessing.javadoc.SearchableDocumentation; +import org.codehaus.mojo.jaxb2.schemageneration.postprocessing.javadoc.SortableLocation; +import org.codehaus.mojo.jaxb2.schemageneration.postprocessing.javadoc.location.ClassLocation; +import org.codehaus.mojo.jaxb2.schemageneration.postprocessing.javadoc.location.FieldLocation; +import org.codehaus.mojo.jaxb2.schemageneration.postprocessing.javadoc.location.MethodLocation; +import org.codehaus.mojo.jaxb2.schemageneration.postprocessing.javadoc.location.PackageLocation; +import org.codehaus.mojo.jaxb2.shared.FileSystemUtilities; +import org.codehaus.mojo.jaxb2.shared.Validate; + +import com.thoughtworks.qdox.JavaProjectBuilder; +import com.thoughtworks.qdox.model.JavaAnnotatedElement; +import com.thoughtworks.qdox.model.JavaAnnotation; +import com.thoughtworks.qdox.model.JavaClass; +import com.thoughtworks.qdox.model.JavaField; +import com.thoughtworks.qdox.model.JavaMethod; +import com.thoughtworks.qdox.model.JavaPackage; +import com.thoughtworks.qdox.model.JavaSource; + +import java.io.File; +import java.io.IOException; +import java.net.URL; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.SortedMap; +import java.util.SortedSet; +import java.util.TreeMap; + +import javax.xml.bind.annotation.XmlAttribute; +import javax.xml.bind.annotation.XmlElement; +import javax.xml.bind.annotation.XmlElementWrapper; +import javax.xml.bind.annotation.XmlEnumValue; +import javax.xml.bind.annotation.XmlType; + +/** + * The schemagen tool operates on compiled bytecode, where JavaDoc comments are not present. + * However, the javadoc documentation present in java source files is required within the generated + * XSD to increase usability and produce an XSD which does not loose out on important usage + * information. + *

+ *

+ * The JavaDocExtractor is used as a post processor after creating the XSDs within the compilation + * unit, and injects XSD annotations into the appropriate XSD elements or types. + *

+ *

+ * HB: This is a modified version of class + * org.codehaus.mojo.jaxb2.schemageneration.postprocessing.javadoc.JavaDocExtractor + * which processes all relevant classes, including the inner classes. + * + * @author Lennart Jörelid, jGuru Europe AB + * @since 2.0 + */ +public class MyJavaDocExtractor +{ + //~ Static fields/initializers ----------------------------------------------------------------- + + /** + * The default value given as the return value from some annotation classes whenever + * the attribute has not been supplied within the codebase. + */ + private static final String DEFAULT_VALUE = "##default"; + + //~ Instance fields ---------------------------------------------------------------------------- + private final JavaProjectBuilder builder; + + private final Log log; + + //~ Constructors ------------------------------------------------------------------------------- + /** + * Creates a NestedJavaDocExtractor wrapping the supplied Maven Log. + * + * @param log A non-null Log. + */ + public MyJavaDocExtractor (final Log log) + { + + // Check sanity + Validate.notNull(log, "log"); + + // Create internal state + this.log = log; + this.builder = new JavaProjectBuilder(); + } + + //~ Methods ------------------------------------------------------------------------------------ + /** + * Assigns the encoding of the underlying {@link JavaProjectBuilder}. + * + * @param encoding The non-empty encoding to be set into the underlying + * {@link JavaProjectBuilder}. + */ + public void setEncoding (final String encoding) + { + this.builder.setEncoding(encoding); + } + + /** + * Adds the supplied sourceCodeFiles for processing by this JavaDocExtractor. + * + * @param sourceCodeFiles The non-null List of source code files to add. + * @return This MyJavaDocExtractor, for call chaining. + * @throws IllegalArgumentException If any of the given sourceCodeFiles could not be read + * properly. + */ + public MyJavaDocExtractor addSourceFiles (final List sourceCodeFiles) + throws IllegalArgumentException + { + + // Check sanity + Validate.notNull(sourceCodeFiles, "addSourceFiles"); + + // Add the files. + for (File current : sourceCodeFiles) { + try { + builder.addSource(current); + } catch (IOException e) { + throw new IllegalArgumentException( + "Could not add file [" + FileSystemUtilities.getCanonicalPath(current) + + "]", + e); + } + } + + // All done. + return this; + } + + /** + * Adds the supplied sourceCodeFiles for processing by this JavaDocExtractor. + * + * @param sourceCodeURLs The non-null List of source code URLs to add. + * @return This MyJavaDocExtractor, for call chaining. + * @throws IllegalArgumentException If any of the given sourceCodeURLs could not be read + * properly. + */ + public MyJavaDocExtractor addSourceURLs (final List sourceCodeURLs) + throws IllegalArgumentException + { + + // Check sanity + Validate.notNull(sourceCodeURLs, "sourceCodeURLs"); + + // Add the URLs + for (URL current : sourceCodeURLs) { + try { + builder.addSource(current); + } catch (IOException e) { + throw new IllegalArgumentException( + "Could not add URL [" + current.toString() + "]", + e); + } + } + + // All done + return this; + } + + /** + * Processes all supplied Java source Files and URLs to extract JavaDocData for all + * ClassLocations from which JavaDoc has been collected. + * + * @return A SearchableDocumentation relating SortableLocations and their paths to harvested + * JavaDocData. + */ + public SearchableDocumentation process () + { + + // Start processing. + final SortedMap dataHolder = new TreeMap<>(); + final Collection sources = builder.getSources(); + + if (log.isInfoEnabled()) { + log.info("Processing " + sources.size() + " java sources."); + } + + for (JavaSource current : sources) { + + // Add the package-level JavaDoc + final JavaPackage currentPackage = current.getPackage(); + final String packageName = currentPackage.getName(); + addEntry(dataHolder, new PackageLocation(packageName), currentPackage); + + if (log.isDebugEnabled()) { + log.debug("Added package-level JavaDoc for [" + packageName + "]"); + } + + for (JavaClass currentClass : current.getClasses()) { + processClass(currentClass, packageName, dataHolder); + } + } + + // All done. + return new ReadOnlySearchableDocumentation(dataHolder); + } + + private void processClass (JavaClass currentClass, + String packageName, + SortedMap dataHolder) + { + // Add the class-level JavaDoc + final String simpleClassName = currentClass.getName(); + final String classXmlName = getAnnotationAttributeValueFrom( + XmlType.class, + "name", + currentClass.getAnnotations()); + + // HB. Replaced packageName with a more relevant containerName + final JavaClass declaringClass = currentClass.getDeclaringClass(); + final String containerName = (declaringClass == null) ? packageName + : declaringClass.getFullyQualifiedName(); + + final ClassLocation classLocation = new ClassLocation( + containerName, // HB was: packageName, + simpleClassName, + classXmlName); + + addEntry(dataHolder, classLocation, currentClass); + + if (log.isDebugEnabled()) { + log.debug("Added class-level JavaDoc for [" + classLocation + "]"); + } + + // Fields + for (JavaField currentField : currentClass.getFields()) { + + final List currentFieldAnnotations = currentField.getAnnotations(); + String annotatedXmlName = null; + + // + // Is this field a collection, annotated with @XmlElementWrapper? + // If so, the documentation should pertain to the corresponding XML Sequence, + // rather than the individual XML elements. + // + if (hasAnnotation(XmlElementWrapper.class, currentFieldAnnotations)) { + + // There are 2 cases here: + // + // 1: The XmlElementWrapper is named. + // ================================== + // @XmlElementWrapper(name = "foobar") + // @XmlElement(name = "aString") + // private List strings; + // + // ==> annotatedXmlName == "foobar" + // + // 2: The XmlElementWrapper is not named. + // ====================================== + // @XmlElementWrapper + // @XmlElement(name = "anInteger") + // private SortedSet integerSet; + // + // ==> annotatedXmlName == "integerSet" + // + annotatedXmlName = getAnnotationAttributeValueFrom( + XmlElementWrapper.class, + "name", + currentFieldAnnotations); + + if (annotatedXmlName == null || annotatedXmlName.equals(DEFAULT_VALUE)) { + annotatedXmlName = currentField.getName(); + } + } + + // Find the XML name if provided within an annotation. + if (annotatedXmlName == null) { + annotatedXmlName = getAnnotationAttributeValueFrom( + XmlElement.class, + "name", + currentFieldAnnotations); + } + + if (annotatedXmlName == null) { + annotatedXmlName = getAnnotationAttributeValueFrom( + XmlAttribute.class, + "name", + currentFieldAnnotations); + } + if (annotatedXmlName == null) { + annotatedXmlName = getAnnotationAttributeValueFrom( + XmlEnumValue.class, + "value", + currentFieldAnnotations); + } + + // Add the field-level JavaDoc + final FieldLocation fieldLocation = new FieldLocation( + containerName, // HB. was packageName, + simpleClassName, + classXmlName, + currentField.getName(), + annotatedXmlName); + + addEntry(dataHolder, fieldLocation, currentField); + + if (log.isDebugEnabled()) { + log.debug("Added field-level JavaDoc for [" + fieldLocation + "]"); + } + } + + // Methods + for (JavaMethod currentMethod : currentClass.getMethods()) { + + final List currentMethodAnnotations = currentMethod.getAnnotations(); + String annotatedXmlName = null; + + // + // Is this field a collection, annotated with @XmlElementWrapper? + // If so, the documentation should pertain to the corresponding XML Sequence, + // rather than the individual XML elements. + // + if (hasAnnotation(XmlElementWrapper.class, currentMethodAnnotations)) { + + // There are 2 cases here: + // + // 1: The XmlElementWrapper is named. + // ================================== + // @XmlElementWrapper(name = "foobar") + // @XmlElement(name = "aString") + // public List getStrings() { ... }; + // + // ==> annotatedXmlName == "foobar" + // + // 2: The XmlElementWrapper is not named. + // ====================================== + // @XmlElementWrapper + // @XmlElement(name = "anInteger") + // public SortedSet getIntegerSet() { ... }; + // + // ==> annotatedXmlName == "getIntegerSet" + // + annotatedXmlName = getAnnotationAttributeValueFrom( + XmlElementWrapper.class, + "name", + currentMethodAnnotations); + + if (annotatedXmlName == null || annotatedXmlName.equals(DEFAULT_VALUE)) { + annotatedXmlName = currentMethod.getName(); + } + } + + // Find the XML name if provided within an annotation. + if (annotatedXmlName == null) { + annotatedXmlName = getAnnotationAttributeValueFrom( + XmlElement.class, + "name", + currentMethod.getAnnotations()); + } + + if (annotatedXmlName == null) { + annotatedXmlName = getAnnotationAttributeValueFrom( + XmlAttribute.class, + "name", + currentMethod.getAnnotations()); + } + + // Add the method-level JavaDoc + final MethodLocation location = new MethodLocation( + containerName, // HB. was packageName, + simpleClassName, + classXmlName, + currentMethod.getName(), + annotatedXmlName, + currentMethod.getParameters()); + addEntry(dataHolder, location, currentMethod); + + if (log.isDebugEnabled()) { + log.debug("Added method-level JavaDoc for [" + location + "]"); + } + } + + // HB: Recursion to inner classes if any + for (JavaClass innerClass : currentClass.getNestedClasses()) { + if (log.isDebugEnabled()) { + log.debug("Recursion from " + currentClass + " to inner " + innerClass); + } + processClass(innerClass, packageName, dataHolder); + } + } + + /** + * Finds the value of the attribute with the supplied name within the first matching + * JavaAnnotation of + * the given type encountered in the given annotations List. This is typically used for reading + * values of + * annotations such as {@link XmlElement}, {@link XmlAttribute} or {@link XmlEnumValue}. + * + * @param annotations The list of JavaAnnotations to filter from. + * @param annotationType The type of annotation to read attribute values from. + * @param attributeName The name of the attribute the value of which should be returned. + * @return The first matching JavaAnnotation of type annotationType within the given annotations + * List, or {@code null} if none was found. + * @since 2.2 + */ + private static String getAnnotationAttributeValueFrom (final Class annotationType, + final String attributeName, + final List annotations) + { + + // QDox uses the fully qualified class name of the annotation for comparison. + // Extract it. + final String fullyQualifiedClassName = annotationType.getName(); + + JavaAnnotation annotation = null; + String toReturn = null; + + if (annotations != null) { + + for (JavaAnnotation current : annotations) { + if (current.getType().isA(fullyQualifiedClassName)) { + annotation = current; + break; + } + } + + if (annotation != null) { + + final Object nameValue = annotation.getNamedParameter(attributeName); + + if (nameValue != null && nameValue instanceof String) { + + toReturn = ((String) nameValue).trim(); + + // Remove initial and trailing " chars, if present. + if (toReturn.startsWith("\"") && toReturn.endsWith("\"")) { + toReturn = (((String) nameValue).trim()).substring( + 1, + toReturn.length() - 1); + } + } + } + } + + // All Done. + return toReturn; + } + + private static boolean hasAnnotation (final Class annotationType, + final List annotations) + { + + if (annotations != null && !annotations.isEmpty() && annotationType != null) { + + final String fullAnnotationClassName = annotationType.getName(); + + for (JavaAnnotation current : annotations) { + if (current.getType().isA(fullAnnotationClassName)) { + return true; + } + } + } + + return false; + } + + // + // Private helpers + // + private void addEntry (final SortedMap map, + final SortableLocation key, + final JavaAnnotatedElement value) + { + if (log.isDebugEnabled()) { + try { + log.debug("addEntry key: " + key + " value: " + value); + } catch (Exception ex) { + log.debug("Exception: " + ex); + } + } + + // Check sanity + if (map.containsKey(key)) { + final String given = "[" + value.getClass().getName() + "]: " + value; + + // Get something to compare with + final JavaDocData existing = map.get(key); + + // Is this an empty package-level documentation? + if (key instanceof PackageLocation) { + + final boolean emptyExisting = existing.getComment() == null || existing.getComment() + .isEmpty(); + final boolean emptyGiven = value.getComment() == null || value.getComment() + .isEmpty(); + + if (emptyGiven) { + if (log.isDebugEnabled()) { + log.debug("Skipping processing empty Package javadoc from [" + key + "]"); + } + return; + } else if (emptyExisting && log.isWarnEnabled()) { + log.warn( + "Overwriting empty Package javadoc " // + + "\n Key: " + key // + + "\n Given: " + given // + + "\n Existing: " + "[" + existing.getClass().getName() + "]: " + + existing); + } + } else { + throw new IllegalArgumentException( + "Not processing duplicate SortableLocation [" + key + "]. " + + "\n Existing: " + existing + ".\n Given: [" + given + "]"); + } + } + + // Validate.isTrue(!map.containsKey(key), "Found duplicate SortableLocation [" + key + "] in map. " + // + "Current map keySet: " + map.keySet() + ". Got comment: [" + value.getComment() + "]"); + map.put(key, new JavaDocData(value.getComment(), value.getTags())); + } + + //~ Inner Classes ------------------------------------------------------------------------------ + /** + * Standard read-only SearchableDocumentation implementation. + */ + private class ReadOnlySearchableDocumentation + implements SearchableDocumentation + { + + // Internal state + private final TreeMap keyMap; + + private final SortedMap valueMap; + + ReadOnlySearchableDocumentation (final SortedMap valueMap) + { + + // Create internal state + this.valueMap = valueMap; + + keyMap = new TreeMap<>(); + for (Map.Entry current : valueMap.entrySet()) { + + final SortableLocation key = current.getKey(); + keyMap.put(key.getPath(), key); + } + } + + /** + * {@inheritDoc} + */ + @Override + public SortedSet getPaths () + { + return Collections.unmodifiableSortedSet(keyMap.navigableKeySet()); + } + + /** + * {@inheritDoc} + */ + @Override + public JavaDocData getJavaDoc (final String path) + { + + // Check sanity + Validate.notNull(path, "path"); + + // All done. + final SortableLocation location = getLocation(path); + return (location == null) ? null : valueMap.get(location); + } + + /** + * {@inheritDoc} + */ + @Override + @SuppressWarnings("unchecked") + public T getLocation (final String path) + { + + // Check sanity + Validate.notNull(path, "path"); + + // All done + return (T) keyMap.get(path); + } + + /** + * {@inheritDoc} + */ + @Override + @SuppressWarnings("unchecked") + public SortedMap getAll () + { + return (SortedMap) Collections.unmodifiableSortedMap( + valueMap); + } + + /** + * {@inheritDoc} + */ + @Override + @SuppressWarnings("unchecked") + public SortedMap getAll (final Class type) + { + + // Check sanity + Validate.notNull(type, "type"); + + // Filter the valueMap. + final SortedMap toReturn = new TreeMap<>(); + for (Map.Entry current : valueMap.entrySet()) { + if (type == current.getKey().getClass()) { + toReturn.put((T) current.getKey(), current.getValue()); + } + } + + // All done. + return toReturn; + } + } +} diff --git a/schemas/src/main/java/org/audiveris/schema/SchemaAnnotator.java b/schemas/src/main/java/org/audiveris/schema/SchemaAnnotator.java index e0a38d122..f0553ae05 100644 --- a/schemas/src/main/java/org/audiveris/schema/SchemaAnnotator.java +++ b/schemas/src/main/java/org/audiveris/schema/SchemaAnnotator.java @@ -1,298 +1,297 @@ -//------------------------------------------------------------------------------------------------// -// // -// S c h e m a A n n o t a t o r // -// // -//------------------------------------------------------------------------------------------------// -// -// -// Copyright © Audiveris 2022. All rights reserved. -// -// This program is free software: you can redistribute it and/or modify it under the terms of the -// GNU Affero General Public License as published by the Free Software Foundation, either version -// 3 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 Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License along with this -// program. If not, see . -//------------------------------------------------------------------------------------------------// -// -package org.audiveris.schema; - -import org.apache.maven.model.Resource; -import org.apache.maven.plugin.MojoExecutionException; -import org.apache.maven.plugin.MojoFailureException; - -import org.codehaus.mojo.jaxb2.schemageneration.AbstractXsdGeneratorMojo; -import org.codehaus.mojo.jaxb2.schemageneration.SchemaGenerationMojo; -import org.codehaus.mojo.jaxb2.schemageneration.postprocessing.javadoc.JavaDocRenderer; -import org.codehaus.mojo.jaxb2.schemageneration.postprocessing.javadoc.NoAuthorJavaDocRenderer; -import org.codehaus.mojo.jaxb2.schemageneration.postprocessing.javadoc.SearchableDocumentation; -import org.codehaus.mojo.jaxb2.shared.FileSystemUtilities; -import org.codehaus.mojo.jaxb2.shared.filters.Filter; -import org.codehaus.mojo.jaxb2.shared.filters.pattern.FileFilterAdapter; -import org.codehaus.mojo.jaxb2.shared.filters.pattern.PatternFileFilter; -import org.codehaus.plexus.util.FileUtils; - -import java.io.File; -import java.io.IOException; -import java.net.URL; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.List; -import java.util.Map.Entry; -import java.util.SortedMap; -import org.codehaus.mojo.jaxb2.schemageneration.postprocessing.javadoc.JavaDocData; -import org.codehaus.mojo.jaxb2.schemageneration.postprocessing.javadoc.SortableLocation; - -/** - * Class {@code SchemaAnnotator} annotates a plain XSD schema with JavaDoc content. - *

- * Code is a very simplified version of {@link SchemaGenerationMojo} since the plain 'vanilla' - * XSD files already exist, generated from Java sources. - * Its role is thus limited to the injection of JavaDoc content directly retrieved from the same - * Java sources. - *

- * It uses an enhanced JavaDocExtractor, since the original one did not process inner classes. - *

- * NOTA: Unfortunately, it can only process XSD files whose names match "schemaN.xsd". - * This is due to final {@link AbstractXsdGeneratorMojo#SCHEMAGEN_EMITTED_FILENAME} pattern. - * - * @author Hervé Bitteur - */ -public class SchemaAnnotator - extends AbstractXsdGeneratorMojo -{ - - //~ Static fields/initializers ----------------------------------------------------------------- - //~ Instance fields ---------------------------------------------------------------------------- - /** - * The directory where the plain XSD files are read. - * From this directory the XSDs will be copied to the outputDirectory for further processing. - * - * @see #outputDirectory - */ - // "${project.build.directory}/schemagen-work/compile_scope" - private final File workDirectory; - - /** - * The directory where the annotated XSD file(s) will be - * placed, after all transformations are done. - */ - private final File outputDirectory; - - /** - * List of paths to files and/or directories which should be recursively searched - * for Java source files. - */ - private final List sources; - - //~ Constructors ------------------------------------------------------------------------------- - /** - * Create the {@code SchemaAnnotator} instance with its parameters. - * - * @param workDirectory directory where plain XSD files are read - * @param outputDirectory directory where annotated XSD files are written - * @param sources list of files/directories to be recursively searched for Java sources - */ - public SchemaAnnotator (File workDirectory, - File outputDirectory, - List sources) - { - this.workDirectory = workDirectory; - this.outputDirectory = outputDirectory; - this.sources = sources; - } - - //~ Methods ------------------------------------------------------------------------------------ - @Override - protected File getWorkDirectory () - { - return workDirectory; - } - - @Override - protected List getCompiledClassNames () - { - return Collections.emptyList(); - } - - @Override - protected List getSources () - { - return null; - } - - @Override - protected void addResource (Resource resource) - { - } - - @Override - protected File getOutputDirectory () - { - return outputDirectory; - } - - @Override - protected List getClasspath () - throws MojoExecutionException - { - return Collections.emptyList(); - } - - @Override - protected String getStaleFileName () - { - return SchemaGenerationMojo.STALE_FILENAME; - } - - //---------// - // process // - //---------// - public final void process () - throws MojoExecutionException, - MojoFailureException - { - try { - // Copy plain XSDs files from the WorkDirectory to the OutputDirectory. - final List> exclusionFilters = PatternFileFilter - .createIncludeFilterList(getLog(), "\\.class"); - - final List toCopy = FileSystemUtilities.resolveRecursively( - Arrays.asList(getWorkDirectory()), - exclusionFilters, getLog()); - - for (File current : toCopy) { - // Get the path to the current file - final String currentPath = FileSystemUtilities.getCanonicalPath(current - .getAbsoluteFile()); - final File target = new File(getOutputDirectory(), - FileSystemUtilities.relativize(currentPath, - getWorkDirectory(), - true)); - - // Copy the file to the same relative structure within the output directory. - FileSystemUtilities.createDirectory(target.getParentFile(), false); - FileUtils.copyFile(current, target); - } - - // - // The XSD post-processing should be applied in the following order: - // - // 1. [XsdAnnotationProcessor]: Inject JavaDoc annotations for Classes. - // 2. [XsdEnumerationAnnotationProcessor]: Inject JavaDoc annotations for Enums. - // 3. [ChangeNamespacePrefixProcessor]: Change namespace prefixes within XSDs. - // 4. [ChangeFilenameProcessor]: Change the fileNames of XSDs. - // -// // Map the XML Namespaces to their respective XML URIs (and reverse) -// // The keys are the generated 'vanilla' XSD file names. -// Map resolverMap = null; -// final Map resolverMap = XsdGeneratorHelper -// .getFileNameToResolverMap(getOutputDirectory()); - getLog().info("XSD post-processing: Adding JavaDoc annotations in generated XSDs."); - - // Retrieve all .java files under 'sources' directories - final Filter acceptJavaFilter = new FileFilterAdapter( - f -> f.isDirectory() || f.getName().endsWith(".java")); - acceptJavaFilter.initialize(getLog()); - final List javaFiles = FileSystemUtilities.filterFiles( - sources, acceptJavaFilter, getLog()); - getLog().info("Java files: " + javaFiles.size()); - - // Acquire JavaDocs, using the MyJavaDocExtractor - final MyJavaDocExtractor extractor = new MyJavaDocExtractor(getLog()) - .addSourceFiles(javaFiles); - final SearchableDocumentation javaDocs = extractor.process(); - ///dumpDocs(javaDocs); - - // Modify the 'vanilla' generated XSDs by inserting the JavaDoc as annotations - final JavaDocRenderer renderer = new NoAuthorJavaDocRenderer(); - final int numProcessedFiles = MyXsdGeneratorHelper.insertJavaDocAsAnnotations( - getLog(), - getEncoding(false), - getOutputDirectory(), - javaDocs, - renderer); - - ///if (getLog().isDebugEnabled()) { - getLog().info("XSD post-processing: " + numProcessedFiles - + " files processed."); - ///} - } catch (MojoExecutionException e) { - throw e; - } catch (IOException | - IllegalArgumentException e) { - - // Find the root exception, and print its stack trace to the Maven Log. - // These invocation target exceptions tend to produce really deep stack traces, - // hiding the actual root cause of the exception. - Throwable current = e; - while (current.getCause() != null) { - current = current.getCause(); - } - - getLog().error("Execution failed."); - - // - // Print a stack trace - // - StringBuilder rootCauseBuilder = new StringBuilder(); - rootCauseBuilder.append("\n"); - rootCauseBuilder.append("[Exception]: ").append(current.getClass().getName()).append( - "\n"); - rootCauseBuilder.append("[Message]: ").append(current.getMessage()).append("\n"); - for (StackTraceElement el : current.getStackTrace()) { - rootCauseBuilder.append(" ").append(el.toString()).append("\n"); - } - getLog().error(rootCauseBuilder.toString()); - } - } - - // HB - public void dumpDocs (SearchableDocumentation docs) - { - SortedMap map = docs.getAll(); - - for (Entry entry : map.entrySet()) { - getLog().info(entry.getKey() + " => " + entry.getValue()); - } - } - - //------// - // main // - //------// - /** - * Main entry point for the annotator. - * - * @param args the command line arguments - * - arg1: work directory - * - arg2: output directory - * - following args: files/directories containing Java sources - */ - public static void main (String[] args) - { - if ((args == null) || args.length < 3) { - System.err.println("Error in SchemaAnnotator, expecting 3 arguments at least"); - throw new RuntimeException("Incorrect arguments to SchemaAnnotator"); - } - - try { - final File work = new File(args[0]); - final File output = new File(args[1]); - final List sources = new ArrayList<>(); - - for (int i = 2; i < args.length; i++) { - sources.add(new File(args[i])); - } - - final SchemaAnnotator sa = new SchemaAnnotator(work, output, sources); - sa.process(); - } catch (Exception ex) { - System.err.println("Error in schema annotator: " + ex); - } - } -} +//------------------------------------------------------------------------------------------------// +// // +// S c h e m a A n n o t a t o r // +// // +//------------------------------------------------------------------------------------------------// +// +// +// Copyright © Audiveris 2022. All rights reserved. +// +// This program is free software: you can redistribute it and/or modify it under the terms of the +// GNU Affero General Public License as published by the Free Software Foundation, either version +// 3 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 Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License along with this +// program. If not, see . +//------------------------------------------------------------------------------------------------// +// +package org.audiveris.schema; + +import org.apache.maven.model.Resource; +import org.apache.maven.plugin.MojoExecutionException; +import org.apache.maven.plugin.MojoFailureException; +import org.codehaus.mojo.jaxb2.schemageneration.AbstractXsdGeneratorMojo; +import org.codehaus.mojo.jaxb2.schemageneration.SchemaGenerationMojo; +import org.codehaus.mojo.jaxb2.schemageneration.postprocessing.javadoc.JavaDocData; +import org.codehaus.mojo.jaxb2.schemageneration.postprocessing.javadoc.JavaDocRenderer; +import org.codehaus.mojo.jaxb2.schemageneration.postprocessing.javadoc.NoAuthorJavaDocRenderer; +import org.codehaus.mojo.jaxb2.schemageneration.postprocessing.javadoc.SearchableDocumentation; +import org.codehaus.mojo.jaxb2.schemageneration.postprocessing.javadoc.SortableLocation; +import org.codehaus.mojo.jaxb2.shared.FileSystemUtilities; +import org.codehaus.mojo.jaxb2.shared.filters.Filter; +import org.codehaus.mojo.jaxb2.shared.filters.pattern.FileFilterAdapter; +import org.codehaus.mojo.jaxb2.shared.filters.pattern.PatternFileFilter; +import org.codehaus.plexus.util.FileUtils; + +import java.io.File; +import java.io.IOException; +import java.net.URL; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Map.Entry; +import java.util.SortedMap; + +/** + * Class {@code SchemaAnnotator} annotates a plain XSD schema with JavaDoc content. + *

+ * Code is a very simplified version of {@link SchemaGenerationMojo} since the plain 'vanilla' + * XSD files already exist, generated from Java sources. + * Its role is thus limited to the injection of JavaDoc content directly retrieved from the same + * Java sources. + *

+ * It uses an enhanced JavaDocExtractor, since the original one did not process inner classes. + *

+ * NOTA: Unfortunately, it can only process XSD files whose names match "schemaN.xsd". + * This is due to final {@link AbstractXsdGeneratorMojo#SCHEMAGEN_EMITTED_FILENAME} pattern. + * + * @author Hervé Bitteur + */ +public class SchemaAnnotator + extends AbstractXsdGeneratorMojo +{ + + //~ Static fields/initializers ----------------------------------------------------------------- + //~ Instance fields ---------------------------------------------------------------------------- + /** + * The directory where the plain XSD files are read. + * From this directory the XSDs will be copied to the outputDirectory for further processing. + * + * @see #outputDirectory + */ + // "${project.build.directory}/schemagen-work/compile_scope" + private final File workDirectory; + + /** + * The directory where the annotated XSD file(s) will be + * placed, after all transformations are done. + */ + private final File outputDirectory; + + /** + * List of paths to files and/or directories which should be recursively searched + * for Java source files. + */ + private final List sources; + + //~ Constructors ------------------------------------------------------------------------------- + /** + * Create the {@code SchemaAnnotator} instance with its parameters. + * + * @param workDirectory directory where plain XSD files are read + * @param outputDirectory directory where annotated XSD files are written + * @param sources list of files/directories to be recursively searched for Java sources + */ + public SchemaAnnotator (File workDirectory, + File outputDirectory, + List sources) + { + this.workDirectory = workDirectory; + this.outputDirectory = outputDirectory; + this.sources = sources; + } + + //~ Methods ------------------------------------------------------------------------------------ + @Override + protected File getWorkDirectory () + { + return workDirectory; + } + + @Override + protected List getCompiledClassNames () + { + return Collections.emptyList(); + } + + @Override + protected List getSources () + { + return null; + } + + @Override + protected void addResource (Resource resource) + { + } + + @Override + protected File getOutputDirectory () + { + return outputDirectory; + } + + @Override + protected List getClasspath () + throws MojoExecutionException + { + return Collections.emptyList(); + } + + @Override + protected String getStaleFileName () + { + return SchemaGenerationMojo.STALE_FILENAME; + } + + //---------// + // process // + //---------// + public final void process () + throws MojoExecutionException, MojoFailureException + { + try { + // Copy plain XSDs files from the WorkDirectory to the OutputDirectory. + final List> exclusionFilters = PatternFileFilter.createIncludeFilterList( + getLog(), + "\\.class"); + + final List toCopy = FileSystemUtilities.resolveRecursively( + Arrays.asList(getWorkDirectory()), + exclusionFilters, + getLog()); + + for (File current : toCopy) { + // Get the path to the current file + final String currentPath = FileSystemUtilities.getCanonicalPath( + current.getAbsoluteFile()); + final File target = new File( + getOutputDirectory(), + FileSystemUtilities.relativize(currentPath, getWorkDirectory(), true)); + + // Copy the file to the same relative structure within the output directory. + FileSystemUtilities.createDirectory(target.getParentFile(), false); + FileUtils.copyFile(current, target); + } + + // + // The XSD post-processing should be applied in the following order: + // + // 1. [XsdAnnotationProcessor]: Inject JavaDoc annotations for Classes. + // 2. [XsdEnumerationAnnotationProcessor]: Inject JavaDoc annotations for Enums. + // 3. [ChangeNamespacePrefixProcessor]: Change namespace prefixes within XSDs. + // 4. [ChangeFilenameProcessor]: Change the fileNames of XSDs. + // + // // Map the XML Namespaces to their respective XML URIs (and reverse) + // // The keys are the generated 'vanilla' XSD file names. + // Map resolverMap = null; + // final Map resolverMap = XsdGeneratorHelper + // .getFileNameToResolverMap(getOutputDirectory()); + getLog().info("XSD post-processing: Adding JavaDoc annotations in generated XSDs."); + + // Retrieve all .java files under 'sources' directories + final Filter acceptJavaFilter = new FileFilterAdapter( + f -> f.isDirectory() || f.getName().endsWith(".java")); + acceptJavaFilter.initialize(getLog()); + final List javaFiles = FileSystemUtilities.filterFiles( + sources, + acceptJavaFilter, + getLog()); + getLog().info("Java files: " + javaFiles.size()); + + // Acquire JavaDocs, using the MyJavaDocExtractor + final MyJavaDocExtractor extractor = new MyJavaDocExtractor(getLog()).addSourceFiles( + javaFiles); + final SearchableDocumentation javaDocs = extractor.process(); + ///dumpDocs(javaDocs); + + // Modify the 'vanilla' generated XSDs by inserting the JavaDoc as annotations + final JavaDocRenderer renderer = new NoAuthorJavaDocRenderer(); + final int numProcessedFiles = MyXsdGeneratorHelper.insertJavaDocAsAnnotations( + getLog(), + getEncoding(false), + getOutputDirectory(), + javaDocs, + renderer); + + ///if (getLog().isDebugEnabled()) { + getLog().info("XSD post-processing: " + numProcessedFiles + " files processed."); + ///} + } catch (MojoExecutionException e) { + throw e; + } catch (IOException | IllegalArgumentException e) { + + // Find the root exception, and print its stack trace to the Maven Log. + // These invocation target exceptions tend to produce really deep stack traces, + // hiding the actual root cause of the exception. + Throwable current = e; + while (current.getCause() != null) { + current = current.getCause(); + } + + getLog().error("Execution failed."); + + // + // Print a stack trace + // + StringBuilder rootCauseBuilder = new StringBuilder(); + rootCauseBuilder.append("\n"); + rootCauseBuilder.append("[Exception]: ").append(current.getClass().getName()).append( + "\n"); + rootCauseBuilder.append("[Message]: ").append(current.getMessage()).append("\n"); + for (StackTraceElement el : current.getStackTrace()) { + rootCauseBuilder.append(" ").append(el.toString()).append("\n"); + } + getLog().error(rootCauseBuilder.toString()); + } + } + + // HB + public void dumpDocs (SearchableDocumentation docs) + { + SortedMap map = docs.getAll(); + + for (Entry entry : map.entrySet()) { + getLog().info(entry.getKey() + " => " + entry.getValue()); + } + } + + //------// + // main // + //------// + /** + * Main entry point for the annotator. + * + * @param args the command line arguments + * - arg1: work directory + * - arg2: output directory + * - following args: files/directories containing Java sources + */ + public static void main (String[] args) + { + if ((args == null) || args.length < 3) { + System.err.println("Error in SchemaAnnotator, expecting 3 arguments at least"); + throw new RuntimeException("Incorrect arguments to SchemaAnnotator"); + } + + try { + final File work = new File(args[0]); + final File output = new File(args[1]); + final List sources = new ArrayList<>(); + + for (int i = 2; i < args.length; i++) { + sources.add(new File(args[i])); + } + + final SchemaAnnotator sa = new SchemaAnnotator(work, output, sources); + sa.process(); + } catch (Exception ex) { + System.err.println("Error in schema annotator: " + ex); + } + } +} diff --git a/src/main/org/audiveris/omr/image/FloodFiller.java b/src/main/org/audiveris/omr/image/FloodFiller.java index 66f961803..fb73d2126 100644 --- a/src/main/org/audiveris/omr/image/FloodFiller.java +++ b/src/main/org/audiveris/omr/image/FloodFiller.java @@ -57,7 +57,8 @@ public FloodFiller (BufferedImage image) /** * Flood fill the area from provided (x,y) location by recursively converting * pixels from oldColor to newColor. - * Simplistic implementation, but sufficient for the time being. + *

+ * This implementation flood fills only vertically and horizontally, not in diagonal. * * @param x the abscissa of the pixel to process * @param y the ordinate of the pixel to process @@ -74,18 +75,45 @@ public void fill (int x, if ((pix == oldColor) && (pix != newColor)) { image.setRGB(x, y, newColor); - fill(x - 1, y - 1, oldColor, newColor); - fill(x - 1, y, oldColor, newColor); - fill(x - 1, y + 1, oldColor, newColor); + fill(x - 1, y, oldColor, newColor); // Left + fill(x, y - 1, oldColor, newColor); // Top + fill(x, y + 1, oldColor, newColor); // Bottom + fill(x + 1, y, oldColor, newColor); // Right + } + } + } - fill(x, y - 1, oldColor, newColor); + /** + * Check if the pixel at provided (x,y) location is an isolated pixel, and if so, + * replace it by newColor. + * + * @param x the abscissa of the pixel to process + * @param y the ordinate of the pixel to process + * @param newColor the color to replace with + */ + public void adjust (int x, + int y, + int newColor) + { + // Check if pixel is isolated (no vertical/horizontal neighbor w/ same color) + final int pix = image.getRGB(x, y); - fill(x, y + 1, oldColor, newColor); + if (image.getRGB(x - 1, y) == pix) { + return; + } - fill(x + 1, y - 1, oldColor, newColor); - fill(x + 1, y, oldColor, newColor); - fill(x + 1, y + 1, oldColor, newColor); - } + if (image.getRGB(x, y - 1) == pix) { + return; + } + + if (image.getRGB(x, y + 1) == pix) { + return; + } + + if (image.getRGB(x + 1, y) == pix) { + return; } + + image.setRGB(x, y, newColor); } } diff --git a/src/main/org/audiveris/omr/image/TemplateFactory.java b/src/main/org/audiveris/omr/image/TemplateFactory.java index 6b0ed342e..07d3d8ba0 100644 --- a/src/main/org/audiveris/omr/image/TemplateFactory.java +++ b/src/main/org/audiveris/omr/image/TemplateFactory.java @@ -317,6 +317,7 @@ private Template buildTemplate (Shape shape, pointSize, img, fatBounds) : null; + // Generate the template instance final Template tpl = new Template( shape, @@ -488,6 +489,29 @@ private static void addHoles (Shape shape, } } + //-------------// + // adjustHoles // + //------------// + /** + * Correct any hole pixel left over. + * + * @param img the source image + * @param box bounds of symbol relative to image + */ + private static void adjustHoles (BufferedImage img, + Rectangle box) + { + final FloodFiller floodFiller = new FloodFiller(img); + + for (int iy = box.y + 1, iyBreak = box.y + box.height - 1; iy < iyBreak; iy++) { + for (int ix = box.x + 1, ixBreak = box.x + box.width - 1; ix < ixBreak; ix++) { + if (isBackground(img, ix, iy)) { + floodFiller.adjust(ix, iy, HOLE); + } + } + } + } + //----------// // binarize // //----------// @@ -524,6 +548,7 @@ private static void binarize (BufferedImage img, //----------------// /** * Build the collection of key points to be used for matching tests. + *

* These are the locations where the image distance value will be checked against the recorded * template distance value. * @@ -532,10 +557,11 @@ private static void binarize (BufferedImage img, * @return the collection of key locations, with their corresponding distance value */ private static List buildKeyPoints (BufferedImage img, - DistanceTable distances) + DistanceTable distances, + int pointSize) { final List keyPoints = new ArrayList<>(); - final int maxDist = constants.maxRawDistanceFromSymbol.getSourceValue(); + final int maxDist = maxRawDistanceFromSymbol(pointSize); for (int y = 0, h = img.getHeight(); y < h; y++) { for (int x = 0, w = img.getWidth(); x < w; x++) { @@ -588,7 +614,7 @@ private static DistanceTable computeDistances (BufferedImage img, // Compute template distance transform final DistanceTable distances = new ChamferDistance.Short().compute(fore); - if (logger.isDebugEnabled()) { + if (logger.isTraceEnabled()) { TableUtil.dump(shape + " distances", distances); } @@ -606,9 +632,10 @@ private static DistanceTable computeDistances (BufferedImage img, * @param img the image to modify */ private static void flagIrrelevantPixels (BufferedImage img, - DistanceTable distances) + DistanceTable distances, + int pointSize) { - final int maxDist = constants.maxRawDistanceFromSymbol.getValue(); + final int maxDist = maxRawDistanceFromSymbol(pointSize); for (int y = 0, h = img.getHeight(); y < h; y++) { for (int x = 0, w = img.getWidth(); x < w; x++) { @@ -641,7 +668,7 @@ private static double getBottom (Shape shape, case NOTEHEAD_CROSS_VOID -> slimBox.y + slimBox.height; case NOTEHEAD_DIAMOND_FILLED, NOTEHEAD_DIAMOND_VOID -> slimBox.y + slimBox.height / 2.0; case NOTEHEAD_TRIANGLE_DOWN_FILLED, NOTEHEAD_TRIANGLE_DOWN_VOID -> slimBox.y; - + default -> slimBox.y + slimBox.height * (1 + constants.stemDy.getValue()); }; // @formatter:on @@ -806,7 +833,7 @@ public static void main (String... args) final int max; if ((args == null) || (args.length == 0)) { - min = max = constants.defaultDecoratedPointSize.getValue(); + min = max = constants.defaultPointSize.getValue(); } else { min = Integer.decode(args[0]); max = (args.length > 1) ? Integer.decode(args[1]) : min; @@ -814,6 +841,8 @@ public static void main (String... args) logger.info("minPointSize:{} maxPointSize{}", min, max); + MusicFont.populateAllSymbols(); + final TemplateFactory factory = TemplateFactory.getInstance(); for (MusicFamily family : MusicFamily.values()) { @@ -837,6 +866,23 @@ public static double maxDistanceFromSymbol () / (double) ChamferDistance.DEFAULT_NORMALIZER; } + //--------------------------// + // maxRawDistanceFromSymbol // + //--------------------------// + /** + * Report the maximum raw distance, adjusted according to provided pointSize, + * from symbol foreground for a pixel to be considered. + * + * @param pointSize font point size + * @return the (adjusted) maximum raw distance + */ + private static int maxRawDistanceFromSymbol (int pointSize) + { + return (int) Math.rint( + constants.maxRawDistanceFromSymbol.getValue() // + * ((double) pointSize / constants.defaultPointSize.getValue())); + } + //-------------------// // retrieveKeyPoints // //-------------------// @@ -863,19 +909,34 @@ private static List retrieveKeyPoints (Shape shape, Rectangle fatBounds) { logger.debug("Building template keyPoints for {} {} {}", shape, family, pointSize); + // Distances to foreground final DistanceTable distances = computeDistances(img, shape); // Add holes if any if (shapesWithHoles.contains(shape)) { addHoles(shape, img, fatBounds); // B=FORE & W=BACK + HOLE pixels + + // Safety check + if (img.getRGB(0, 0) == HOLE) { + logger.warn( + "*** UNUSABLE template *** for shape {} in family {} pointSize {}", + shape, + family, + pointSize); + } } - // Flag non-relevant pixels - flagIrrelevantPixels(img, distances); // B=FORE & W=BACK + HOLE pixels + IRRELEVANT pixels + // Flag non-relevant pixels: B=FORE & W=BACK + HOLE pixels + IRRELEVANT pixels + flagIrrelevantPixels(img, distances, pointSize); + + // Adjust holes if any + if (shapesWithHoles.contains(shape)) { + adjustHoles(img, fatBounds); // HOLE + } // Generate key points for relevant pixels only (fore, holes or back) - return buildKeyPoints(img, distances); + return buildKeyPoints(img, distances, pointSize); } //~ Inner Classes ------------------------------------------------------------------------------ @@ -975,10 +1036,10 @@ private static class Constants 50, "Magnification ratio of template images for visual check"); - private final Constant.Integer defaultDecoratedPointSize = new Constant.Integer( + private final Constant.Integer defaultPointSize = new Constant.Integer( "pointSize", - 69, - "Default point size used to generate decorated images"); + 80, + "Default point size"); private final Constant.Integer minCellPerSide = new Constant.Integer( "pixels", diff --git a/src/main/org/audiveris/omr/score/LogicalPart.java b/src/main/org/audiveris/omr/score/LogicalPart.java index b541f6974..cfe88af59 100644 --- a/src/main/org/audiveris/omr/score/LogicalPart.java +++ b/src/main/org/audiveris/omr/score/LogicalPart.java @@ -41,7 +41,7 @@ import javax.xml.bind.annotation.XmlElement; /** - * Class LogicalPart describes a part at score or page level. + * Class LogicalPart describes a part at score level. *

* It can be "instantiated" in one or several SystemInfo by a (physical) * {@link Part} instance. diff --git a/src/main/org/audiveris/omr/sheet/Part.java b/src/main/org/audiveris/omr/sheet/Part.java index 9ac4779ab..03aaccd5f 100644 --- a/src/main/org/audiveris/omr/sheet/Part.java +++ b/src/main/org/audiveris/omr/sheet/Part.java @@ -94,8 +94,8 @@ * {@link LogicalPart}. *

* Generally, such LogicalPart corresponds to a separate (physical) Part instance in each - * system but not always. For example, a singer part may not appear at the very beginning of a - * score, but only after one or several systems played by the piano part. + * system but not always. For example, a singer part may appear only after one or several systems + * played by the piano part. *

* We assume that the configuration of staves within the physical Part instances of the same logical * LogicalPart do not vary (in number of staves, in number of lines in staves or in relative @@ -106,8 +106,6 @@ * During export to MusicXML, dummy parts (and their contained dummy staves and measures) can be * virtually inserted on-the-fly into the structure of page/system/part/measure/staff * to ease the handling of logical parts along the pages and score. - *

- * TODO: include the image of a "merged grand staff". * * @author Hervé Bitteur */ @@ -148,6 +146,10 @@ public class Part /** * This boolean indicates a merged grand staff part, made of 2 staves * not separated by a standard gutter. + *

+ * See example: + *
+ * merged-grand-staff */ @XmlAttribute(name = "merged-grand-staff") @XmlJavaTypeAdapter(type = boolean.class, value = Jaxb.BooleanPositiveAdapter.class) diff --git a/src/main/org/audiveris/omr/sheet/Staff.java b/src/main/org/audiveris/omr/sheet/Staff.java index cacef7ede..1d3c0881b 100644 --- a/src/main/org/audiveris/omr/sheet/Staff.java +++ b/src/main/org/audiveris/omr/sheet/Staff.java @@ -21,7 +21,6 @@ // package org.audiveris.omr.sheet; -import org.audiveris.omr.score.StaffConfig; import org.audiveris.omr.constant.ConstantSet; import org.audiveris.omr.glyph.Glyph; import org.audiveris.omr.glyph.GlyphIndex; @@ -31,6 +30,7 @@ import org.audiveris.omr.math.PointUtil; import org.audiveris.omr.math.Population; import org.audiveris.omr.run.Orientation; +import org.audiveris.omr.score.StaffConfig; import org.audiveris.omr.sheet.grid.LineInfo; import org.audiveris.omr.sheet.grid.StaffFilament; import org.audiveris.omr.sheet.header.StaffHeader; @@ -538,7 +538,7 @@ private void buildLedgerLines (GeoPath line, { for (int i = increment;; i += increment) { final List ledgers = ledgerMap.get(i); - if (ledgers == null) { + if ((ledgers == null) || ledgers.isEmpty()) { return; } diff --git a/src/main/org/audiveris/omr/sheet/doc-files/merged_grand_staff.png b/src/main/org/audiveris/omr/sheet/doc-files/merged_grand_staff.png new file mode 100644 index 000000000..8a8dbea1c Binary files /dev/null and b/src/main/org/audiveris/omr/sheet/doc-files/merged_grand_staff.png differ diff --git a/src/main/org/audiveris/omr/sheet/symbol/LinksStep.java b/src/main/org/audiveris/omr/sheet/symbol/LinksStep.java index bf9b2c3e7..f6a41b258 100644 --- a/src/main/org/audiveris/omr/sheet/symbol/LinksStep.java +++ b/src/main/org/audiveris/omr/sheet/symbol/LinksStep.java @@ -179,8 +179,7 @@ public void impact (UITaskList seq, if ((opKind != OpKind.UNDO) && task instanceof AdditionTask) { linker.linkOneSentence(sentence); - } else if (task instanceof SentenceRoleTask) { - SentenceRoleTask roleTask = (SentenceRoleTask) interTask; + } else if (task instanceof SentenceRoleTask roleTask) { linker.unlinkOneSentence( sentence, (opKind == OpKind.UNDO) ? roleTask.getNewRole() diff --git a/src/main/org/audiveris/omr/sheet/symbol/SymbolsLinker.java b/src/main/org/audiveris/omr/sheet/symbol/SymbolsLinker.java index ce7092de8..2b4344201 100644 --- a/src/main/org/audiveris/omr/sheet/symbol/SymbolsLinker.java +++ b/src/main/org/audiveris/omr/sheet/symbol/SymbolsLinker.java @@ -31,6 +31,7 @@ import org.audiveris.omr.sig.inter.AbstractChordInter; import org.audiveris.omr.sig.inter.NumberInter; import org.audiveris.omr.sig.inter.BeamGroupInter; +import org.audiveris.omr.sig.inter.ChordNameInter; import org.audiveris.omr.sig.inter.DynamicsInter; import org.audiveris.omr.sig.inter.FermataArcInter; import org.audiveris.omr.sig.inter.FermataDotInter; @@ -45,7 +46,6 @@ import org.audiveris.omr.sig.inter.SlurInter; import org.audiveris.omr.sig.inter.SmallChordInter; import org.audiveris.omr.sig.inter.WedgeInter; -import org.audiveris.omr.sig.inter.WordInter; import org.audiveris.omr.sig.relation.ChordGraceRelation; import org.audiveris.omr.sig.relation.ChordNameRelation; import org.audiveris.omr.sig.relation.ChordSentenceRelation; @@ -61,7 +61,6 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.awt.Point; import java.awt.Rectangle; import java.awt.geom.Point2D; import java.util.Collection; @@ -370,22 +369,13 @@ public void linkOneSentence (SentenceInter sentence) { // Map each word with proper chord, in assigned staff for (Inter wInter : sentence.getMembers()) { - WordInter word = (WordInter) wInter; - Point wordCenter = word.getCenter(); - MeasureStack stack = system.getStackAt(wordCenter); + final ChordNameInter word = (ChordNameInter) wInter; + final Link link = word.lookupLink(system); - if (stack == null) { - logger.info("No stack at {}", word); + if (link == null) { + logger.info("No chord below {}", word); } else { - AbstractChordInter chordBelow = stack.getStandardChordBelow( - wordCenter, - word.getBounds()); - - if (chordBelow != null) { - sig.addEdge(chordBelow, word, new ChordNameRelation()); - } else { - logger.info("No chord below {}", word); - } + link.applyTo(wInter); } } } diff --git a/src/main/org/audiveris/omr/sheet/ui/BookActions.java b/src/main/org/audiveris/omr/sheet/ui/BookActions.java index a8b670a62..647351ea8 100644 --- a/src/main/org/audiveris/omr/sheet/ui/BookActions.java +++ b/src/main/org/audiveris/omr/sheet/ui/BookActions.java @@ -2565,9 +2565,9 @@ public boolean setSpecific (Boolean specific) // ResetBookTask // //---------------// /** - * Task that reset all valid selected sheets of book to gray or binary. + * Task that resets all valid selected sheets of book to gray or binary. */ - private static class ResetBookTask + private class ResetBookTask extends VoidTask { @@ -2588,6 +2588,7 @@ protected Void doInBackground () { book.resetTo(step); + setBookTranscribable(true); return null; } diff --git a/src/main/org/audiveris/omr/sheet/ui/resources/SplitAndMerge_fr.properties b/src/main/org/audiveris/omr/sheet/ui/resources/SplitAndMerge_fr.properties index 1a0464caa..ad566ae9b 100644 --- a/src/main/org/audiveris/omr/sheet/ui/resources/SplitAndMerge_fr.properties +++ b/src/main/org/audiveris/omr/sheet/ui/resources/SplitAndMerge_fr.properties @@ -20,11 +20,11 @@ include.Action.shortDescription = Inclure un document parmi ceux d\u00e9j\u00e0 loadFiles.Action.text = Charger fichiers... loadFiles.Action.shortDescription = Charger des documents ou des images dans la playlist -moveDown.Action.text = Vers le haut -moveDown.Action.shortDescription = D\u00e9placer vers le haut l'extrait s\u00e9lectionn\u00e9 +moveDown.Action.text = Vers le bas +moveDown.Action.shortDescription = D\u00e9placer vers le bas l'extrait s\u00e9lectionn\u00e9 -moveUp.Action.text = Vers le bas -moveUp.Action.shortDescription = D\u00e9placer vers le bas l'extrait s\u00e9lectionn\u00e9 +moveUp.Action.text = Vers le haut +moveUp.Action.shortDescription = D\u00e9placer vers le haut l'extrait s\u00e9lectionn\u00e9 open.Action.text = Ouvrir PlayList... open.Action.shortDescription = Ouvrir un ficher playlist diff --git a/src/main/org/audiveris/omr/sig/SigReducer.java b/src/main/org/audiveris/omr/sig/SigReducer.java index 7978c2979..eca9ff3e3 100644 --- a/src/main/org/audiveris/omr/sig/SigReducer.java +++ b/src/main/org/audiveris/omr/sig/SigReducer.java @@ -290,6 +290,21 @@ private void analyzeChords () sig.insertExclusion(stem, ih, Exclusion.ExclusionCause.OVERLAP); } + // Mutual head exclusion between shapes with different size (small vs standard) + final Set smallHeads = new LinkedHashSet<>(); + final Set stdHeads = new LinkedHashSet<>(); + for (Entry> headEntry : heads.entrySet()) { + final Set headSet = headEntry.getValue(); + if (headSet != null) { + if (headEntry.getKey().isSmallHead()) { + smallHeads.addAll(headSet); + } else { + stdHeads.addAll(headSet); + } + } + } + exclude(smallHeads, stdHeads, INCOMPATIBLE); + // Mutual head exclusion between shapes with different DURATION // Mutual head support within shapes with same DURATION final List> allDurSets = new ArrayList<>(durs.values()); @@ -326,15 +341,15 @@ private void analyzeChords () } // Head/Beam support or exclusion based on size - for (Entry> entry : beams.entrySet()) { - final Size size = entry.getKey(); - final Set beamSet = entry.getValue(); + for (Entry> beamEntry : beams.entrySet()) { + final Size beamSize = beamEntry.getKey(); + final Set beamSet = beamEntry.getValue(); - if (size == Size.SMALL) { + if (beamSize == Size.SMALL) { // Small beams exclude all but small (oval) heads for (Entry> headEntry : heads.entrySet()) { if (!headEntry.getKey().isSmallHead()) { - final Set headSet = entry.getValue(); + final Set headSet = headEntry.getValue(); if (headSet != null) { exclude(beamSet, headSet, INCOMPATIBLE); diff --git a/src/main/org/audiveris/omr/sig/inter/ChordNameInter.java b/src/main/org/audiveris/omr/sig/inter/ChordNameInter.java index 66c234734..cdd56c587 100644 --- a/src/main/org/audiveris/omr/sig/inter/ChordNameInter.java +++ b/src/main/org/audiveris/omr/sig/inter/ChordNameInter.java @@ -25,6 +25,7 @@ import org.audiveris.omr.glyph.Shape; import org.audiveris.omr.math.PointUtil; import org.audiveris.omr.sheet.SystemInfo; +import org.audiveris.omr.sheet.rhythm.MeasureStack; import static org.audiveris.omr.sig.inter.ChordNameInter.ChordKind.ChordType.AUGMENTED; import static org.audiveris.omr.sig.inter.ChordNameInter.ChordKind.ChordType.AUGMENTED_SEVENTH; import static org.audiveris.omr.sig.inter.ChordNameInter.ChordKind.ChordType.DIMINISHED; @@ -49,6 +50,7 @@ import static org.audiveris.omr.sig.inter.ChordNameInter.ChordKind.ChordType.MINOR_SIXTH; import static org.audiveris.omr.sig.inter.ChordNameInter.ChordKind.ChordType.SUSPENDED_FOURTH; import static org.audiveris.omr.sig.inter.ChordNameInter.ChordKind.ChordType.SUSPENDED_SECOND; +import org.audiveris.omr.sig.relation.ChordNameRelation; import org.audiveris.omr.sig.relation.Containment; import org.audiveris.omr.sig.relation.Link; import org.audiveris.omr.sig.ui.AdditionTask; @@ -65,8 +67,11 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.awt.Point; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; import java.util.List; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -555,6 +560,28 @@ private static List getPatterns () return patterns; } + //------------// + // lookupLink // + //------------// + /** + * Try to detect a link between this ChordNameInter and a HeadChord nearby. + * + * @param system system to be looked up + * @return the link found or null + */ + public Link lookupLink (SystemInfo system) + { + final Point wordCenter = getCenter(); + final MeasureStack stack = system.getStackAt(wordCenter); + final AbstractChordInter chordBelow = stack.getStandardChordBelow(wordCenter, getBounds()); + + if (chordBelow == null) { + return null; + } + + return new Link(chordBelow, new ChordNameRelation(), false); + } + //------------// // parseChord // //------------// @@ -621,6 +648,27 @@ private static ChordStructure parseChord (String value) return null; } + //-------------// + // searchLinks // + //-------------// + @Override + public Collection searchLinks (SystemInfo system) + { + final Link link = lookupLink(system); + + return (link == null) ? Collections.emptyList() : Collections.singleton(link); + } + + //---------------// + // searchUnlinks // + //---------------// + @Override + public Collection searchUnlinks (SystemInfo system, + Collection links) + { + return searchObsoletelinks(links, ChordNameRelation.class); + } + //----------// // standard // //----------// diff --git a/src/main/org/audiveris/omr/sig/inter/HeadInter.java b/src/main/org/audiveris/omr/sig/inter/HeadInter.java index 9f2d0da5b..1e8da4f52 100644 --- a/src/main/org/audiveris/omr/sig/inter/HeadInter.java +++ b/src/main/org/audiveris/omr/sig/inter/HeadInter.java @@ -1027,7 +1027,9 @@ public List preAdd (WrappedBoolean cancel, if (!stemFound) { // Head without stem - HeadChordInter headChord = new HeadChordInter(null); + HeadChordInter headChord = shape.isSmallHead() // + ? new SmallChordInter(null) + : new HeadChordInter(null); tasks.add( new AdditionTask( theSig, diff --git a/src/main/org/audiveris/omr/sig/inter/SmallChordInter.java b/src/main/org/audiveris/omr/sig/inter/SmallChordInter.java index 393c25e72..750d61543 100644 --- a/src/main/org/audiveris/omr/sig/inter/SmallChordInter.java +++ b/src/main/org/audiveris/omr/sig/inter/SmallChordInter.java @@ -121,6 +121,9 @@ public Voice getVoice () for (Relation rel : sig.getRelations(lastChord, ChordGraceRelation.class)) { AbstractChordInter stdChord = (AbstractChordInter) sig.getOppositeInter(lastChord, rel); + if (stdChord instanceof SmallChordInter) { + continue; // Safer + } return stdChord.getVoice(); } diff --git a/src/main/org/audiveris/omr/sig/inter/StemInter.java b/src/main/org/audiveris/omr/sig/inter/StemInter.java index 8528a1d32..c7a94603c 100644 --- a/src/main/org/audiveris/omr/sig/inter/StemInter.java +++ b/src/main/org/audiveris/omr/sig/inter/StemInter.java @@ -137,12 +137,9 @@ public class StemInter //~ Constructors ------------------------------------------------------------------------------- - /** - * No-arg constructor meant for JAXB. - */ protected StemInter () { - super(null, null, 0.0); + super(null, Shape.STEM, 0.0); } /** diff --git a/src/main/org/audiveris/omr/sig/ui/InterController.java b/src/main/org/audiveris/omr/sig/ui/InterController.java index 728a0c231..f0ed673e9 100644 --- a/src/main/org/audiveris/omr/sig/ui/InterController.java +++ b/src/main/org/audiveris/omr/sig/ui/InterController.java @@ -522,7 +522,6 @@ protected void build () case Lyrics -> { // Convert to LyricItem words, all within a single LyricLine sentence - final WrappedBoolean cancel = new WrappedBoolean(false); final LyricLineInter line = new LyricLineInter( sentence.getBounds(), sentence.getGrade(), @@ -556,11 +555,6 @@ protected void build () seq.add(new LinkTask(sig, link.partner, item, link.relation)); } - if (cancel.isSet()) { - seq.setCancelled(true); - return; - } - // Remove the plain word seq.add(new RemovalTask(orgWord)); } @@ -574,37 +568,30 @@ protected void build () case ChordName -> { - // Convert to ChordName words, each within its own sentence - final WrappedBoolean cancel = new WrappedBoolean(false); - + // Convert to ChordName words within the sentence for (Inter inter : sentence.getMembers()) { if (!(inter instanceof ChordNameInter)) { // Add a new ChordNameInter for any original word - WordInter orgWord = (WordInter) inter; - ChordNameInter cn = new ChordNameInter(orgWord); - cn.setManual(true); - cn.setStaff(staff); + final WordInter orgWord = (WordInter) inter; + final ChordNameInter chordName = new ChordNameInter(orgWord); + chordName.setManual(true); + chordName.setStaff(staff); seq.add( new AdditionTask( sig, - cn, - cn.getBounds(), + chordName, + chordName.getBounds(), Arrays.asList( new Link(sentence, new Containment(), false)))); - if (cancel.isSet()) { - seq.setCancelled(true); - return; - } - // Remove the original word seq.add(new RemovalTask(orgWord)); - } - } - if (!seq.getTasks().isEmpty()) { - // Remove the now useless original sentence (and its original relations) - seq.add(new RemovalTask(sentence)); + // Look for suitable related head chord + for (Link link : chordName.searchLinks(system)) { + seq.add(new LinkTask(sig, link.partner, chordName, link.relation)); + } + } } }