mirror of
https://github.com/google/nomulus.git
synced 2025-05-09 00:08:21 +02:00
Also removed the rdapWhoisServer value, as it's just null and will likely stay that way (it isn't mentioned in the RDAP response profile) If it'll ever become required, we can add it back. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=247643981
557 lines
20 KiB
Java
557 lines
20 KiB
Java
// Copyright 2017 The Nomulus Authors. All Rights Reserved.
|
|
//
|
|
// Licensed 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.
|
|
|
|
package google.registry.rdap;
|
|
|
|
import static com.google.common.truth.Truth.assertThat;
|
|
import static google.registry.testing.DatastoreHelper.createTld;
|
|
import static google.registry.testing.DatastoreHelper.persistResource;
|
|
import static google.registry.testing.DatastoreHelper.persistSimpleResources;
|
|
import static google.registry.testing.FullFieldsTestEntityHelper.makeAndPersistContactResource;
|
|
import static google.registry.testing.FullFieldsTestEntityHelper.makeAndPersistHostResource;
|
|
import static google.registry.testing.FullFieldsTestEntityHelper.makeDomainBase;
|
|
import static google.registry.testing.FullFieldsTestEntityHelper.makeHistoryEntry;
|
|
import static google.registry.testing.FullFieldsTestEntityHelper.makeRegistrar;
|
|
import static google.registry.testing.TestDataHelper.loadFile;
|
|
import static javax.servlet.http.HttpServletResponse.SC_BAD_REQUEST;
|
|
|
|
import com.google.common.collect.ImmutableList;
|
|
import com.google.common.collect.ImmutableSet;
|
|
import com.google.gson.Gson;
|
|
import com.google.gson.JsonElement;
|
|
import com.googlecode.objectify.Key;
|
|
import google.registry.model.contact.ContactResource;
|
|
import google.registry.model.domain.DesignatedContact;
|
|
import google.registry.model.domain.DomainBase;
|
|
import google.registry.model.domain.Period;
|
|
import google.registry.model.eppcommon.StatusValue;
|
|
import google.registry.model.host.HostResource;
|
|
import google.registry.model.ofy.Ofy;
|
|
import google.registry.model.registrar.Registrar;
|
|
import google.registry.model.registrar.RegistrarContact;
|
|
import google.registry.model.reporting.HistoryEntry;
|
|
import google.registry.model.transfer.TransferData;
|
|
import google.registry.model.transfer.TransferStatus;
|
|
import google.registry.rdap.RdapJsonFormatter.OutputDataType;
|
|
import google.registry.rdap.RdapObjectClasses.BoilerplateType;
|
|
import google.registry.rdap.RdapObjectClasses.ReplyPayloadBase;
|
|
import google.registry.rdap.RdapObjectClasses.TopLevelReplyObject;
|
|
import google.registry.testing.AppEngineRule;
|
|
import google.registry.testing.FakeClock;
|
|
import google.registry.testing.InjectRule;
|
|
import java.util.Optional;
|
|
import org.joda.time.DateTime;
|
|
import org.junit.Before;
|
|
import org.junit.Rule;
|
|
import org.junit.Test;
|
|
import org.junit.runner.RunWith;
|
|
import org.junit.runners.JUnit4;
|
|
|
|
/** Unit tests for {@link RdapJsonFormatter}. */
|
|
@RunWith(JUnit4.class)
|
|
public class RdapJsonFormatterTest {
|
|
|
|
@Rule
|
|
public final AppEngineRule appEngine = AppEngineRule.builder()
|
|
.withDatastore()
|
|
.build();
|
|
@Rule public final InjectRule inject = new InjectRule();
|
|
|
|
private final FakeClock clock = new FakeClock(DateTime.parse("1999-01-01T00:00:00Z"));
|
|
|
|
private RdapJsonFormatter rdapJsonFormatter;
|
|
|
|
private Registrar registrar;
|
|
private DomainBase domainBaseFull;
|
|
private DomainBase domainBaseNoNameserversNoTransfers;
|
|
private HostResource hostResourceIpv4;
|
|
private HostResource hostResourceIpv6;
|
|
private HostResource hostResourceBoth;
|
|
private HostResource hostResourceNoAddresses;
|
|
private HostResource hostResourceNotLinked;
|
|
private HostResource hostResourceSuperordinatePendingTransfer;
|
|
private ContactResource contactResourceRegistrant;
|
|
private ContactResource contactResourceAdmin;
|
|
private ContactResource contactResourceTech;
|
|
private ContactResource contactResourceNotLinked;
|
|
|
|
|
|
@Before
|
|
public void setUp() {
|
|
inject.setStaticField(Ofy.class, "clock", clock);
|
|
|
|
rdapJsonFormatter = RdapTestHelper.getTestRdapJsonFormatter(clock);
|
|
rdapJsonFormatter.rdapAuthorization =
|
|
RdapAuthorization.create(RdapAuthorization.Role.REGISTRAR, "unicoderegistrar");
|
|
|
|
// Create the registrar in 1999, then update it in 2000.
|
|
clock.setTo(DateTime.parse("1999-01-01T00:00:00Z"));
|
|
createTld("xn--q9jyb4c");
|
|
registrar = persistResource(makeRegistrar("unicoderegistrar", "みんな", Registrar.State.ACTIVE));
|
|
clock.setTo(DateTime.parse("2000-01-01T00:00:00Z"));
|
|
registrar = persistResource(registrar);
|
|
|
|
persistSimpleResources(makeMoreRegistrarContacts(registrar));
|
|
|
|
contactResourceRegistrant = makeAndPersistContactResource(
|
|
"8372808-ERL",
|
|
"(◕‿◕)",
|
|
"lol@cat.みんな",
|
|
null,
|
|
clock.nowUtc().minusYears(1),
|
|
registrar);
|
|
contactResourceAdmin = makeAndPersistContactResource(
|
|
"8372808-IRL",
|
|
"Santa Claus",
|
|
null,
|
|
ImmutableList.of("Santa Claus Tower", "41st floor", "Suite みんな"),
|
|
clock.nowUtc().minusYears(2),
|
|
registrar);
|
|
contactResourceTech = makeAndPersistContactResource(
|
|
"8372808-TRL",
|
|
"The Raven",
|
|
"bog@cat.みんな",
|
|
ImmutableList.of("Chamber Door", "upper level"),
|
|
clock.nowUtc().minusYears(3),
|
|
registrar);
|
|
contactResourceNotLinked = makeAndPersistContactResource(
|
|
"8372808-QRL",
|
|
"The Wizard",
|
|
"dog@cat.みんな",
|
|
ImmutableList.of("Somewhere", "Over the Rainbow"),
|
|
clock.nowUtc().minusYears(4),
|
|
registrar);
|
|
hostResourceIpv4 =
|
|
makeAndPersistHostResource(
|
|
"ns1.cat.みんな", "1.2.3.4", null, clock.nowUtc().minusYears(1), "unicoderegistrar");
|
|
hostResourceIpv6 =
|
|
makeAndPersistHostResource(
|
|
"ns2.cat.みんな",
|
|
"bad:f00d:cafe:0:0:0:15:beef",
|
|
null,
|
|
clock.nowUtc().minusYears(2),
|
|
"unicoderegistrar");
|
|
hostResourceBoth =
|
|
makeAndPersistHostResource(
|
|
"ns3.cat.みんな",
|
|
"1.2.3.4",
|
|
"bad:f00d:cafe:0:0:0:15:beef",
|
|
clock.nowUtc().minusYears(3),
|
|
"unicoderegistrar");
|
|
hostResourceNoAddresses =
|
|
makeAndPersistHostResource(
|
|
"ns4.cat.みんな", null, null, clock.nowUtc().minusYears(4), "unicoderegistrar");
|
|
hostResourceNotLinked =
|
|
makeAndPersistHostResource(
|
|
"ns5.cat.みんな", null, null, clock.nowUtc().minusYears(5), "unicoderegistrar");
|
|
hostResourceSuperordinatePendingTransfer =
|
|
persistResource(
|
|
makeAndPersistHostResource(
|
|
"ns1.dog.みんな", null, null, clock.nowUtc().minusYears(6), "unicoderegistrar")
|
|
.asBuilder()
|
|
.setSuperordinateDomain(
|
|
Key.create(
|
|
persistResource(
|
|
makeDomainBase(
|
|
"dog.みんな",
|
|
contactResourceRegistrant,
|
|
contactResourceAdmin,
|
|
contactResourceTech,
|
|
null,
|
|
null,
|
|
registrar)
|
|
.asBuilder()
|
|
.addStatusValue(StatusValue.PENDING_TRANSFER)
|
|
.setTransferData(
|
|
new TransferData.Builder()
|
|
.setTransferStatus(TransferStatus.PENDING)
|
|
.setGainingClientId("NewRegistrar")
|
|
.setTransferRequestTime(clock.nowUtc().minusDays(1))
|
|
.setLosingClientId("TheRegistrar")
|
|
.setPendingTransferExpirationTime(
|
|
clock.nowUtc().plusYears(100))
|
|
.setTransferredRegistrationExpirationTime(
|
|
DateTime.parse("2111-10-08T00:44:59Z"))
|
|
.build())
|
|
.build())))
|
|
.build());
|
|
domainBaseFull = persistResource(
|
|
makeDomainBase(
|
|
"cat.みんな",
|
|
contactResourceRegistrant,
|
|
contactResourceAdmin,
|
|
contactResourceTech,
|
|
hostResourceIpv4,
|
|
hostResourceIpv6,
|
|
registrar));
|
|
domainBaseNoNameserversNoTransfers = persistResource(
|
|
makeDomainBase(
|
|
"fish.みんな",
|
|
contactResourceRegistrant,
|
|
contactResourceAdmin,
|
|
contactResourceTech,
|
|
null,
|
|
null,
|
|
registrar));
|
|
|
|
// Create an unused domain that references hostResourceBoth and hostResourceNoAddresses so that
|
|
// they will have "associated" (ie, StatusValue.LINKED) status.
|
|
persistResource(
|
|
makeDomainBase(
|
|
"dog.みんな",
|
|
contactResourceRegistrant,
|
|
contactResourceAdmin,
|
|
contactResourceTech,
|
|
hostResourceBoth,
|
|
hostResourceNoAddresses,
|
|
registrar));
|
|
|
|
// history entries
|
|
persistResource(
|
|
makeHistoryEntry(
|
|
domainBaseFull,
|
|
HistoryEntry.Type.DOMAIN_CREATE,
|
|
Period.create(1, Period.Unit.YEARS),
|
|
"created",
|
|
clock.nowUtc().minusMonths(4)));
|
|
persistResource(
|
|
makeHistoryEntry(
|
|
domainBaseNoNameserversNoTransfers,
|
|
HistoryEntry.Type.DOMAIN_CREATE,
|
|
Period.create(1, Period.Unit.YEARS),
|
|
"created",
|
|
clock.nowUtc()));
|
|
// We create 3 "transfer approved" entries, to make sure we only save the last one
|
|
persistResource(
|
|
makeHistoryEntry(
|
|
domainBaseFull,
|
|
HistoryEntry.Type.DOMAIN_TRANSFER_APPROVE,
|
|
null,
|
|
null,
|
|
clock.nowUtc().minusMonths(3)));
|
|
persistResource(
|
|
makeHistoryEntry(
|
|
domainBaseFull,
|
|
HistoryEntry.Type.DOMAIN_TRANSFER_APPROVE,
|
|
null,
|
|
null,
|
|
clock.nowUtc().minusMonths(1)));
|
|
persistResource(
|
|
makeHistoryEntry(
|
|
domainBaseFull,
|
|
HistoryEntry.Type.DOMAIN_TRANSFER_APPROVE,
|
|
null,
|
|
null,
|
|
clock.nowUtc().minusMonths(2)));
|
|
// We create a "transfer approved" entry for domainBaseNoNameserversNoTransfers that happened
|
|
// before the domain was created, to make sure we don't show it
|
|
persistResource(
|
|
makeHistoryEntry(
|
|
domainBaseNoNameserversNoTransfers,
|
|
HistoryEntry.Type.DOMAIN_TRANSFER_APPROVE,
|
|
null,
|
|
null,
|
|
clock.nowUtc().minusMonths(3)));
|
|
}
|
|
|
|
public static ImmutableList<RegistrarContact> makeMoreRegistrarContacts(Registrar registrar) {
|
|
return ImmutableList.of(
|
|
new RegistrarContact.Builder()
|
|
.setParent(registrar)
|
|
.setName("Baby Doe")
|
|
.setEmailAddress("babydoe@example.com")
|
|
.setPhoneNumber("+1.2125551217")
|
|
.setFaxNumber("+1.2125551218")
|
|
.setTypes(ImmutableSet.of(RegistrarContact.Type.ADMIN))
|
|
.setVisibleInWhoisAsAdmin(false)
|
|
.setVisibleInWhoisAsTech(false)
|
|
.build(),
|
|
new RegistrarContact.Builder()
|
|
.setParent(registrar)
|
|
.setName("John Doe")
|
|
.setEmailAddress("johndoe@example.com")
|
|
.setFaxNumber("+1.2125551213")
|
|
.setTypes(ImmutableSet.of(RegistrarContact.Type.ADMIN))
|
|
.setVisibleInWhoisAsAdmin(false)
|
|
.setVisibleInWhoisAsTech(true)
|
|
.build(),
|
|
new RegistrarContact.Builder()
|
|
.setParent(registrar)
|
|
.setName("Jane Doe")
|
|
.setEmailAddress("janedoe@example.com")
|
|
.setPhoneNumber("+1.2125551215")
|
|
.setTypes(ImmutableSet.of(RegistrarContact.Type.TECH, RegistrarContact.Type.ADMIN))
|
|
.setVisibleInWhoisAsAdmin(true)
|
|
.setVisibleInWhoisAsTech(false)
|
|
.build(),
|
|
new RegistrarContact.Builder()
|
|
.setParent(registrar)
|
|
.setName("Play Doe")
|
|
.setEmailAddress("playdoe@example.com")
|
|
.setPhoneNumber("+1.2125551217")
|
|
.setFaxNumber("+1.2125551218")
|
|
.setTypes(ImmutableSet.of(RegistrarContact.Type.BILLING))
|
|
.setVisibleInWhoisAsAdmin(true)
|
|
.setVisibleInWhoisAsTech(true)
|
|
.build());
|
|
}
|
|
|
|
private JsonElement loadJson(String expectedFileName) {
|
|
return new Gson().fromJson(loadFile(this.getClass(), expectedFileName), JsonElement.class);
|
|
}
|
|
|
|
@Test
|
|
public void testRegistrar() {
|
|
assertThat(rdapJsonFormatter.makeRdapJsonForRegistrar(registrar, OutputDataType.FULL).toJson())
|
|
.isEqualTo(loadJson("rdapjson_registrar.json"));
|
|
}
|
|
|
|
@Test
|
|
public void testRegistrar_summary() {
|
|
assertThat(
|
|
rdapJsonFormatter.makeRdapJsonForRegistrar(registrar, OutputDataType.SUMMARY).toJson())
|
|
.isEqualTo(loadJson("rdapjson_registrar_summary.json"));
|
|
}
|
|
|
|
@Test
|
|
public void testHost_ipv4() {
|
|
assertThat(
|
|
rdapJsonFormatter.makeRdapJsonForHost(hostResourceIpv4, OutputDataType.FULL).toJson())
|
|
.isEqualTo(loadJson("rdapjson_host_ipv4.json"));
|
|
}
|
|
|
|
@Test
|
|
public void testHost_ipv6() {
|
|
assertThat(
|
|
rdapJsonFormatter.makeRdapJsonForHost(hostResourceIpv6, OutputDataType.FULL).toJson())
|
|
.isEqualTo(loadJson("rdapjson_host_ipv6.json"));
|
|
}
|
|
|
|
@Test
|
|
public void testHost_both() {
|
|
assertThat(
|
|
rdapJsonFormatter.makeRdapJsonForHost(hostResourceBoth, OutputDataType.FULL).toJson())
|
|
.isEqualTo(loadJson("rdapjson_host_both.json"));
|
|
}
|
|
|
|
@Test
|
|
public void testHost_both_summary() {
|
|
assertThat(
|
|
rdapJsonFormatter
|
|
.makeRdapJsonForHost(hostResourceBoth, OutputDataType.SUMMARY)
|
|
.toJson())
|
|
.isEqualTo(loadJson("rdapjson_host_both_summary.json"));
|
|
}
|
|
|
|
@Test
|
|
public void testHost_noAddresses() {
|
|
assertThat(
|
|
rdapJsonFormatter
|
|
.makeRdapJsonForHost(hostResourceNoAddresses, OutputDataType.FULL)
|
|
.toJson())
|
|
.isEqualTo(loadJson("rdapjson_host_no_addresses.json"));
|
|
}
|
|
|
|
@Test
|
|
public void testHost_notLinked() {
|
|
assertThat(
|
|
rdapJsonFormatter
|
|
.makeRdapJsonForHost(hostResourceNotLinked, OutputDataType.FULL)
|
|
.toJson())
|
|
.isEqualTo(loadJson("rdapjson_host_not_linked.json"));
|
|
}
|
|
|
|
@Test
|
|
public void testHost_superordinateHasPendingTransfer() {
|
|
assertThat(
|
|
rdapJsonFormatter
|
|
.makeRdapJsonForHost(hostResourceSuperordinatePendingTransfer, OutputDataType.FULL)
|
|
.toJson())
|
|
.isEqualTo(loadJson("rdapjson_host_pending_transfer.json"));
|
|
}
|
|
|
|
@Test
|
|
public void testRegistrant() {
|
|
assertThat(
|
|
rdapJsonFormatter
|
|
.makeRdapJsonForContact(
|
|
contactResourceRegistrant,
|
|
Optional.of(DesignatedContact.Type.REGISTRANT),
|
|
OutputDataType.FULL)
|
|
.toJson())
|
|
.isEqualTo(loadJson("rdapjson_registrant.json"));
|
|
}
|
|
|
|
@Test
|
|
public void testRegistrant_summary() {
|
|
assertThat(
|
|
rdapJsonFormatter
|
|
.makeRdapJsonForContact(
|
|
contactResourceRegistrant,
|
|
Optional.of(DesignatedContact.Type.REGISTRANT),
|
|
OutputDataType.SUMMARY)
|
|
.toJson())
|
|
.isEqualTo(loadJson("rdapjson_registrant_summary.json"));
|
|
}
|
|
|
|
@Test
|
|
public void testRegistrant_loggedOut() {
|
|
rdapJsonFormatter.rdapAuthorization = RdapAuthorization.PUBLIC_AUTHORIZATION;
|
|
assertThat(
|
|
rdapJsonFormatter
|
|
.makeRdapJsonForContact(
|
|
contactResourceRegistrant,
|
|
Optional.of(DesignatedContact.Type.REGISTRANT),
|
|
OutputDataType.FULL)
|
|
.toJson())
|
|
.isEqualTo(loadJson("rdapjson_registrant_logged_out.json"));
|
|
}
|
|
|
|
@Test
|
|
public void testRegistrant_baseHasNoTrailingSlash() {
|
|
// First, make sure we have a trailing slash at the end by default!
|
|
// This test tries to change the default state, if the default doesn't have a /, then this test
|
|
// doesn't help.
|
|
assertThat(rdapJsonFormatter.fullServletPath).endsWith("/");
|
|
rdapJsonFormatter.fullServletPath =
|
|
rdapJsonFormatter.fullServletPath.substring(
|
|
0, rdapJsonFormatter.fullServletPath.length() - 1);
|
|
assertThat(
|
|
rdapJsonFormatter
|
|
.makeRdapJsonForContact(
|
|
contactResourceRegistrant,
|
|
Optional.of(DesignatedContact.Type.REGISTRANT),
|
|
OutputDataType.FULL)
|
|
.toJson())
|
|
.isEqualTo(loadJson("rdapjson_registrant.json"));
|
|
}
|
|
|
|
@Test
|
|
public void testAdmin() {
|
|
assertThat(
|
|
rdapJsonFormatter
|
|
.makeRdapJsonForContact(
|
|
contactResourceAdmin,
|
|
Optional.of(DesignatedContact.Type.ADMIN),
|
|
OutputDataType.FULL)
|
|
.toJson())
|
|
.isEqualTo(loadJson("rdapjson_admincontact.json"));
|
|
}
|
|
|
|
@Test
|
|
public void testTech() {
|
|
assertThat(
|
|
rdapJsonFormatter
|
|
.makeRdapJsonForContact(
|
|
contactResourceTech,
|
|
Optional.of(DesignatedContact.Type.TECH),
|
|
OutputDataType.FULL)
|
|
.toJson())
|
|
.isEqualTo(loadJson("rdapjson_techcontact.json"));
|
|
}
|
|
|
|
@Test
|
|
public void testRolelessContact() {
|
|
assertThat(
|
|
rdapJsonFormatter
|
|
.makeRdapJsonForContact(
|
|
contactResourceTech,
|
|
Optional.empty(),
|
|
OutputDataType.FULL)
|
|
.toJson())
|
|
.isEqualTo(loadJson("rdapjson_rolelesscontact.json"));
|
|
}
|
|
|
|
@Test
|
|
public void testUnlinkedContact() {
|
|
assertThat(
|
|
rdapJsonFormatter
|
|
.makeRdapJsonForContact(
|
|
contactResourceNotLinked,
|
|
Optional.empty(),
|
|
OutputDataType.FULL)
|
|
.toJson())
|
|
.isEqualTo(loadJson("rdapjson_unlinkedcontact.json"));
|
|
}
|
|
|
|
@Test
|
|
public void testDomain_full() {
|
|
assertThat(
|
|
rdapJsonFormatter
|
|
.makeRdapJsonForDomain(
|
|
domainBaseFull,
|
|
OutputDataType.FULL)
|
|
.toJson())
|
|
.isEqualTo(loadJson("rdapjson_domain_full.json"));
|
|
}
|
|
|
|
@Test
|
|
public void testDomain_summary() {
|
|
assertThat(
|
|
rdapJsonFormatter
|
|
.makeRdapJsonForDomain(domainBaseFull, OutputDataType.SUMMARY)
|
|
.toJson())
|
|
.isEqualTo(loadJson("rdapjson_domain_summary.json"));
|
|
}
|
|
|
|
@Test
|
|
public void testDomain_logged_out() {
|
|
rdapJsonFormatter.rdapAuthorization = RdapAuthorization.PUBLIC_AUTHORIZATION;
|
|
assertThat(
|
|
rdapJsonFormatter.makeRdapJsonForDomain(domainBaseFull, OutputDataType.FULL).toJson())
|
|
.isEqualTo(loadJson("rdapjson_domain_logged_out.json"));
|
|
}
|
|
|
|
@Test
|
|
public void testDomain_noNameserversNoTransfers() {
|
|
assertThat(
|
|
rdapJsonFormatter
|
|
.makeRdapJsonForDomain(domainBaseNoNameserversNoTransfers, OutputDataType.FULL)
|
|
.toJson())
|
|
.isEqualTo(loadJson("rdapjson_domain_no_nameservers.json"));
|
|
}
|
|
|
|
@Test
|
|
public void testError() {
|
|
assertThat(
|
|
RdapObjectClasses.ErrorResponse.create(
|
|
SC_BAD_REQUEST, "Invalid Domain Name", "Not a valid domain name")
|
|
.toJson())
|
|
.isEqualTo(loadJson("rdapjson_error.json"));
|
|
}
|
|
|
|
@Test
|
|
public void testTopLevel() {
|
|
assertThat(
|
|
TopLevelReplyObject.create(
|
|
new ReplyPayloadBase(BoilerplateType.OTHER) {
|
|
@JsonableElement public String key = "value";
|
|
},
|
|
rdapJsonFormatter.createTosNotice())
|
|
.toJson())
|
|
.isEqualTo(loadJson("rdapjson_toplevel.json"));
|
|
}
|
|
|
|
@Test
|
|
public void testTopLevel_domain() {
|
|
assertThat(
|
|
TopLevelReplyObject.create(
|
|
new ReplyPayloadBase(BoilerplateType.DOMAIN) {
|
|
@JsonableElement public String key = "value";
|
|
},
|
|
rdapJsonFormatter.createTosNotice())
|
|
.toJson())
|
|
.isEqualTo(loadJson("rdapjson_toplevel_domain.json"));
|
|
}
|
|
}
|