Compare commits

..

21 Commits

Author SHA1 Message Date
d117052ca4 Aggregate Change
All checks were successful
zdm/undo-redo/pipeline/head This commit looks good
The aggregate change combines multiple changes into one while
implementing the Change interface.
2022-02-11 09:48:41 +01:00
b30f806894 Improve Javadoc, make wrapper properties final 2022-02-11 09:48:34 +01:00
4872fd3db3 Merge pull request 'Bump Version to 0.1.0' (#9) from r/0.1.0 into develop
All checks were successful
zdm/undo-redo/pipeline/head This commit looks good
Reviewed-on: https://git.kske.dev/zdm/undo-redo/pulls/9
Reviewed-by: DieGurke <maxi@kske.dev>
Reviewed-by: delvh <leon@kske.dev>
2021-12-25 21:33:57 +01:00
833c346914 Bump version to 0.1.0
All checks were successful
zdm/undo-redo/pipeline/head This commit looks good
2021-12-25 22:27:28 +02:00
8ef4a9a572 Fix typo in Javadoc tag 2021-12-25 22:27:03 +02:00
ae2f2e8a84 Merge pull request 'Fix ChangeManagerWrapper' (#8) from b/javafx-wrapper-properties into develop
All checks were successful
zdm/undo-redo/pipeline/head This commit looks good
Reviewed-on: https://git.kske.dev/zdm/undo-redo/pulls/8
Reviewed-by: DieGurke <maxi@kske.dev>
Reviewed-by: delvh <leon@kske.dev>
2021-12-25 21:14:53 +01:00
7101523584 Update properties on wrapper creation, fix NPE
All checks were successful
zdm/undo-redo/pipeline/head This commit looks good
2021-12-25 21:57:25 +02:00
4a70d954ef Merge pull request 'Correctly Deal with Identity Changes and Divergent History' (#5) from b/divergent-history into develop
All checks were successful
zdm/undo-redo/pipeline/head This commit looks good
Reviewed-on: https://git.kske.dev/zdm/undo-redo/pulls/5
Reviewed-by: delvh <leon@kske.dev>
2021-12-24 22:16:45 +01:00
d649f24ad8 Merge pull request 'Add Change Manager Unmarking' (#6) from f/change-unmarking into develop
Reviewed-on: https://git.kske.dev/zdm/undo-redo/pulls/6
Reviewed-by: delvh <leon@kske.dev>
2021-12-24 22:16:36 +01:00
9c2971a078 Merge pull request 'Generate Code Coverage Report for Jenkins' (#7) from f/coverage into develop
All checks were successful
zdm/undo-redo/pipeline/head This commit looks good
Reviewed-on: https://git.kske.dev/zdm/undo-redo/pulls/7
Reviewed-by: delvh <leon@kske.dev>
2021-12-24 14:37:58 +01:00
d26c0fe256 Publish coverage report to Jenkins
All checks were successful
zdm/undo-redo/pipeline/head This commit looks good
2021-12-24 15:12:13 +02:00
a39d03abf8 Generate coverage report
All checks were successful
zdm/undo-redo/pipeline/head This commit looks good
2021-12-24 14:15:14 +02:00
0fc3577750 Add change manager unmarking
All checks were successful
zdm/undo-redo/pipeline/head This commit looks good
2021-12-23 10:05:29 +02:00
d484839c8b Fix divergent changes removal, improve unit test
All checks were successful
zdm/undo-redo/pipeline/head This commit looks good
2021-12-23 09:42:33 +02:00
5d1ef84770 Discard identity changes, discard divergent branches
All checks were successful
zdm/undo-redo/pipeline/head This commit looks good
2021-12-22 16:51:33 +02:00
1155541350 Fix javafx module artifact name
All checks were successful
zdm/undo-redo/pipeline/head This commit looks good
2021-12-22 16:32:27 +02:00
fa5c2419bf Merge pull request 'JavaFX Integration' (#4) from f/javafx into develop
All checks were successful
zdm/undo-redo/pipeline/head This commit looks good
Reviewed-on: https://git.kske.dev/zdm/undo-redo/pulls/4
Reviewed-by: DieGurke <maxi@kske.dev>
Reviewed-by: delvh <leon@kske.dev>
2021-12-16 12:05:49 +01:00
0f1c3e06d8 JavaFX reflective access + Javadoc + @delvh
All checks were successful
zdm/undo-redo/pipeline/head This commit looks good
* Allow reflective access to the JavaFX module for javafx.base
* Add space to the unit test orders
* Improve Javadoc
* Add @delvh as developer
2021-12-16 10:22:37 +01:00
1b6d7f3dde Add ObservableChangeManager interface with wrapper implementation
All checks were successful
zdm/undo-redo/pipeline/head This commit looks good
2021-12-12 08:05:24 +01:00
d49772a127 Add javafx module
Some checks failed
zdm/undo-redo/pipeline/head There was a failure building this commit
The undo-redo-javafx module will contain the JavaFX-compatible API for
Undo-Redo.
2021-12-12 05:59:28 +01:00
d80dd94e90 Allow retrieving the last change from a change manager 2021-12-12 05:49:47 +01:00
17 changed files with 560 additions and 28 deletions

1
Jenkinsfile vendored
View File

@ -18,6 +18,7 @@ pipeline {
post {
always {
junit '*/target/surefire-reports/*.xml'
publishCoverage adapters: [jacocoAdapter(mergeToOneReport: true, path: '*/target/site/jacoco/jacoco.xml')]
}
}
}

View File

@ -9,7 +9,7 @@
<parent>
<groupId>dev.kske</groupId>
<artifactId>undo-redo</artifactId>
<version>0.0.1-SNAPSHOT</version>
<version>0.1.0</version>
</parent>
</project>

View File

@ -0,0 +1,47 @@
package dev.kske.undoredo.core;
import java.util.*;
import java.util.stream.Collectors;
/**
* @author Kai S. K. Engelbart
* @since 0.2.0
*/
public final class AggregateChange<C extends Change> implements Change {
private final List<C> changes = new ArrayList<>();
@SafeVarargs
public AggregateChange(C... changes) {
this(Arrays.asList(changes));
}
public AggregateChange(Collection<C> changes) {
changes.addAll(changes);
}
@Override
public void apply() {
changes.forEach(Change::apply);
}
@Override
public Change invert() {
List<Change> invertedChanges =
changes
.parallelStream()
.map(Change::invert)
.collect(Collectors.toList());
Collections.reverse(invertedChanges);
return new AggregateChange<>(invertedChanges);
}
@Override
public boolean isIdentity() {
return changes.stream().allMatch(Change::isIdentity);
}
public List<C> changes() {
return Collections.unmodifiableList(changes);
}
}

View File

@ -1,7 +1,7 @@
package dev.kske.undoredo.core;
/**
* Base interface for changes to be registered in an undo manager.
* Base interface for changes to be registered in a change manager.
*
* @author Maximilian K&auml;fer
* @since 0.0.1
@ -26,6 +26,8 @@ public interface Change {
Change invert();
/**
* @apiNote If this method returns {@code true} when adding this change to a change manager, it
* will be discarded immediately and therefore can be garbage collected.
* @return whether the application of this change would result in an identical state
* @since 0.0.1
*/

View File

@ -1,11 +1,15 @@
package dev.kske.undoredo.core;
import java.util.List;
import java.util.*;
/**
* A change manager keeps track of subsequent changes and allows un- and redoing them. A specific
* change can be marked using {@link #mark()} to keep track of a saved state in the application that
* uses the manager.
* <p>
* If you intend to listen to the state of a change manager, consider writing a wrapper
* implementation for an existing change manager that adds the necessary hooks. If you use JavaFX,
* take a look at the {@code dev.kske.undoredo.javafx} module.
*
* @param <C> the change type to store in this change manager
* @author Maximilian K&auml;fer
@ -15,12 +19,20 @@ import java.util.List;
public interface ChangeManager<C extends Change> {
/**
* Applies the given change and appends it to the change list.
* Applies the given change and appends it to the change list. If the change is an identity, no
* action is taken.
*
* @param change the change to add
* @return whether the change has been added
* @since 0.0.1
*/
void addChange(C change);
boolean addChange(C change);
/**
* @return the change that was applied last
* @since 0.0.1
*/
Optional<C> getLastChange();
/**
* Undoes the current change.
@ -45,11 +57,18 @@ public interface ChangeManager<C extends Change> {
*/
void mark();
/**
* Resets the marked index so that no change is marked.
*
* @since 0.0.1
*/
void unmark();
/**
* @return whether the current change is marked
* @since 0.0.1
*/
boolean isAtMarkedIndex();
boolean isAtMarkedChange();
/**
* @return whether a change is present that can be undone

View File

@ -3,6 +3,8 @@ package dev.kske.undoredo.core;
import java.util.*;
/**
* A simple change manager with a linear history model backed by an array list.
*
* @param <C> the change type to store in this change manager
* @author Maximilian K&auml;fer
* @author Kai S. K. Engelbart
@ -15,11 +17,28 @@ public final class UnlimitedChangeManager<C extends Change> implements ChangeMan
private int index = -1;
private int markedIndex = -1;
/**
* @implNote As this change manager uses a linear history model, all changes behind the last
* applied change will be discarded and therefore can be garbage collected.
*/
@Override
public void addChange(C change) {
public boolean addChange(C change) {
if (!change.isIdentity()) {
change.apply();
// Remove divergent changes
changes.subList(index + 1, changes.size()).clear();
changes.add(change);
++index;
return true;
}
return false;
}
@Override
public Optional<C> getLastChange() {
return index < 0 ? Optional.empty() : Optional.of(changes.get(index));
}
@Override
@ -48,7 +67,12 @@ public final class UnlimitedChangeManager<C extends Change> implements ChangeMan
}
@Override
public boolean isAtMarkedIndex() {
public void unmark() {
markedIndex = -1;
}
@Override
public boolean isAtMarkedChange() {
return markedIndex == index;
}

View File

@ -14,11 +14,22 @@ class ChangeManagerTest {
IntWrapper wrapper;
IntChange change;
/**
* Generates an int change with the given value.
*
* @param value the value of the change
* @return the created change
* @since 0.0.1
*/
IntChange change(int value) {
return new IntChange(wrapper, value);
}
@BeforeEach
void prepareChangeManager() {
manager = new UnlimitedChangeManager<>();
wrapper = new IntWrapper();
change = new IntChange(wrapper, 1);
change = change(1);
}
/**
@ -27,20 +38,46 @@ class ChangeManagerTest {
* @since 0.0.1
*/
@Test
@Order(1)
@Order(10)
void testAddChange() {
assertSame(0, wrapper.value);
manager.addChange(change);
assertTrue(manager.addChange(change));
assertSame(1, wrapper.value);
}
/**
* Tests whether identity changes are ignored when adding them to the change manager.
*
* @since 0.0.1
*/
@Test
@Order(15)
void testDiscardIdentity() {
manager.addChange(change);
assertFalse(manager.addChange(change(0)));
assertSame(1, wrapper.value);
}
/**
* Tests retrieving the last change.
*
* @since 0.0.1
*/
@Test
@Order(20)
void testLastChange() {
assertTrue(manager.getLastChange().isEmpty());
manager.addChange(change);
assertEquals(change, manager.getLastChange().get());
}
/**
* Tests the consistency of the change list.
*
* @since 0.0.1
*/
@Test
@Order(2)
@Order(30)
void testGetChanges() {
assertTrue(manager.getChanges().isEmpty());
manager.addChange(change);
@ -54,7 +91,7 @@ class ChangeManagerTest {
* @since 0.0.1
*/
@Test
@Order(2)
@Order(40)
void testUndo() {
assertFalse(manager.isUndoAvailable());
assertFalse(manager.undo());
@ -63,6 +100,7 @@ class ChangeManagerTest {
assertTrue(manager.undo());
assertFalse(manager.isUndoAvailable());
assertFalse(manager.undo());
assertTrue(manager.getLastChange().isEmpty());
}
/**
@ -71,7 +109,7 @@ class ChangeManagerTest {
* @since 0.0.1
*/
@Test
@Order(4)
@Order(50)
void testRedo() {
assertFalse(manager.isRedoAvailable());
assertFalse(manager.redo());
@ -83,6 +121,25 @@ class ChangeManagerTest {
assertTrue(manager.redo());
assertFalse(manager.isRedoAvailable());
assertFalse(manager.redo());
assertEquals(change, manager.getLastChange().get());
}
/**
* Tests whether the changes after the current index are discarded when adding a change.
*
* @since 0.0.1
*/
@Test
@Order(55)
void testDiscardDivergentHistory() {
manager.addChange(change);
manager.addChange(change(2));
manager.undo();
manager.undo();
assertTrue(manager.addChange(change(3)));
assertFalse(manager.isRedoAvailable());
assertSame(3, wrapper.value);
assertSame(1, manager.getChanges().size());
}
/**
@ -91,14 +148,30 @@ class ChangeManagerTest {
* @since 0.0.1
*/
@Test
@Order(5)
@Order(60)
void testMark() {
assertTrue(manager.isAtMarkedIndex());
assertTrue(manager.isAtMarkedChange());
manager.addChange(change);
assertFalse(manager.isAtMarkedIndex());
assertFalse(manager.isAtMarkedChange());
manager.mark();
assertTrue(manager.isAtMarkedIndex());
assertTrue(manager.isAtMarkedChange());
manager.undo();
assertFalse(manager.isAtMarkedIndex());
assertFalse(manager.isAtMarkedChange());
}
/**
* Tests unmarking a change.
*
* @since 0.0.1
*/
@Test
@Order(70)
void testUnmark() {
manager.addChange(change);
manager.mark();
manager.unmark();
assertFalse(manager.isAtMarkedChange());
manager.undo();
assertTrue(manager.isAtMarkedChange());
}
}

View File

@ -1,5 +1,7 @@
package dev.kske.undoredo.core;
import java.util.Objects;
/**
* @author Kai S. K. Engelbart
* @since 0.0.1
@ -28,4 +30,24 @@ class IntChange implements Change {
public boolean isIdentity() {
return value == 0;
}
@Override
public String toString() {
return String.valueOf(value);
}
@Override
public int hashCode() {
return Objects.hash(value, wrapper);
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (!(obj instanceof IntChange))
return false;
IntChange other = (IntChange) obj;
return value == other.value && Objects.equals(wrapper, other.wrapper);
}
}

View File

@ -1,5 +1,7 @@
package dev.kske.undoredo.core;
import java.util.Objects;
/**
* @author Kai S. K. Engelbart
* @since 0.0.1
@ -7,4 +9,24 @@ package dev.kske.undoredo.core;
class IntWrapper {
int value;
@Override
public String toString() {
return String.valueOf(value);
}
@Override
public int hashCode() {
return Objects.hash(value);
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (!(obj instanceof IntWrapper))
return false;
IntWrapper other = (IntWrapper) obj;
return value == other.value;
}
}

27
javafx/.classpath Normal file
View File

@ -0,0 +1,27 @@
<?xml version="1.0" encoding="UTF-8"?>
<classpath>
<classpathentry kind="src" output="target/classes" path="src/main/java">
<attributes>
<attribute name="optional" value="true"/>
<attribute name="maven.pomderived" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="src" output="target/test-classes" path="src/test/java">
<attributes>
<attribute name="optional" value="true"/>
<attribute name="maven.pomderived" value="true"/>
<attribute name="test" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-11">
<attributes>
<attribute name="maven.pomderived" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="con" path="org.eclipse.m2e.MAVEN2_CLASSPATH_CONTAINER">
<attributes>
<attribute name="maven.pomderived" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="output" path="target/classes"/>
</classpath>

23
javafx/.project Normal file
View File

@ -0,0 +1,23 @@
<?xml version="1.0" encoding="UTF-8"?>
<projectDescription>
<name>undo-redo-javafx</name>
<comment></comment>
<projects>
</projects>
<buildSpec>
<buildCommand>
<name>org.eclipse.jdt.core.javabuilder</name>
<arguments>
</arguments>
</buildCommand>
<buildCommand>
<name>org.eclipse.m2e.core.maven2Builder</name>
<arguments>
</arguments>
</buildCommand>
</buildSpec>
<natures>
<nature>org.eclipse.jdt.core.javanature</nature>
<nature>org.eclipse.m2e.core.maven2Nature</nature>
</natures>
</projectDescription>

28
javafx/pom.xml Normal file
View File

@ -0,0 +1,28 @@
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<artifactId>undo-redo-javafx</artifactId>
<name>Undo-Redo JavaFX Integration</name>
<parent>
<groupId>dev.kske</groupId>
<artifactId>undo-redo</artifactId>
<version>0.1.0</version>
</parent>
<dependencies>
<dependency>
<groupId>dev.kske</groupId>
<artifactId>undo-redo-core</artifactId>
<version>0.1.0</version>
</dependency>
<dependency>
<groupId>org.openjfx</groupId>
<artifactId>javafx-base</artifactId>
<version>11</version>
</dependency>
</dependencies>
</project>

View File

@ -0,0 +1,144 @@
package dev.kske.undoredo.javafx;
import java.util.List;
import javafx.beans.property.*;
import dev.kske.undoredo.core.*;
/**
* Wraps an ordinary change manager into an observable change manager, providing the required
* properties for concrete implementations.
* <p>
* The properties have the same name as their corresponding {@code -property()} methods and can be
* accessed reflectively from JavaFX, e.g. through
* {@link javafx.beans.binding.Bindings#select(Object, String...)}. Alternatively, the property
* names are available as constants.
*
* @param <C> the change type to store in this change manager
* @param <M> the type of change manager to wrap
* @author Kai S. K. Engelbart
* @since 0.0.1
*/
public class ChangeManagerWrapper<C extends Change, M extends ChangeManager<C>>
implements ObservableChangeManager<C> {
public static final String LAST_CHANGE = "lastChange";
public static final String AT_MARKED_CHANGE = "atMarkedChange";
public static final String UNDO_AVAILABLE = "undoAvailable";
public static final String REDO_AVAILABLE = "redoAvailable";
protected final ReadOnlyObjectWrapper<C> lastChange =
new ReadOnlyObjectWrapper<>(this, LAST_CHANGE);
protected final ReadOnlyBooleanWrapper atMarkedChange =
new ReadOnlyBooleanWrapper(this, AT_MARKED_CHANGE);
protected final ReadOnlyBooleanWrapper undoAvailable =
new ReadOnlyBooleanWrapper(this, UNDO_AVAILABLE);
protected final ReadOnlyBooleanWrapper redoAvailable =
new ReadOnlyBooleanWrapper(this, REDO_AVAILABLE);
protected final M manager;
/**
* Initializes a change manager wrapper.
*
* @param manager the change manager to wrap
* @since 0.0.1
*/
public ChangeManagerWrapper(M manager) {
this.manager = manager;
updateProperties();
}
@Override
public boolean addChange(C change) {
if (manager.addChange(change)) {
updateProperties();
return true;
}
return false;
}
@Override
public boolean undo() {
if (manager.undo()) {
updateProperties();
return true;
}
return false;
}
@Override
public boolean redo() {
if (manager.redo()) {
updateProperties();
return true;
}
return false;
}
@Override
public void mark() {
manager.mark();
setAtMarkedChange(manager.isAtMarkedChange());
}
@Override
public void unmark() {
manager.unmark();
setAtMarkedChange(manager.isAtMarkedChange());
}
/**
* Sets the values of all properties to those present in the wrapped change manager.
*
* @since 0.0.1
*/
private void updateProperties() {
setLastChange(manager.getLastChange().orElse(null));
setAtMarkedChange(manager.isAtMarkedChange());
setUndoAvailable(manager.isUndoAvailable());
setRedoAvailable(manager.isRedoAvailable());
}
@Override
public final ReadOnlyObjectProperty<C> lastChangeProperty() {
return lastChange.getReadOnlyProperty();
}
protected final void setLastChange(C lastChange) {
this.lastChange.set(lastChange);
}
@Override
public final ReadOnlyBooleanProperty atMarkedChangeProperty() {
return atMarkedChange.getReadOnlyProperty();
}
protected final void setAtMarkedChange(boolean atMarkedChange) {
this.atMarkedChange.set(atMarkedChange);
}
@Override
public final ReadOnlyBooleanProperty undoAvailableProperty() {
return undoAvailable.getReadOnlyProperty();
}
protected final void setUndoAvailable(boolean undoAvailable) {
this.undoAvailable.set(undoAvailable);
}
@Override
public final ReadOnlyBooleanProperty redoAvailableProperty() {
return redoAvailable.getReadOnlyProperty();
}
protected final void setRedoAvailable(boolean redoAvailable) {
this.redoAvailable.set(redoAvailable);
}
@Override
public List<C> getChanges() {
return manager.getChanges();
}
}

View File

@ -0,0 +1,47 @@
package dev.kske.undoredo.javafx;
import java.util.Optional;
import javafx.beans.property.*;
import dev.kske.undoredo.core.*;
/**
* A change manager that exposes its state through JavaFX properties, thereby allowing a direct
* integration of Undo-Redo with JavaFX listeners and property bindings.
*
* @param <C> the change type to store in this change manager
* @author Kai S. K. Engelbart
* @since 0.0.1
* @see ChangeManagerWrapper
*/
public interface ObservableChangeManager<C extends Change> extends ChangeManager<C> {
ReadOnlyObjectProperty<C> lastChangeProperty();
@Override
default Optional<C> getLastChange() {
return Optional.ofNullable(lastChangeProperty().get());
}
ReadOnlyBooleanProperty atMarkedChangeProperty();
@Override
default boolean isAtMarkedChange() {
return atMarkedChangeProperty().get();
}
ReadOnlyBooleanProperty undoAvailableProperty();
@Override
default boolean isUndoAvailable() {
return undoAvailableProperty().get();
}
ReadOnlyBooleanProperty redoAvailableProperty();
@Override
default boolean isRedoAvailable() {
return redoAvailableProperty().get();
}
}

View File

@ -0,0 +1,7 @@
/**
* Contains JavaFX-based wrapper API for integrating Undo-Redo with JavaFX.
*
* @author Kai S. K. Engelbart
* @since 0.0.1
*/
package dev.kske.undoredo.javafx;

View File

@ -0,0 +1,15 @@
/**
* Contains JavaFX-based wrapper API for integrating Undo-Redo with JavaFX.
*
* @author Kai S. K. Engelbart
* @since 0.0.1
*/
module dev.kske.undoredo.javafx {
exports dev.kske.undoredo.javafx;
opens dev.kske.undoredo.javafx to javafx.base;
requires transitive dev.kske.undoredo.core;
requires transitive javafx.base;
}

37
pom.xml
View File

@ -5,7 +5,7 @@
<groupId>dev.kske</groupId>
<artifactId>undo-redo</artifactId>
<version>0.0.1-SNAPSHOT</version>
<version>0.1.0</version>
<packaging>pom</packaging>
<name>Undo-Redo</name>
@ -14,6 +14,7 @@
<modules>
<module>core</module>
<module>javafx</module>
</modules>
<licenses>
@ -47,6 +48,16 @@
</roles>
<timezone>Europe/Berlin</timezone>
</developer>
<developer>
<name>Leon Hofmeister</name>
<email>leon@kske.dev</email>
<url>https://git.kske.dev/delvh</url>
<roles>
<role>architect</role>
<role>developer</role>
</roles>
<timezone>Europe/Berlin</timezone>
</developer>
</developers>
<scm>
@ -171,11 +182,31 @@
<artifactId>maven-surefire-plugin</artifactId>
<version>3.0.0-M5</version>
<configuration>
<argLine>--add-opens dev.kske.undoredo.core/dev.kske.undoredo.core=ALL-UNNAMED</argLine>
<argLine>${argLine} --add-opens dev.kske.undoredo.core/dev.kske.undoredo.core=ALL-UNNAMED</argLine>
</configuration>
</plugin>
<!-- Generate coverage report -->
<plugin>
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
<version>0.8.7</version>
<executions>
<execution>
<goals>
<goal>prepare-agent</goal>
</goals>
</execution>
<execution>
<id>report</id>
<phase>test</phase>
<goals>
<goal>report</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>