[OLINGO-206] TDD refactoring
This commit is contained in:
parent
d3d411118e
commit
99d5781940
|
@ -115,4 +115,11 @@ public interface UriInfoResource {
|
|||
*/
|
||||
List<UriResource> getUriResourceParts();
|
||||
|
||||
/**
|
||||
* Give the last part of a resource path.
|
||||
*
|
||||
* @return An uri resource object.
|
||||
*/
|
||||
UriResource getUriResourceLastPart();
|
||||
|
||||
}
|
||||
|
|
|
@ -286,4 +286,9 @@ public class UriInfoImpl implements UriInfo {
|
|||
public Collection<SystemQueryOption> getSystemQueryOptions() {
|
||||
return Collections.unmodifiableCollection(systemQueryOptions.values());
|
||||
}
|
||||
|
||||
@Override
|
||||
public UriResource getUriResourceLastPart() {
|
||||
return lastResourcePart;
|
||||
}
|
||||
}
|
|
@ -99,6 +99,7 @@ public class Parser {
|
|||
(BatchEOFContext) parseRule(uri.pathSegmentListDecoded.get(0), ParserEntryRules.Batch);
|
||||
|
||||
uriParseTreeVisitor.visitBatchEOF(ctxBatchEOF);
|
||||
readQueryParameter = true;
|
||||
} else if (firstSegment.startsWith("$metadata")) {
|
||||
MetadataEOFContext ctxMetadataEOF =
|
||||
(MetadataEOFContext) parseRule(uri.pathSegmentListDecoded.get(0), ParserEntryRules.Metadata);
|
||||
|
@ -207,7 +208,7 @@ public class Parser {
|
|||
|
||||
context.contextUriInfo.setSystemQueryOption(filterOption);
|
||||
} else if (option.name.equals("$search")) {
|
||||
// TODO $search is not supported yet
|
||||
throw new RuntimeException("System query option '$search' not implemented!");
|
||||
} else if (option.name.equals("$select")) {
|
||||
SelectEOFContext ctxSelectEOF =
|
||||
(SelectEOFContext) parseRule(option.value, ParserEntryRules.Select);
|
||||
|
|
|
@ -18,9 +18,12 @@
|
|||
*/
|
||||
package org.apache.olingo.server.core.uri.validator;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import org.apache.olingo.commons.api.ODataRuntimeException;
|
||||
import org.apache.olingo.commons.api.edm.Edm;
|
||||
import org.apache.olingo.server.api.uri.UriInfo;
|
||||
import org.apache.olingo.server.api.uri.UriResource;
|
||||
import org.apache.olingo.server.api.uri.queryoption.SystemQueryOption;
|
||||
import org.apache.olingo.server.api.uri.queryoption.SystemQueryOptionKind;
|
||||
|
||||
|
@ -39,7 +42,9 @@ public class SystemQueryValidator {
|
|||
/* resource 5 */ { false, true , false, false, false, false, false, false, false, false, false, false },
|
||||
/* service 6 */ { false, true , false, false, false, false, false, false, false, false, false, false },
|
||||
/* entitySet 7 */ { true , true , true , false, true , true , true , true , true , true , true , true },
|
||||
|
||||
/* entitySetCount 8 */ { false, false, false, false, false, false, false, false, false, false, false, false },
|
||||
|
||||
/* entity 9 */ { false, true , true , false, false, false, false, true , false, false, true , false },
|
||||
/* mediaStream 10 */ { false, true , false, false, false, false, false, false, false, false, false, false },
|
||||
/* references 11 */ { true , true , false, false, false, true , true , false, true , true , false, true },
|
||||
|
@ -57,7 +62,7 @@ public class SystemQueryValidator {
|
|||
|
||||
public void validate(final UriInfo uriInfo, final Edm edm) throws UriValidationException {
|
||||
|
||||
validateQueryOptions(uriInfo);
|
||||
validateQueryOptions(uriInfo, edm);
|
||||
validateKeyPredicateTypes(uriInfo, edm);
|
||||
|
||||
}
|
||||
|
@ -108,7 +113,7 @@ public class SystemQueryValidator {
|
|||
return idx;
|
||||
}
|
||||
|
||||
private int rowIndex(final UriInfo uriInfo) {
|
||||
private int rowIndex(final UriInfo uriInfo, Edm edm) throws UriValidationException {
|
||||
int idx;
|
||||
|
||||
switch (uriInfo.getKind()) {
|
||||
|
@ -128,7 +133,7 @@ public class SystemQueryValidator {
|
|||
idx = 4;
|
||||
break;
|
||||
case resource:
|
||||
idx = 5;
|
||||
idx = rowIndexForResourceKind(uriInfo, edm);
|
||||
break;
|
||||
case service:
|
||||
idx = 6;
|
||||
|
@ -140,27 +145,77 @@ public class SystemQueryValidator {
|
|||
return idx;
|
||||
}
|
||||
|
||||
private void validateQueryOptions(final UriInfo uriInfo) throws UriValidationException {
|
||||
try {
|
||||
int row = rowIndex(uriInfo);
|
||||
private int rowIndexForResourceKind(UriInfo uriInfo, Edm edm) throws UriValidationException {
|
||||
int idx = 5;
|
||||
|
||||
for (SystemQueryOption option : uriInfo.getSystemQueryOptions()) {
|
||||
int col = colIndex(option.getKind());
|
||||
UriResource lastPathSegemnt = uriInfo.getUriResourceLastPart();
|
||||
|
||||
System.out.print("[" + row +"][" + col +"]");
|
||||
|
||||
|
||||
if (!decisionMatrix[row][col]) {
|
||||
throw new UriValidationException("System query option not allowed: " + option.getName());
|
||||
switch (lastPathSegemnt.getKind()) {
|
||||
case count:
|
||||
List<UriResource> parts = uriInfo.getUriResourceParts();
|
||||
UriResource secondLastPart = parts.get(parts.size() - 2);
|
||||
switch (secondLastPart.getKind()) {
|
||||
case entitySet:
|
||||
idx = 8;
|
||||
break;
|
||||
default : throw new UriValidationException("Illegal path part kind: " + lastPathSegemnt.getKind());
|
||||
}
|
||||
break;
|
||||
case action:
|
||||
break;
|
||||
case complexProperty:
|
||||
break;
|
||||
case entitySet:
|
||||
idx = 7;
|
||||
break;
|
||||
case function:
|
||||
break;
|
||||
case it:
|
||||
break;
|
||||
case lambdaAll:
|
||||
break;
|
||||
case lambdaAny:
|
||||
break;
|
||||
case lambdaVariable:
|
||||
break;
|
||||
case navigationProperty:
|
||||
break;
|
||||
case primitiveProperty:
|
||||
break;
|
||||
case ref:
|
||||
break;
|
||||
case root:
|
||||
break;
|
||||
case singleton:
|
||||
break;
|
||||
case value:
|
||||
break;
|
||||
default:
|
||||
throw new ODataRuntimeException("Unsupported uriResource kind: " + lastPathSegemnt.getKind());
|
||||
}
|
||||
}finally {
|
||||
|
||||
return idx;
|
||||
}
|
||||
|
||||
private void validateQueryOptions(final UriInfo uriInfo, Edm edm) throws UriValidationException {
|
||||
try {
|
||||
int row = rowIndex(uriInfo, edm);
|
||||
|
||||
for (SystemQueryOption option : uriInfo.getSystemQueryOptions()) {
|
||||
int col = colIndex(option.getKind());
|
||||
|
||||
System.out.print("[" + row + "][" + col + "]");
|
||||
|
||||
if (!decisionMatrix[row][col]) {
|
||||
throw new UriValidationException("System query option not allowed: " + option.getName());
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
System.out.println();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private void validateKeyPredicateTypes(final UriInfo uriInfo, final Edm edm) throws UriValidationException {
|
||||
}
|
||||
private void validateKeyPredicateTypes(final UriInfo uriInfo, final Edm edm) throws UriValidationException {}
|
||||
|
||||
}
|
||||
|
|
|
@ -21,6 +21,8 @@ package org.apache.olingo.server.core.uri.validator;
|
|||
import static org.junit.Assert.assertTrue;
|
||||
import static org.junit.Assert.fail;
|
||||
|
||||
import java.util.ArrayList;
|
||||
|
||||
import org.apache.olingo.commons.api.edm.Edm;
|
||||
import org.apache.olingo.server.api.uri.UriInfo;
|
||||
import org.apache.olingo.server.core.edm.provider.EdmProviderImpl;
|
||||
|
@ -32,8 +34,173 @@ import org.junit.Test;
|
|||
|
||||
public class UriEdmValidatorTest {
|
||||
|
||||
private static final String URI_ALL = "$all";
|
||||
private static final String URI_BATCH = "$batch";
|
||||
private static final String URI_CROSSJOIN = "$crossjoin(ESAllPrim)";
|
||||
private static final String URI_ENTITY_ID = "/$entity";
|
||||
private static final String URI_METADATA = "$metadata";
|
||||
private static final String URI_SERVICE = "";
|
||||
private static final String URI_ENTITY_SET = "/ESAllPrim";
|
||||
private static final String URI_ENTITY_SET_COUNT = "/ESAllPrim/$count";
|
||||
private static final String URI_ENTITY = "/ESAllPrim(1)";
|
||||
private static final String URI_MEDIA_STREAM = "/ESMedia(1)/$value";
|
||||
private static final String URI_REFERENCES = "/ESAllPrim/$ref";
|
||||
private static final String URI_REFERENECE = "/ESAllPrim(1)/$ref";
|
||||
private static final String URI_PROPERTY_COMPLEX = "/ESCompComp(1)/PropertyComplex";
|
||||
private static final String URI_PROPERTY_COMPLEX_COLLECTION =
|
||||
"/ESCompCollComp(1)/PropertyComplex/CollPropertyComplex";
|
||||
private static final String URI_PROPERTY_COMPLEX_COLLECTION_COUNT =
|
||||
"/ESCompCollComp(1)/PropertyComplex/CollPropertyComplex/$count";
|
||||
private static final String URI_PROPERTY_PRIMITIVE = "/ESAllPrim(1)/PropertyString";
|
||||
private static final String URI_PROPERTY_PRIMITIVE_COLLECTION = "/ESCollAllPrim/CollPropertyString";
|
||||
private static final String URI_PROPERTY_PRIMITIVE_COLLECTION_COUNT =
|
||||
"/ESCollAllPrim/CollPropertyString/$count";
|
||||
private static final String URI_PROPERTY_PRIMITIVE_VALUE = "/ESAllPrim(1)/PropertyString/$value";
|
||||
|
||||
private static final String QO_FILTER = "$filter='1' eq '1'";
|
||||
private static final String QO_FORMAT = "$format=bla";
|
||||
private static final String QO_EXPAND = "$expand=*";
|
||||
private static final String QO_ID = "$id=Products(0)";
|
||||
private static final String QO_COUNT = "$count";
|
||||
// private static final String QO_ORDERBY = "$orderby=bla asc";
|
||||
// private static final String QO_SEARCH = "$search='bla'";
|
||||
private static final String QO_SELECT = "$select=*";
|
||||
private static final String QO_SKIP = "$skip=3";
|
||||
private static final String QO_SKIPTOKEN = "$skiptoken=123";
|
||||
private static final String QO_LEVELS = "$expand=*($levels=1)";
|
||||
private static final String QO_TOP = "$top=1";
|
||||
|
||||
private Edm edm = new EdmProviderImpl(new EdmTechProvider());
|
||||
|
||||
private String[][] urisWithValidSystemQueryOptions = {
|
||||
{ URI_ALL, QO_FILTER, }, { URI_ALL, QO_FORMAT }, { URI_ALL, QO_EXPAND }, { URI_ALL, QO_COUNT },
|
||||
/* { URI_ALL, QO_ORDERBY }, *//* { URI_ALL, QO_SEARCH }, */{ URI_ALL, QO_SELECT }, { URI_ALL, QO_SKIP },
|
||||
{ URI_ALL, QO_SKIPTOKEN }, { URI_ALL, QO_LEVELS },
|
||||
|
||||
{ URI_CROSSJOIN, QO_FILTER, }, { URI_CROSSJOIN, QO_FORMAT },
|
||||
{ URI_CROSSJOIN, QO_EXPAND }, { URI_CROSSJOIN, QO_COUNT }, /* { URI_CROSSJOIN, QO_ORDERBY }, */
|
||||
/* { URI_CROSSJOIN, QO_SEARCH }, */{ URI_CROSSJOIN, QO_SELECT }, { URI_CROSSJOIN, QO_SKIP },
|
||||
{ URI_CROSSJOIN, QO_SKIPTOKEN }, { URI_CROSSJOIN, QO_LEVELS }, { URI_CROSSJOIN, QO_TOP },
|
||||
|
||||
{ URI_ENTITY_ID, QO_ID, QO_FORMAT }, { URI_ENTITY_ID, QO_ID, }, { URI_ENTITY_ID, QO_ID, QO_EXPAND },
|
||||
{ URI_ENTITY_ID, QO_ID, QO_SELECT }, { URI_ENTITY_ID, QO_ID, QO_LEVELS },
|
||||
|
||||
{ URI_METADATA, QO_FORMAT },
|
||||
|
||||
{ URI_SERVICE, QO_FORMAT },
|
||||
|
||||
{ URI_ENTITY_SET, QO_FILTER, }, { URI_ENTITY_SET, QO_FORMAT }, { URI_ENTITY_SET, QO_EXPAND },
|
||||
{ URI_ENTITY_SET, QO_COUNT }, /* { URI_ENTITY_SET, QO_ORDERBY }, *//* { URI_ENTITY_SET, QO_SEARCH }, */
|
||||
{ URI_ENTITY_SET, QO_SELECT },
|
||||
{ URI_ENTITY_SET, QO_SKIP }, { URI_ENTITY_SET, QO_SKIPTOKEN }, { URI_ENTITY_SET, QO_LEVELS },
|
||||
{ URI_ENTITY_SET, QO_TOP },
|
||||
|
||||
};
|
||||
|
||||
private String[][] urisWithNonValidSystemQueryOptions = {
|
||||
{ URI_ALL, QO_ID, }, { URI_ALL, QO_TOP },
|
||||
|
||||
{ URI_BATCH, QO_FILTER, }, { URI_BATCH, QO_FORMAT }, { URI_BATCH, QO_ID, }, { URI_BATCH, QO_EXPAND },
|
||||
{ URI_BATCH, QO_COUNT }, /* { URI_BATCH, QO_ORDERBY }, *//* { URI_BATCH, QO_SEARCH }, */{ URI_BATCH, QO_SELECT },
|
||||
{ URI_BATCH, QO_SKIP }, { URI_BATCH, QO_SKIPTOKEN }, { URI_BATCH, QO_LEVELS }, { URI_BATCH, QO_TOP },
|
||||
|
||||
{ URI_CROSSJOIN, QO_ID, },
|
||||
|
||||
{ URI_ENTITY_ID, QO_ID, QO_FILTER, },
|
||||
{ URI_ENTITY_ID, QO_ID, QO_COUNT }, /* { URI_ENTITY_ID, QO_ORDERBY }, *//* { URI_ENTITY_ID, QO_SEARCH }, */
|
||||
|
||||
{ URI_ENTITY_ID, QO_ID, QO_SKIP }, { URI_ENTITY_ID, QO_ID, QO_SKIPTOKEN }, { URI_ENTITY_ID, QO_ID, QO_TOP },
|
||||
|
||||
{ URI_METADATA, QO_FILTER, }, { URI_METADATA, QO_ID, }, { URI_METADATA, QO_EXPAND },
|
||||
{ URI_METADATA, QO_COUNT }, /* { URI_METADATA, QO_ORDERBY }, *//* { URI_METADATA, QO_SEARCH }, */
|
||||
{ URI_METADATA, QO_SELECT }, { URI_METADATA, QO_SKIP }, { URI_METADATA, QO_SKIPTOKEN },
|
||||
{ URI_METADATA, QO_LEVELS }, { URI_METADATA, QO_TOP },
|
||||
|
||||
{ URI_SERVICE, QO_FILTER }, { URI_SERVICE, QO_ID }, { URI_SERVICE, QO_EXPAND }, { URI_SERVICE, QO_COUNT },
|
||||
/* { URI_SERVICE, QO_ORDERBY }, *//* { URI_SERVICE, QO_SEARCH }, */{ URI_SERVICE, QO_SELECT },
|
||||
{ URI_SERVICE, QO_SKIP }, { URI_SERVICE, QO_SKIPTOKEN }, { URI_SERVICE, QO_LEVELS }, { URI_SERVICE, QO_TOP },
|
||||
|
||||
{ URI_ENTITY_SET, QO_ID },
|
||||
|
||||
{ URI_ENTITY_SET_COUNT, QO_FILTER }, { URI_ENTITY_SET_COUNT, QO_FORMAT }, { URI_ENTITY_SET_COUNT, QO_ID },
|
||||
{ URI_ENTITY_SET_COUNT, QO_EXPAND }, { URI_ENTITY_SET_COUNT, QO_COUNT },
|
||||
/* { URI_ENTITY_SET_COUNT, QO_ORDERBY }, *//* { URI_ENTITY_SET_COUNT, QO_SEARCH }, */
|
||||
{ URI_ENTITY_SET_COUNT, QO_SELECT }, { URI_ENTITY_SET_COUNT, QO_SKIP }, { URI_ENTITY_SET_COUNT, QO_SKIPTOKEN },
|
||||
{ URI_ENTITY_SET_COUNT, QO_LEVELS }, { URI_ENTITY_SET_COUNT, QO_TOP },
|
||||
};
|
||||
|
||||
@Test
|
||||
public void bla() throws Exception {
|
||||
String[][] m = { { URI_ENTITY_SET_COUNT, QO_FILTER } };
|
||||
String[] uris = constructUri(m);
|
||||
System.out.println(uris[0]);
|
||||
|
||||
parseAndValidate(uris[0]);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void checkValidSystemQueryOption() throws Exception {
|
||||
String[] uris = constructUri(urisWithValidSystemQueryOptions);
|
||||
|
||||
for (String uri : uris) {
|
||||
try {
|
||||
parseAndValidate(uri);
|
||||
} catch (Exception e) {
|
||||
throw new Exception("Faild for uri: " + uri, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void checkNonValidSystemQueryOption() throws Exception {
|
||||
String[] uris = constructUri(urisWithNonValidSystemQueryOptions);
|
||||
|
||||
for (String uri : uris) {
|
||||
try {
|
||||
parseAndValidate(uri);
|
||||
fail("Validation Exception not thrown: " + uri);
|
||||
} catch (UriValidationException e) {
|
||||
assertTrue(e instanceof UriValidationException);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@Ignore
|
||||
public void systemQueryOptionValid() throws Exception {
|
||||
String[] uris =
|
||||
{
|
||||
/* $filter */
|
||||
"/$all?$format=bla",
|
||||
// "/$batch?$format=bla",
|
||||
"/$crossjoin(ESAllPrim)?$format=bla",
|
||||
"/$entity?$id=Products(0)?$format=bla",
|
||||
"/$metadata?$format=bla",
|
||||
"?$format=bla",
|
||||
"/ESAllPrim?$format=bla",
|
||||
"/ESAllPrim/$count?$format=bla",
|
||||
"/ESAllPrim(1)?$format=bla",
|
||||
"/ESMedia(1)/$value?$format=bla",
|
||||
"/ESAllPrim/$ref?$format=bla",
|
||||
"/ESAllPrim(1)/$ref?$format=bla",
|
||||
"/ESCompComp(1)/PropertyComplex?$format=bla",
|
||||
"/ESCompCollComp(1)/PropertyComplex/CollPropertyComplex?$format=bla",
|
||||
"/ESCompCollComp(1)/PropertyComplex/CollPropertyComplex/$count?$format=bla",
|
||||
"/ESAllPrim(1)/PropertyString?$format=bla",
|
||||
"/ESCollAllPrim/CollPropertyString?$format=bla",
|
||||
"/ESCollAllPrim/CollPropertyString/$count?$format=bla",
|
||||
"/ESAllPrim(1)/PropertyString/$value?$format=bla"
|
||||
};
|
||||
|
||||
for (String uri : uris) {
|
||||
try {
|
||||
parseAndValidate(uri);
|
||||
} catch (Exception e) {
|
||||
throw new Exception("Faild for uri: " + uri, e);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
String[] tmpUri = {
|
||||
"$crossjoin(ESKeyNav, ESTwoKeyNav)/invalid ",
|
||||
|
@ -83,66 +250,29 @@ public class UriEdmValidatorTest {
|
|||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void systemQueryOptionValid() throws Exception {
|
||||
String[] uris =
|
||||
{
|
||||
/* $filter */
|
||||
"/$all?$format=bla",
|
||||
"/$batch?$format=bla",
|
||||
"/$crossjoin(ESAllPrim)?$format=bla",
|
||||
"/$entity?$id=Products(0)?$format=bla",
|
||||
"/$metadata?$format=bla",
|
||||
"?$format=bla",
|
||||
"/ESAllPrim?$format=bla",
|
||||
"/ESAllPrim/$count?$format=bla",
|
||||
"/ESAllPrim(1)?$format=bla" ,
|
||||
"/ESMedia(1)/$value?$format=bla",
|
||||
"/ESAllPrim/$ref?$format=bla",
|
||||
"/ESAllPrim(1)/$ref?$format=bla",
|
||||
"/ESCompComp(1)/PropertyComplex?$format=bla",
|
||||
"/ESCompCollComp(1)/PropertyComplex/CollPropertyComplex?$format=bla",
|
||||
"/ESCompCollComp(1)/PropertyComplex/CollPropertyComplex/$count?$format=bla",
|
||||
"/ESAllPrim(1)/PropertyString?$format=bla",
|
||||
"/ESCollAllPrim/CollPropertyString?$format=bla",
|
||||
"/ESCollAllPrim/CollPropertyString/$count?$format=bla",
|
||||
"/ESAllPrim(1)/PropertyString/$value?$format=bla"
|
||||
/* all */
|
||||
/* batch */
|
||||
/* crossjoin */
|
||||
/* entityId */
|
||||
/* metadata */
|
||||
/* resource */
|
||||
/* service */
|
||||
/* entitySet */
|
||||
/* entitySetCount */
|
||||
/* entity */
|
||||
/* mediaStream */
|
||||
/* references */
|
||||
/* reference */
|
||||
/* propertyComplex */
|
||||
/* propertyComplexCollection */
|
||||
/* propertyComplexCollectionCount */
|
||||
/* propertyPrimitive */
|
||||
/* propertyPrimitiveCollection */
|
||||
/* propertyPrimitiveCollectionCount */
|
||||
/* propertyPrimitiveValue */};
|
||||
|
||||
for (String uri : uris) {
|
||||
try {
|
||||
parseAndValidate(uri);
|
||||
} catch (Exception e) {
|
||||
throw new Exception("Faild for uri: " + uri, e);
|
||||
private String[] constructUri(String[][] uriParameterMatrix) {
|
||||
ArrayList<String> uris = new ArrayList<String>();
|
||||
for (String[] uriParameter : uriParameterMatrix) {
|
||||
String uri = uriParameter[0];
|
||||
if (uriParameter.length > 1) {
|
||||
uri += "?";
|
||||
}
|
||||
for (int i = 1; i < uriParameter.length; i++) {
|
||||
uri += uriParameter[i];
|
||||
if (i < (uriParameter.length - 1)) {
|
||||
uri += "&";
|
||||
}
|
||||
}
|
||||
uris.add(uri);
|
||||
}
|
||||
|
||||
return uris.toArray(new String[0]);
|
||||
}
|
||||
|
||||
@Test
|
||||
@Ignore
|
||||
public void systemQueryOptionInvalid() throws Exception {
|
||||
String[] uris =
|
||||
{
|
||||
{
|
||||
};
|
||||
|
||||
for (String uri : uris) {
|
||||
|
@ -160,7 +290,7 @@ public class UriEdmValidatorTest {
|
|||
UriInfo uriInfo = new Parser().parseUri(uri.trim(), edm);
|
||||
SystemQueryValidator validator = new SystemQueryValidator();
|
||||
|
||||
System.out.print("URI: " + uri );
|
||||
System.out.print("URI: " + uri);
|
||||
validator.validate(uriInfo, edm);
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue