ACS-4026: Create orphaned tag with POST /tags (#1748)

* ACS-4026: Create orphaned tag with POST /tags
This commit is contained in:
Krystian Dabrowski
2023-02-10 16:51:22 +01:00
committed by GitHub
parent 9457e019ef
commit 96c1464b6c
16 changed files with 933 additions and 73 deletions

View File

@@ -2,7 +2,7 @@
* #%L
* Alfresco Remote API
* %%
* Copyright (C) 2005 - 2016 Alfresco Software Limited
* Copyright (C) 2005 - 2023 Alfresco Software Limited
* %%
* This file is part of the Alfresco software.
* If the software was purchased under a paid Alfresco license, the terms of
@@ -25,22 +25,33 @@
*/
package org.alfresco.rest.api;
import static org.alfresco.service.cmr.repository.StoreRef.STORE_REF_WORKSPACE_SPACESSTORE;
import java.util.List;
import org.alfresco.rest.api.model.Tag;
import org.alfresco.rest.framework.resource.parameters.CollectionWithPagingInfo;
import org.alfresco.rest.framework.resource.parameters.Paging;
import org.alfresco.rest.framework.resource.parameters.Parameters;
import org.alfresco.service.Experimental;
import org.alfresco.service.cmr.repository.StoreRef;
public interface Tags
{
public List<Tag> addTags(String nodeId, List<Tag> tags);
public Tag getTag(StoreRef storeRef, String tagId);
public void deleteTag(String nodeId, String tagId);
public CollectionWithPagingInfo<Tag> getTags(StoreRef storeRef, Parameters params);
public Tag changeTag(StoreRef storeRef, String tagId, Tag tag);
public CollectionWithPagingInfo<Tag> getTags(String nodeId, Parameters params);
List<Tag> addTags(String nodeId, List<Tag> tags);
Tag getTag(StoreRef storeRef, String tagId);
void deleteTag(String nodeId, String tagId);
CollectionWithPagingInfo<Tag> getTags(StoreRef storeRef, Parameters params);
Tag changeTag(StoreRef storeRef, String tagId, Tag tag);
CollectionWithPagingInfo<Tag> getTags(String nodeId, Parameters params);
@Experimental
List<Tag> createTags(StoreRef storeRef, List<Tag> tags, Parameters parameters);
@Experimental
default List<Tag> createTags(List<Tag> tags, Parameters parameters)
{
return createTags(STORE_REF_WORKSPACE_SPACESSTORE, tags, parameters);
}
void deleteTagById(StoreRef storeRef, String tagId);
}

View File

@@ -2,7 +2,7 @@
* #%L
* Alfresco Remote API
* %%
* Copyright (C) 2005 - 2016 Alfresco Software Limited
* Copyright (C) 2005 - 2023 Alfresco Software Limited
* %%
* This file is part of the Alfresco software.
* If the software was purchased under a paid Alfresco license, the terms of
@@ -27,9 +27,13 @@ package org.alfresco.rest.api.impl;
import java.util.AbstractList;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.stream.Collectors;
import org.alfresco.query.PagingResults;
import org.alfresco.repo.tagging.NonExistentTagException;
@@ -47,12 +51,14 @@ import org.alfresco.rest.framework.core.exceptions.UnsupportedResourceOperationE
import org.alfresco.rest.framework.resource.parameters.CollectionWithPagingInfo;
import org.alfresco.rest.framework.resource.parameters.Paging;
import org.alfresco.rest.framework.resource.parameters.Parameters;
import org.alfresco.service.Experimental;
import org.alfresco.service.cmr.repository.NodeRef;
import org.alfresco.service.cmr.repository.StoreRef;
import org.alfresco.service.cmr.security.AuthorityService;
import org.alfresco.service.cmr.tagging.TaggingService;
import org.alfresco.util.Pair;
import org.alfresco.util.TypeConstraint;
import org.apache.commons.collections.CollectionUtils;
/**
* Centralises access to tag services and maps between representations.
@@ -63,13 +69,14 @@ import org.alfresco.util.TypeConstraint;
public class TagsImpl implements Tags
{
private static final Object PARAM_INCLUDE_COUNT = "count";
private Nodes nodes;
static final String NOT_A_VALID_TAG = "An invalid parameter has been supplied";
static final String NO_PERMISSION_TO_MANAGE_A_TAG = "Current user does not have permission to manage a tag";
private Nodes nodes;
private TaggingService taggingService;
private TypeConstraint typeConstraint;
private AuthorityService authorityService;
static final String NO_PERMISSION_TO_MANAGE_A_TAG = "Current user does not have permission to manage a tag";
public void setTypeConstraint(TypeConstraint typeConstraint)
{
this.typeConstraint = typeConstraint;
@@ -140,10 +147,7 @@ public class TagsImpl implements Tags
@Override
public void deleteTagById(StoreRef storeRef, String tagId) {
if (!authorityService.hasAdminAuthority())
{
throw new PermissionDeniedException(NO_PERMISSION_TO_MANAGE_A_TAG);
}
verifyAdminAuthority();
NodeRef tagNodeRef = validateTag(storeRef, tagId);
String tagValue = taggingService.getTagName(tagNodeRef);
@@ -253,4 +257,38 @@ public class TagsImpl implements Tags
return CollectionWithPagingInfo.asPaged(params.getPaging(), tags, results.hasMoreItems(), (totalItems == null ? null : totalItems.intValue()));
}
@Experimental
@Override
public List<Tag> createTags(final StoreRef storeRef, final List<Tag> tags, final Parameters parameters)
{
verifyAdminAuthority();
final List<String> tagNames = Optional.ofNullable(tags).orElse(Collections.emptyList()).stream()
.filter(Objects::nonNull)
.map(Tag::getTag)
.distinct()
.collect(Collectors.toList());
if (CollectionUtils.isEmpty(tagNames))
{
throw new InvalidArgumentException(NOT_A_VALID_TAG);
}
return taggingService.createTags(storeRef, tagNames).stream()
.map(pair -> Tag.builder().tag(pair.getFirst()).nodeRef(pair.getSecond()).create())
.peek(tag -> {
if (parameters.getInclude().contains(PARAM_INCLUDE_COUNT))
{
tag.setCount(0);
}
}).collect(Collectors.toList());
}
private void verifyAdminAuthority()
{
if (!authorityService.hasAdminAuthority())
{
throw new PermissionDeniedException(NO_PERMISSION_TO_MANAGE_A_TAG);
}
}
}

View File

@@ -2,7 +2,7 @@
* #%L
* Alfresco Remote API
* %%
* Copyright (C) 2005 - 2016 Alfresco Software Limited
* Copyright (C) 2005 - 2023 Alfresco Software Limited
* %%
* This file is part of the Alfresco software.
* If the software was purchased under a paid Alfresco license, the terms of
@@ -25,6 +25,8 @@
*/
package org.alfresco.rest.api.model;
import java.util.Objects;
import com.fasterxml.jackson.annotation.JsonProperty;
import org.alfresco.rest.framework.resource.UniqueId;
import org.alfresco.service.cmr.repository.NodeRef;
@@ -109,7 +111,7 @@ public class Tag implements Comparable<Tag>
/*
* Tags are equal if they have the same NodeRef
*
*
* (non-Javadoc)
* @see java.lang.Object#equals(java.lang.Object)
*/
@@ -134,7 +136,45 @@ public class Tag implements Comparable<Tag>
@Override
public String toString()
{
return "Tag [nodeRef=" + nodeRef + ", tag=" + tag + "]";
return "Tag{" + "nodeRef=" + nodeRef + ", tag='" + tag + '\'' + ", count=" + count + '}';
}
public static Builder builder()
{
return new Builder();
}
public static class Builder
{
private NodeRef nodeRef;
private String tag;
private Integer count;
public Builder nodeRef(NodeRef nodeRef)
{
this.nodeRef = nodeRef;
return this;
}
public Builder tag(String tag)
{
this.tag = tag;
return this;
}
public Builder count(Integer count)
{
this.count = count;
return this;
}
public Tag create()
{
final Tag tag = new Tag();
tag.setNodeRef(nodeRef);
tag.setTag(this.tag);
tag.setCount(count);
return tag;
}
}
}

View File

@@ -2,7 +2,7 @@
* #%L
* Alfresco Remote API
* %%
* Copyright (C) 2005 - 2016 Alfresco Software Limited
* Copyright (C) 2005 - 2023 Alfresco Software Limited
* %%
* This file is part of the Alfresco software.
* If the software was purchased under a paid Alfresco license, the terms of
@@ -25,6 +25,9 @@
*/
package org.alfresco.rest.api.tags;
import javax.servlet.http.HttpServletResponse;
import java.util.List;
import org.alfresco.rest.api.Tags;
import org.alfresco.rest.api.model.Tag;
import org.alfresco.rest.framework.WebApiDescription;
@@ -33,12 +36,14 @@ import org.alfresco.rest.framework.resource.EntityResource;
import org.alfresco.rest.framework.resource.actions.interfaces.EntityResourceAction;
import org.alfresco.rest.framework.resource.parameters.CollectionWithPagingInfo;
import org.alfresco.rest.framework.resource.parameters.Parameters;
import org.alfresco.service.Experimental;
import org.alfresco.service.cmr.repository.StoreRef;
import org.alfresco.util.ParameterCheck;
import org.springframework.beans.factory.InitializingBean;
@EntityResource(name="tags", title = "Tags")
public class TagsEntityResource implements EntityResourceAction.Read<Tag>, EntityResourceAction.ReadById<Tag>, EntityResourceAction.Update<Tag>, EntityResourceAction.Delete, InitializingBean
public class TagsEntityResource implements EntityResourceAction.Read<Tag>,
EntityResourceAction.ReadById<Tag>, EntityResourceAction.Update<Tag>, EntityResourceAction.Create<Tag>, EntityResourceAction.Delete, InitializingBean
{
private Tags tags;
@@ -78,6 +83,21 @@ public class TagsEntityResource implements EntityResourceAction.Read<Tag>, Entit
return tags.getTag(StoreRef.STORE_REF_WORKSPACE_SPACESSTORE, id);
}
/**
* POST /tags
*/
@Experimental
@WebApiDescription(
title = "Create an orphan tag",
description = "Creates a tag, which is not associated with any node",
successStatus = HttpServletResponse.SC_CREATED
)
@Override
public List<Tag> create(List<Tag> tags, Parameters parameters)
{
return this.tags.createTags(tags, parameters);
}
@Override
public void delete(String id, Parameters parameters)
{

View File

@@ -50,6 +50,7 @@ import org.junit.runners.Suite;
org.alfresco.repo.webdav.WebDAVLockServiceImplTest.class,
org.alfresco.rest.api.RulesUnitTests.class,
org.alfresco.rest.api.CategoriesUnitTests.class,
org.alfresco.rest.api.TagsUnitTests.class,
org.alfresco.rest.api.impl.ContentStorageInformationImplTest.class,
org.alfresco.rest.api.nodes.NodeStorageInfoRelationTest.class,
org.alfresco.rest.api.search.ResultMapperTests.class,

View File

@@ -0,0 +1,42 @@
/*
* #%L
* Alfresco Remote API
* %%
* Copyright (C) 2005 - 2023 Alfresco Software Limited
* %%
* This file is part of the Alfresco software.
* If the software was purchased under a paid Alfresco license, the terms of
* the paid license agreement will prevail. Otherwise, the software is
* provided under the following open source license terms:
*
* Alfresco is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Alfresco 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 Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
* #L%
*/
package org.alfresco.rest.api;
import org.alfresco.rest.api.impl.TagsImplTest;
import org.alfresco.rest.api.tags.TagsEntityResourceTest;
import org.alfresco.service.Experimental;
import org.junit.runner.RunWith;
import org.junit.runners.Suite;
@Experimental
@RunWith(Suite.class)
@Suite.SuiteClasses({
TagsImplTest.class,
TagsEntityResourceTest.class
})
public class TagsUnitTests
{
}

View File

@@ -133,7 +133,6 @@ public class CategoriesImplTest
given(typeConstraint.matches(any())).willReturn(true);
given(permissionServiceMock.hasReadPermission(any())).willReturn(AccessStatus.ALLOWED);
given(permissionServiceMock.hasPermission(any(), any())).willReturn(AccessStatus.ALLOWED);
//given(parametersMock.getInclude()).willReturn(Co);
}
@Test

View File

@@ -23,16 +23,33 @@
* along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
* #L%
*/
package org.alfresco.rest.api.impl;
import static org.alfresco.rest.api.impl.TagsImpl.NOT_A_VALID_TAG;
import static org.alfresco.rest.api.impl.TagsImpl.NO_PERMISSION_TO_MANAGE_A_TAG;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.catchThrowable;
import static org.junit.Assert.assertThrows;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.BDDMockito.given;
import static org.mockito.BDDMockito.then;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;
import org.alfresco.rest.api.Nodes;
import org.alfresco.rest.api.model.Tag;
import org.alfresco.rest.framework.core.exceptions.EntityNotFoundException;
import org.alfresco.rest.framework.core.exceptions.InvalidArgumentException;
import org.alfresco.rest.framework.core.exceptions.PermissionDeniedException;
import org.alfresco.rest.framework.resource.parameters.Parameters;
import org.alfresco.service.cmr.repository.DuplicateChildNodeNameException;
import org.alfresco.service.cmr.repository.NodeRef;
import org.alfresco.service.cmr.repository.StoreRef;
import org.alfresco.service.cmr.security.AuthorityService;
import org.alfresco.service.cmr.tagging.TaggingService;
import org.alfresco.util.Pair;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -40,16 +57,12 @@ import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnitRunner;
import static org.junit.Assert.assertThrows;
import static org.mockito.BDDMockito.given;
import static org.mockito.BDDMockito.then;
@RunWith(MockitoJUnitRunner.class)
public class TagsImplTest
{
private static final String TAG_ID = "tag-node-id";
private static final String TAG_NAME = "tag-dummy-name";
private static final NodeRef TAG_NODE_REF = new NodeRef(StoreRef.STORE_REF_WORKSPACE_SPACESSTORE,TAG_ID);
private static final NodeRef TAG_NODE_REF = new NodeRef(StoreRef.STORE_REF_WORKSPACE_SPACESSTORE, TAG_ID);
@Mock
private Nodes nodesMock;
@@ -57,6 +70,8 @@ public class TagsImplTest
private AuthorityService authorityServiceMock;
@Mock
private TaggingService taggingServiceMock;
@Mock
private Parameters parametersMock;
@InjectMocks
private TagsImpl objectUnderTest;
@@ -116,4 +131,162 @@ public class TagsImplTest
then(taggingServiceMock).shouldHaveNoInteractions();
}
@Test
public void testCreateTags()
{
final List<String> tagNames = List.of("tag1", "99gat");
final List<Tag> tagsToCreate = createTags(tagNames);
given(taggingServiceMock.createTags(any(), any())).willAnswer(invocation -> createTagAndNodeRefPairs(invocation.getArgument(1)));
//when
final List<Tag> actualCreatedTags = objectUnderTest.createTags(tagsToCreate, parametersMock);
then(authorityServiceMock).should().hasAdminAuthority();
then(authorityServiceMock).shouldHaveNoMoreInteractions();
then(taggingServiceMock).should().createTags(StoreRef.STORE_REF_WORKSPACE_SPACESSTORE, tagNames);
then(taggingServiceMock).shouldHaveNoMoreInteractions();
final List<Tag> expectedTags = createTagsWithNodeRefs(tagNames);
assertThat(actualCreatedTags)
.isNotNull()
.isEqualTo(expectedTags);
}
@Test
public void testCreateTags_withoutPermission()
{
given(authorityServiceMock.hasAdminAuthority()).willReturn(false);
//when
final Throwable actualException = catchThrowable(() -> objectUnderTest.createTags(List.of(createTag(TAG_NAME)), parametersMock));
then(authorityServiceMock).should().hasAdminAuthority();
then(authorityServiceMock).shouldHaveNoMoreInteractions();
then(taggingServiceMock).shouldHaveNoInteractions();
assertThat(actualException)
.isInstanceOf(PermissionDeniedException.class)
.hasMessageContaining(NO_PERMISSION_TO_MANAGE_A_TAG);
}
@Test
public void testCreateTags_passingNullInsteadList()
{
//when
final Throwable actualException = catchThrowable(() -> objectUnderTest.createTags(null, parametersMock));
then(taggingServiceMock).shouldHaveNoInteractions();
assertThat(actualException)
.isInstanceOf(InvalidArgumentException.class)
.hasMessageContaining(NOT_A_VALID_TAG);
}
@Test
public void testCreateTags_passingEmptyList()
{
//when
final Throwable actualException = catchThrowable(() -> objectUnderTest.createTags(Collections.emptyList(), parametersMock));
then(taggingServiceMock).shouldHaveNoInteractions();
assertThat(actualException)
.isInstanceOf(InvalidArgumentException.class)
.hasMessageContaining(NOT_A_VALID_TAG);
}
@Test
public void testCreateTags_passingListOfNulls()
{
//when
final Throwable actualException = catchThrowable(() -> objectUnderTest.createTags(Collections.singletonList(null), parametersMock));
then(taggingServiceMock).shouldHaveNoInteractions();
assertThat(actualException)
.isInstanceOf(InvalidArgumentException.class)
.hasMessageContaining(NOT_A_VALID_TAG);
}
@Test
public void testCreateTags_whileTagAlreadyExists()
{
given(taggingServiceMock.createTags(any(), any())).willThrow(new DuplicateChildNodeNameException(null, null, TAG_NAME, null));
//when
final Throwable actualException = catchThrowable(() -> objectUnderTest.createTags(List.of(createTag(TAG_NAME)), parametersMock));
then(taggingServiceMock).should().createTags(StoreRef.STORE_REF_WORKSPACE_SPACESSTORE, List.of(TAG_NAME));
then(taggingServiceMock).shouldHaveNoMoreInteractions();
assertThat(actualException).isInstanceOf(DuplicateChildNodeNameException.class);
}
@Test
public void testCreateTags_withRepeatedTagName()
{
final List<String> tagNames = List.of(TAG_NAME, TAG_NAME);
final List<Tag> tagsToCreate = createTags(tagNames);
given(taggingServiceMock.createTags(any(), any())).willAnswer(invocation -> createTagAndNodeRefPairs(invocation.getArgument(1)));
//when
final List<Tag> actualCreatedTags = objectUnderTest.createTags(tagsToCreate, parametersMock);
then(taggingServiceMock).should().createTags(StoreRef.STORE_REF_WORKSPACE_SPACESSTORE, List.of(TAG_NAME));
final List<Tag> expectedTags = List.of(createTagWithNodeRef(TAG_NAME));
assertThat(actualCreatedTags)
.isNotNull()
.isEqualTo(expectedTags);
}
@Test
public void testCreateTags_includingCount()
{
final List<String> tagNames = List.of("tag1", "99gat");
final List<Tag> tagsToCreate = createTags(tagNames);
given(taggingServiceMock.createTags(any(), any())).willAnswer(invocation -> createTagAndNodeRefPairs(invocation.getArgument(1)));
given(parametersMock.getInclude()).willReturn(List.of("count"));
//when
final List<Tag> actualCreatedTags = objectUnderTest.createTags(tagsToCreate, parametersMock);
final List<Tag> expectedTags = createTagsWithNodeRefs(tagNames).stream()
.peek(tag -> tag.setCount(0))
.collect(Collectors.toList());
assertThat(actualCreatedTags)
.isNotNull()
.isEqualTo(expectedTags);
}
private static List<Pair<String, NodeRef>> createTagAndNodeRefPairs(final List<String> tagNames)
{
return tagNames.stream()
.map(tagName -> createPair(tagName, new NodeRef(StoreRef.STORE_REF_WORKSPACE_SPACESSTORE, TAG_ID.concat("-").concat(tagName))))
.collect(Collectors.toList());
}
private static Pair<String, NodeRef> createPair(final String tagName, final NodeRef nodeRef)
{
return new Pair<>(tagName, nodeRef);
}
private static List<Tag> createTags(final List<String> tagNames)
{
return tagNames.stream().map(TagsImplTest::createTag).collect(Collectors.toList());
}
private static List<Tag> createTagsWithNodeRefs(final List<String> tagNames)
{
return tagNames.stream().map(TagsImplTest::createTagWithNodeRef).collect(Collectors.toList());
}
private static Tag createTag(final String tagName)
{
return Tag.builder()
.tag(tagName)
.create();
}
private static Tag createTagWithNodeRef(final String tagName)
{
return Tag.builder()
.nodeRef(new NodeRef(StoreRef.STORE_REF_WORKSPACE_SPACESSTORE, TAG_ID.concat("-").concat(tagName)))
.tag(tagName)
.create();
}
}

View File

@@ -0,0 +1,78 @@
/*
* #%L
* Alfresco Remote API
* %%
* Copyright (C) 2005 - 2023 Alfresco Software Limited
* %%
* This file is part of the Alfresco software.
* If the software was purchased under a paid Alfresco license, the terms of
* the paid license agreement will prevail. Otherwise, the software is
* provided under the following open source license terms:
*
* Alfresco is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Alfresco 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 Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
* #L%
*/
package org.alfresco.rest.api.tags;
import static org.alfresco.service.cmr.repository.StoreRef.STORE_REF_WORKSPACE_SPACESSTORE;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.BDDMockito.given;
import static org.mockito.BDDMockito.then;
import java.util.List;
import org.alfresco.rest.api.Tags;
import org.alfresco.rest.api.model.Tag;
import org.alfresco.rest.framework.resource.parameters.Parameters;
import org.alfresco.service.cmr.repository.NodeRef;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnitRunner;
@RunWith(MockitoJUnitRunner.class)
public class TagsEntityResourceTest
{
private static final String TAG_ID = "tag-dummy-id";
private static final String TAG_NAME = "tag-dummy-name";
private static final NodeRef TAG_NODE_REF = new NodeRef(STORE_REF_WORKSPACE_SPACESSTORE, TAG_ID);
@Mock
private Parameters parametersMock;
@Mock
private Tags tagsMock;
@InjectMocks
private TagsEntityResource tagsEntityResource;
@Test
public void testCreate()
{
final Tag tag = Tag.builder().tag(TAG_NAME).create();
final List<Tag> tags = List.of(tag);
given(tagsMock.createTags(any(), any())).willCallRealMethod();
given(tagsMock.createTags(any(), any(), any())).willReturn(List.of(Tag.builder().nodeRef(TAG_NODE_REF).tag(TAG_NAME).create()));
//when
final List<Tag> actualCreatedTags = tagsEntityResource.create(tags, parametersMock);
then(tagsMock).should().createTags(STORE_REF_WORKSPACE_SPACESSTORE, tags, parametersMock);
final List<Tag> expectedTags = List.of(Tag.builder().nodeRef(TAG_NODE_REF).tag(TAG_NAME).create());
assertThat(actualCreatedTags)
.isNotEmpty()
.isEqualTo(expectedTags);
}
}