Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add BlockListener support... #1575

Open
wants to merge 21 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions docs/extensions.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -1377,3 +1377,25 @@ It is primarily for framework developers who want to provide a default value for
Or users of a framework that doesn't provide default values for their special types.

If you want to change the default response behavior for `Stub` have a look at <<interaction_based_testing.adoc#ALaCarteMocks,A la Carte Mocks>> and how to use your own `org.spockframework.mock.IDefaultResponse`.

=== Listeners

Extensions can register listeners to receive notifications about the progress of the test run.
These listeners are intended to be used for reporting, logging, or other monitoring purposes.
They are not intended to modify the test run in any way.
You can register the same listener instance on multiple specifications or features.
Please consult the JavaDoc of the respective listener interfaces for more information.

==== `IRunListener`

The `org.spockframework.runtime.IRunListener` can be registered via `SpecInfo.addListener(IRunListener)` and will receive notifications about the progress of the test run of a single specification.

==== `IBlockListener`

The `org.spockframework.runtime.extension.IBlockListener` can be registered on a feature via, for example, `FeatureInfo.addBlockListener(IBlockListener)` and will receive notifications about the progress of the feature.

It will be called once when entering a block (`blockEntered`) and once when exiting a block (`blockExited`).

When an exception is thrown in a block, the `blockExited` will not be called for that block.
The failed block will be part of the `ErrorContext` in `ErrorInfo` that is passed to `IRunListener.error(ErrorInfo)`.
If a `cleanup` block is present the cleanup block listener methods will still be called.
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ public class AstNodeCache {
public final ClassNode SpecificationContext = ClassHelper.makeWithoutCaching(SpecificationContext.class);
public final ClassNode DataVariableMultiplication = ClassHelper.makeWithoutCaching(DataVariableMultiplication.class);
public final ClassNode DataVariableMultiplicationFactor = ClassHelper.makeWithoutCaching(DataVariableMultiplicationFactor.class);
public final ClassNode BlockInfo = ClassHelper.makeWithoutCaching(BlockInfo.class);

public final MethodNode SpecInternals_GetSpecificationContext =
SpecInternals.getDeclaredMethods(Identifiers.GET_SPECIFICATION_CONTEXT).get(0);
Expand All @@ -71,6 +72,12 @@ public class AstNodeCache {
public final MethodNode SpockRuntime_DespreadList =
SpockRuntime.getDeclaredMethods(org.spockframework.runtime.SpockRuntime.DESPREAD_LIST).get(0);

public final MethodNode SpockRuntime_CallEnterBlock =
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rename if method gets renamed

SpockRuntime.getDeclaredMethods(org.spockframework.runtime.SpockRuntime.CALL_ENTER_BLOCK).get(0);

public final MethodNode SpockRuntime_CallExitBlock =
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rename if method gets renamed

SpockRuntime.getDeclaredMethods(org.spockframework.runtime.SpockRuntime.CALL_EXIT_BLOCK).get(0);

public final MethodNode ValueRecorder_Reset =
ValueRecorder.getDeclaredMethods(org.spockframework.runtime.ValueRecorder.RESET).get(0);

Expand Down Expand Up @@ -107,6 +114,12 @@ public class AstNodeCache {
public final MethodNode SpecificationContext_GetSharedInstance =
SpecificationContext.getDeclaredMethods(org.spockframework.runtime.SpecificationContext.GET_SHARED_INSTANCE).get(0);

public final MethodNode SpecificationContext_GetBlockCurrentBlock =
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
public final MethodNode SpecificationContext_GetBlockCurrentBlock =
public final MethodNode SpecificationContext_GetCurrentBlock =

SpecificationContext.getDeclaredMethods(org.spockframework.runtime.SpecificationContext.GET_CURRENT_BLOCK).get(0);

public final MethodNode SpecificationContext_SetBlockCurrentBlock =
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
public final MethodNode SpecificationContext_SetBlockCurrentBlock =
public final MethodNode SpecificationContext_SetCurrentBlock =

SpecificationContext.getDeclaredMethods(org.spockframework.runtime.SpecificationContext.SET_CURRENT_BLOCK).get(0);

public final MethodNode List_Get =
ClassHelper.LIST_TYPE.getDeclaredMethods("get").get(0);

Expand Down
15 changes: 15 additions & 0 deletions spock-core/src/main/java/org/spockframework/compiler/AstUtil.java
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
package org.spockframework.compiler;

import org.codehaus.groovy.syntax.Token;
import org.codehaus.groovy.syntax.Types;
import org.spockframework.lang.Wildcard;
import org.spockframework.util.*;
import spock.lang.Specification;
Expand Down Expand Up @@ -390,4 +391,18 @@
public static ConstantExpression primitiveConstExpression(boolean value) {
return new ConstantExpression(value, true);
}

public static BinaryExpression createVariableIsNotNullExpression(VariableExpression var) {
return new BinaryExpression(
new VariableExpression(var),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need to create a new VariableExpression from the supplied one?

Token.newSymbol(Types.COMPARE_NOT_EQUAL, -1, -1),
new ConstantExpression(null));
}

public static BinaryExpression createVariableIsNullExpression(VariableExpression var) {
leonard84 marked this conversation as resolved.
Show resolved Hide resolved
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it intended that this unused method is added for some future use or is it a left-over from being used at some point?

return new BinaryExpression(

Check warning on line 403 in spock-core/src/main/java/org/spockframework/compiler/AstUtil.java

View check run for this annotation

Codecov / codecov/patch

spock-core/src/main/java/org/spockframework/compiler/AstUtil.java#L403

Added line #L403 was not covered by tests
new VariableExpression(var),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need to create a new VariableExpression from the supplied one?

Token.newSymbol(Types.COMPARE_EQUAL, -1, -1),

Check warning on line 405 in spock-core/src/main/java/org/spockframework/compiler/AstUtil.java

View check run for this annotation

Codecov / codecov/patch

spock-core/src/main/java/org/spockframework/compiler/AstUtil.java#L405

Added line #L405 was not covered by tests
new ConstantExpression(null));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@

import org.codehaus.groovy.ast.*;
import org.codehaus.groovy.ast.expr.*;
import org.spockframework.util.Assert;

import static java.util.stream.Collectors.*;
import static org.spockframework.compiler.AstUtil.*;
Expand Down Expand Up @@ -190,8 +191,9 @@
ann.setMember(FeatureMetadata.BLOCKS, blockAnnElems = new ListExpression());

ListExpression paramNames = new ListExpression();
for (Parameter param : feature.getAst().getParameters())
for (Parameter param : feature.getAst().getParameters()) {
paramNames.addExpression(new ConstantExpression(param.getName()));
}
ann.setMember(FeatureMetadata.PARAMETER_NAMES, paramNames);

feature.getAst().addAnnotation(ann);
Expand All @@ -202,9 +204,13 @@
blockAnn.setMember(BlockMetadata.KIND, new PropertyExpression(
new ClassExpression(nodeCache.BlockKind), kind.name()));
ListExpression textExprs = new ListExpression();
for (String text : block.getDescriptions())
for (String text : block.getDescriptions()) {
textExprs.addExpression(new ConstantExpression(text));
}
blockAnn.setMember(BlockMetadata.TEXTS, textExprs);
int index = blockAnnElems.getExpressions().size();
Assert.that(index == block.getBlockMetaDataIndex(),
() -> kind + " block mismatch of index: " + index + ", block.getBlockMetaDataIndex(): " + block.getBlockMetaDataIndex());

Check warning on line 213 in spock-core/src/main/java/org/spockframework/compiler/SpecAnnotator.java

View check run for this annotation

Codecov / codecov/patch

spock-core/src/main/java/org/spockframework/compiler/SpecAnnotator.java#L213

Added line #L213 was not covered by tests
blockAnnElems.addExpression(new AnnotationConstantExpression(blockAnn));
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,12 @@ private void buildBlocks(Method method) throws InvalidSpecCompileException {
checkIsValidSuccessor(method, BlockParseInfo.METHOD_END,
method.getAst().getLastLineNumber(), method.getAst().getLastColumnNumber());

// set the block metaData index for each block this must be equal to the index of the block in the @BlockMetadata annotation
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// set the block metaData index for each block this must be equal to the index of the block in the @BlockMetadata annotation
// set the block metadata index for each block this must be equal to the index of the block in the @BlockMetadata annotation

int i = -1;
for (Block block : method.getBlocks()) {
if(!block.hasBlockMetadata()) continue;
block.setBlockMetaDataIndex(++i);
}
// now that statements have been copied to blocks, the original statement
// list is cleared; statements will be copied back after rewriting is done
stats.clear();
Expand Down
126 changes: 96 additions & 30 deletions spock-core/src/main/java/org/spockframework/compiler/SpecRewriter.java
Original file line number Diff line number Diff line change
Expand Up @@ -16,19 +16,24 @@

package org.spockframework.compiler;

import org.spockframework.compiler.model.*;
import org.spockframework.runtime.SpockException;
import org.spockframework.util.*;

import java.lang.reflect.InvocationTargetException;
import java.util.*;

import org.codehaus.groovy.ast.*;
import org.codehaus.groovy.ast.expr.*;
import org.codehaus.groovy.ast.stmt.*;
import org.codehaus.groovy.runtime.MetaClassHelper;
import org.codehaus.groovy.syntax.*;
import org.codehaus.groovy.syntax.Token;
import org.codehaus.groovy.syntax.Types;
import org.jetbrains.annotations.NotNull;
import org.objectweb.asm.Opcodes;
import org.spockframework.compiler.model.*;
import org.spockframework.runtime.SpockException;
import org.spockframework.util.InternalIdentifiers;
import org.spockframework.util.ObjectUtil;
import org.spockframework.util.ReflectionUtil;

import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

import static java.util.Arrays.asList;
import static java.util.Collections.singletonList;
Expand Down Expand Up @@ -159,7 +164,7 @@ private void createFinalFieldGetter(Field field) {

private void createSharedFieldSetter(Field field) {
String setterName = "set" + MetaClassHelper.capitalize(field.getName());
Parameter[] params = new Parameter[] { new Parameter(field.getAst().getType(), "$spock_value") };
Parameter[] params = new Parameter[] { new Parameter(field.getAst().getType(), SpockNames.SPOCK_VALUE) };
MethodNode setter = spec.getAst().getMethod(setterName, params);
if (setter != null) {
errorReporter.error(field.getAst(),
Expand All @@ -180,7 +185,7 @@ private void createSharedFieldSetter(Field field) {
// use internal name
new ConstantExpression(field.getAst().getName())),
Token.newSymbol(Types.ASSIGN, -1, -1),
new VariableExpression("$spock_value"))));
new VariableExpression(SpockNames.SPOCK_VALUE))));

setter.setSourcePosition(field.getAst());
spec.getAst().addMethod(setter);
Expand Down Expand Up @@ -390,13 +395,20 @@ private void handleWhereBlock(Method method) {
public void visitMethodAgain(Method method) {
this.block = null;

if (!movedStatsBackToMethod)
for (Block b : method.getBlocks())
if (!movedStatsBackToMethod) {
for (Block b : method.getBlocks()) {
// This will only run if there was no 'cleanup' block in the method.
// Otherwise, the blocks have already been copied to try block by visitCleanupBlock.
// We need to run as late as possible, so we'll have to do the handling here and in visitCleanupBlock.
addBlockListeners(b);
method.getStatements().addAll(b.getAst());
}
leonard84 marked this conversation as resolved.
Show resolved Hide resolved
}

// for global required interactions
if (method instanceof FeatureMethod)
if (method instanceof FeatureMethod) {
method.getStatements().add(createMockControllerCall(nodeCache.MockController_LeaveScope));
}

if (methodHasCondition) {
defineValueRecorder(method.getStatements(), "");
Expand All @@ -406,6 +418,63 @@ public void visitMethodAgain(Method method) {
}
}


private void addBlockListeners(Block block) {
BlockParseInfo blockType = block.getParseInfo();
if (blockType == BlockParseInfo.WHERE
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is FILTER missing here?

|| blockType == BlockParseInfo.METHOD_END
|| blockType == BlockParseInfo.COMBINED
|| blockType == BlockParseInfo.ANONYMOUS) return;
leonard84 marked this conversation as resolved.
Show resolved Hide resolved

// SpockRuntime.enterBlock(getSpecificationContext(), new BlockInfo(blockKind, blockMetaDataIndex))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// SpockRuntime.enterBlock(getSpecificationContext(), new BlockInfo(blockKind, blockMetaDataIndex))
// SpockRuntime.callEnterBlock(getSpecificationContext(), blockMetadataIndex)

or maybe callBlockEntered?

MethodCallExpression enterBlockCall = createBlockListenerCall(block, blockType, nodeCache.SpockRuntime_CallEnterBlock);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

callEnterBlockCall or callBlockEnteredCall, depending on how the method in the end will be called

// SpockRuntime.exitedBlock(getSpecificationContext(), new BlockInfo(blockKind, blockMetaDataIndex))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// SpockRuntime.exitedBlock(getSpecificationContext(), new BlockInfo(blockKind, blockMetaDataIndex))
// SpockRuntime.callExitedBlock(getSpecificationContext(), blockMetadataIndex)

or maybe callBlockExited?

MethodCallExpression exitBlockCall = createBlockListenerCall(block, blockType, nodeCache.SpockRuntime_CallExitBlock);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

callExitBlockCall or callBlockExitedCall, depending on how the method in the end will be called


if (blockType == BlockParseInfo.CLEANUP) {
// In case of a cleanup block we need store a reference of the previously `currentBlock` in case that an exception occurred
// and restore it at the end of the cleanup block, so that the correct `BlockInfo` is available for the `IErrorContext`.
// The restoration happens in the `finally` statement created by `createCleanupTryCatch`.
VariableExpression failedBlock = new VariableExpression(SpockNames.FAILED_BLOCK, nodeCache.BlockInfo);
block.getAst().addAll(0, asList(
ifThrowableIsNotNull(storeFailedBlock(failedBlock)),
new ExpressionStatement(enterBlockCall)
));
} else {
block.getAst().add(0, new ExpressionStatement(enterBlockCall));
}
Comment on lines +434 to +445
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if (blockType == BlockParseInfo.CLEANUP) {
// In case of a cleanup block we need store a reference of the previously `currentBlock` in case that an exception occurred
// and restore it at the end of the cleanup block, so that the correct `BlockInfo` is available for the `IErrorContext`.
// The restoration happens in the `finally` statement created by `createCleanupTryCatch`.
VariableExpression failedBlock = new VariableExpression(SpockNames.FAILED_BLOCK, nodeCache.BlockInfo);
block.getAst().addAll(0, asList(
ifThrowableIsNotNull(storeFailedBlock(failedBlock)),
new ExpressionStatement(enterBlockCall)
));
} else {
block.getAst().add(0, new ExpressionStatement(enterBlockCall));
}
block.getAst().add(0, new ExpressionStatement(enterBlockCall));
if (blockType == BlockParseInfo.CLEANUP) {
// In case of a cleanup block we need store a reference of the previously `currentBlock` in case that an exception occurred
// and restore it at the end of the cleanup block, so that the correct `BlockInfo` is available for the `IErrorContext`.
// The restoration happens in the `finally` statement created by `createCleanupTryCatch`.
VariableExpression failedBlock = new VariableExpression(SpockNames.FAILED_BLOCK, nodeCache.BlockInfo);
block.getAst().add(0, ifThrowableIsNotNull(storeFailedBlock(failedBlock)));
}

block.getAst().add(new ExpressionStatement(exitBlockCall));
}

private @NotNull Statement storeFailedBlock(VariableExpression failedBlock) {
MethodCallExpression getCurrentBlock = createDirectMethodCall(getSpecificationContext(), nodeCache.SpecificationContext_GetBlockCurrentBlock, ArgumentListExpression.EMPTY_ARGUMENTS);
return new ExpressionStatement(new BinaryExpression(failedBlock, Token.newSymbol(Types.ASSIGN, -1, -1), getCurrentBlock));
}

private @NotNull Statement restoreFailedBlock(VariableExpression failedBlock) {
return new ExpressionStatement(createDirectMethodCall(new CastExpression(nodeCache.SpecificationContext, getSpecificationContext()), nodeCache.SpecificationContext_SetBlockCurrentBlock, new ArgumentListExpression(failedBlock)));
}

private IfStatement ifThrowableIsNotNull(Statement statement) {
return new IfStatement(
// if ($spock_feature_throwable != null)
new BooleanExpression(AstUtil.createVariableIsNotNullExpression(new VariableExpression(SpockNames.SPOCK_FEATURE_THROWABLE, nodeCache.Throwable))),
statement,
EmptyStatement.INSTANCE
);
}

private MethodCallExpression createBlockListenerCall(Block block, BlockParseInfo blockType, MethodNode blockListenerMethod) {
if (block.getBlockMetaDataIndex() < 0) throw new SpockException("Block metadata index not set: " + block);
return createDirectMethodCall(
new ClassExpression(nodeCache.SpockRuntime),
blockListenerMethod,
new ArgumentListExpression(
getSpecificationContext(),
new ConstantExpression(block.getBlockMetaDataIndex(), true)
));
}

@Override
public void visitAnyBlock(Block block) {
this.block = block;
Expand Down Expand Up @@ -484,12 +553,15 @@ private Statement createMockControllerCall(MethodNode method) {
@Override
public void visitCleanupBlock(CleanupBlock block) {
for (Block b : method.getBlocks()) {
// call addBlockListeners() here, as this method will already copy the contents of the blocks,
// so we need to transform the block listeners here as they won't be copied in visitMethodAgain where we normally add them
addBlockListeners(b);
if (b == block) break;
moveVariableDeclarations(b.getAst(), method.getStatements());
}

VariableExpression featureThrowableVar =
new VariableExpression("$spock_feature_throwable", nodeCache.Throwable);
new VariableExpression(SpockNames.SPOCK_FEATURE_THROWABLE, nodeCache.Throwable);
method.getStatements().add(createVariableDeclarationStatement(featureThrowableVar));

List<Statement> featureStats = new ArrayList<>();
Expand All @@ -499,9 +571,10 @@ public void visitCleanupBlock(CleanupBlock block) {
}

CatchStatement featureCatchStat = createThrowableAssignmentAndRethrowCatchStatement(featureThrowableVar);

List<Statement> cleanupStats = singletonList(
createCleanupTryCatch(block, featureThrowableVar));
VariableExpression failedBlock = new VariableExpression(SpockNames.FAILED_BLOCK, nodeCache.BlockInfo);
List<Statement> cleanupStats = asList(
new ExpressionStatement(new DeclarationExpression(failedBlock, Token.newSymbol(Types.ASSIGN, -1, -1), ConstantExpression.NULL)),
createCleanupTryCatch(block, featureThrowableVar, failedBlock));

TryCatchStatement tryFinally =
new TryCatchStatement(
Expand All @@ -517,13 +590,6 @@ public void visitCleanupBlock(CleanupBlock block) {
movedStatsBackToMethod = true;
}

private BinaryExpression createVariableNotNullExpression(VariableExpression var) {
return new BinaryExpression(
new VariableExpression(var),
Token.newSymbol(Types.COMPARE_NOT_EQUAL, -1, -1),
new ConstantExpression(null));
}

private Statement createVariableDeclarationStatement(VariableExpression var) {
DeclarationExpression throwableDecl =
new DeclarationExpression(
Expand All @@ -534,21 +600,21 @@ private Statement createVariableDeclarationStatement(VariableExpression var) {
return new ExpressionStatement(throwableDecl);
}

private TryCatchStatement createCleanupTryCatch(CleanupBlock block, VariableExpression featureThrowableVar) {
private TryCatchStatement createCleanupTryCatch(CleanupBlock block, VariableExpression featureThrowableVar, VariableExpression failedBlock) {
List<Statement> cleanupStats = new ArrayList<>(block.getAst());

TryCatchStatement tryCatchStat =
new TryCatchStatement(
new BlockStatement(cleanupStats, null),
EmptyStatement.INSTANCE);
ifThrowableIsNotNull(restoreFailedBlock(failedBlock))
);

tryCatchStat.addCatch(createHandleSuppressedThrowableStatement(featureThrowableVar));

return tryCatchStat;
}

private CatchStatement createThrowableAssignmentAndRethrowCatchStatement(VariableExpression assignmentVar) {
Parameter catchParameter = new Parameter(nodeCache.Throwable, "$spock_tmp_throwable");
Parameter catchParameter = new Parameter(nodeCache.Throwable, SpockNames.SPOCK_TMP_THROWABLE);

BinaryExpression assignThrowableExpr =
new BinaryExpression(
Expand All @@ -565,9 +631,9 @@ private CatchStatement createThrowableAssignmentAndRethrowCatchStatement(Variabl
}

private CatchStatement createHandleSuppressedThrowableStatement(VariableExpression featureThrowableVar) {
Parameter catchParameter = new Parameter(nodeCache.Throwable, "$spock_tmp_throwable");
Parameter catchParameter = new Parameter(nodeCache.Throwable, SpockNames.SPOCK_TMP_THROWABLE);

BinaryExpression featureThrowableNotNullExpr = createVariableNotNullExpression(featureThrowableVar);
BinaryExpression featureThrowableNotNullExpr = AstUtil.createVariableIsNotNullExpression(featureThrowableVar);

List<Statement> addSuppressedStats =
singletonList(new ExpressionStatement(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
package org.spockframework.compiler;

public class SpockNames {
public static final String VALUE_RECORDER = "$spock_valueRecorder";
public static final String ERROR_COLLECTOR = "$spock_errorCollector";
public static final String FAILED_BLOCK = "$spock_failedBlock";
public static final String OLD_VALUE = "$spock_oldValue";
public static final String SPOCK_EX = "$spock_ex";
public static final String SPOCK_FEATURE_THROWABLE = "$spock_feature_throwable";
public static final String SPOCK_TMP_THROWABLE = "$spock_tmp_throwable";
public static final String SPOCK_VALUE = "$spock_value";
public static final String VALUE_RECORDER = "$spock_valueRecorder";
}
Original file line number Diff line number Diff line change
Expand Up @@ -37,4 +37,9 @@ public void accept(ISpecVisitor visitor) throws Exception {
public BlockParseInfo getParseInfo() {
return BlockParseInfo.ANONYMOUS;
}

@Override
public boolean hasBlockMetadata() {
return false;
}
}
Loading
Loading