JavaRush /Blog Java /Random-MS /Kami menambah keupayaan untuk melanggan kumpulan artikel....

Kami menambah keupayaan untuk melanggan kumpulan artikel. (Bahagian 1) - "Projek Java dari A hingga Z"

Diterbitkan dalam kumpulan
hello! Hari ini kami akan menambah langganan kepada kumpulan artikel dalam JavaRush. Ini sepadan dengan isu JRTB-5 pada GitHub. Biar saya jelaskan: JavaRush mempunyai bahagian yang dipanggil Artikel , dan di dalamnya terdapat Kumpulan Artikel. Ideanya adalah untuk menerima pemberitahuan tentang artikel baharu daripada satu atau lebih kumpulan melalui bot telegram."Projek Java dari A hingga Z": Menambah keupayaan untuk melanggan sekumpulan artikel.  Bahagian 1 - 1

Tambah JRTB-5

Katakan saya berminat dengan artikel daripada kumpulan Kisah Kejayaan . Oleh itu, saya ingin melanggan kemas kini daripada kumpulan ini dan menerima pautan ke penerbitan baharu setiap kali. Sebagai sebahagian daripada tugas ini, kita perlu belajar cara menggunakan API terbuka untuk bekerja dengan kumpulan dalam JavaRush. Hanya pada masa ini perkara seperti itu tiba. Berikut ialah pautan kepada perihalan API terbuka .
Kawan-kawan! Adakah anda ingin mengetahui dengan segera apabila kod baharu untuk projek atau artikel baharu dikeluarkan? Sertai saluran tg saya . Di sana saya mengumpulkan artikel, pemikiran dan pembangunan sumber terbuka saya bersama-sama.

Apa itu swagger? Mari kita fikirkan sekarang

Kami belum bercakap tentang kesombongan lagi. Bagi mereka yang tidak tahu, saya akan menerangkan secara ringkas: ini adalah tempat di mana anda boleh melihat secara terbuka pada API pelayan dan cuba membuat beberapa permintaan kepadanya. Biasanya, seorang yang sombong mengumpulkan permintaan yang mungkin. Dalam kes kami, terdapat tiga kumpulan: forum-question , group , post . Dalam setiap kumpulan akan ada satu atau lebih permintaan yang menunjukkan semua data yang diperlukan untuk membina permintaan ini (iaitu, apakah parameter tambahan yang boleh diluluskan, apa yang perlu dilakukan dengan mereka, kaedah http apa, dan sebagainya). Saya menasihati anda untuk membaca dan menonton lebih lanjut mengenai topik ini, kerana ini adalah bahagian pembangunan yang hampir setiap daripada anda akan hadapi. Untuk mengetahuinya, mari kita ketahui berapa banyak kumpulan yang terdapat dalam JavaRush. Untuk melakukan ini, kembangkan kumpulan pengawal kumpulan dan pilih Dapatkan permintaan /api/1.0/rest/groups/count . Ia akan mengembalikan kepada kami bilangan kumpulan dalam JavaRush. Mari lihat: "Projek Java dari A hingga Z": Menambah keupayaan untuk melanggan sekumpulan artikel.  Bahagian 1 - 2Imej menunjukkan bahawa pertanyaan ini menyokong beberapa parameter (pertanyaan, jenis, penapis). Untuk mencuba permintaan ini, anda perlu mencari butang Try it out , selepas itu parameter ini boleh dikonfigurasikan: "Projek Java dari A hingga Z": Menambah keupayaan untuk melanggan sekumpulan artikel.  Bahagian 1 - 3Anda juga boleh mengkonfigurasi jenis, penapis dan pertanyaan di sana (ia sebenarnya menarik di sini: ini akan menjadi carian teks dalam kumpulan). Tetapi buat masa ini, mari jalankannya tanpa sebarang sekatan dan lihat berapa banyak kumpulan yang terdapat dalam JavaRush. Untuk melakukan ini, klik pada Laksanakan. Di bawahnya akan ada respons (dalam bahagian Respons Pelayan) untuk permintaan ini: "Projek Java dari A hingga Z": Menambah keupayaan untuk melanggan sekumpulan artikel.  Bahagian 1 - 4Kami melihat bahawa terdapat 30 kumpulan secara keseluruhan , permintaan ini telah diselesaikan dalam 95ms dan terdapat satu set beberapa pengepala dalam respons. Seterusnya, mari cuba untuk mengkonfigurasi beberapa parameter. Mari pilih parameter jenis yang sama dengan nilai SYARIKAT dan lihat bagaimana keputusan berubah: "Projek Java dari A hingga Z": Menambah keupayaan untuk melanggan sekumpulan artikel.  Bahagian 1 - 5Terdapat 4 daripadanya. Bagaimana untuk menyemak ini? Mudah sahaja: anda boleh pergi ke tapak web, cari bahagian artikel, pilih semua kumpulan dan tambah penapis yang sesuai di sana ( https://javarush.com/groups/all?type=COMPANY ). Dan ya, memang ada 4 sahaja. Walaupun sebenarnya ada tiga :D "Projek Java dari A hingga Z": Menambah keupayaan untuk melanggan sekumpulan artikel.  Bahagian 1 - 6Setakat ini ia sesuai. By the way, kalau kita semak universiti, belum ada lagi. Hanya untuk keseronokan, lihat apa yang berlaku jika anda menetapkan penapis = MY dalam penyemak imbas di mana anda log masuk ke Javarush dan tidak log masuk. Lebih lanjut mengenai kesombongan - dalam artikel tentang Habré ini .

Menulis pelanggan untuk Javarush API untuk kumpulan

Kini, berdasarkan API terbuka, kami akan menulis klien Java yang boleh membuat permintaan, menerima respons dan mengetahui dengan tepat objek yang akan tiba. Kami juga akan mengambil objek dari kesombongan, dari bahagian Model (di bahagian paling bawah halaman). Mari buat pakej baharu dan panggil ia javarushclient di sebelah perkhidmatan, repositori. Pada masa hadapan, kami akan memindahkannya ke perpustakaan berasingan dalam organisasi Komuniti Javarush dan akan menggunakannya secara eksklusif sebagai pergantungan.
Saya sudah menulis tentang membuat pelanggan Java dalam artikel "Panduan untuk mencipta klien untuk API Skyscanner dan menerbitkannya dalam jCenter dan Maven Central" .
Pertama sekali, anda perlu menambah Unitrest, perpustakaan untuk membuat permintaan http ke API JavaRush:
<dependency>
  <groupId>com.konghq</groupId>
  <artifactId>unirest-java</artifactId>
  <version>${unirest.version}</version>
</dependency>
Dan letakkan versi dalam blok sifat:
<unirest.version>3.11.01</unirest.version>
Sebaik sahaja kami mempunyai pergantungan, kami boleh mula menambah kod. Mari buat klien untuk kumpulan JavaRushGroupClient dan pelaksanaan dalam kelas JavaRushGroupClientImpl. Tetapi pertama-tama anda perlu membuat DTO (objek pemindahan data) - iaitu, kelas yang objeknya akan membawa semua data yang diperlukan untuk pelanggan. Semua model boleh dilihat dalam kesombongan. Di bahagian paling bawah terdapat bahagian Model , di mana anda boleh mengiranya. Beginilah rupa GroupDiscussionInfo dalam swagger: Dalam pakej javarushclient, kami akan mencipta "Projek Java dari A hingga Z": Menambah keupayaan untuk melanggan sekumpulan artikel.  Bahagian 1 - 7pakej dto , yang mana kami akan menambah, berdasarkan data dari swagger, kelas berikut:
  • MeGroupInfoStatus :

    package com.github.javarushcommunity.jrtb.javarushclient.dto;
    
    /**
    * Member group status.
    */
    public enum MeGroupInfoStatus {
       UNKNOWN, CANDIDATE, INVITEE, MEMBER, EDITOR, MODERATOR, ADMINISTRATOR, BANNED
    }

  • MeGroupInfo :

    package com.github.javarushcommunity.jrtb.javarushclient.dto;
    
    import lombok.Data;
    
    /**
    * Group information related to authorized user. If there is no user - will be null.
    */
    @Data
    public class MeGroupInfo {
       private MeGroupInfoStatus status;
       private Integer userGroupId;
    }

  • GroupInfoType :

    package com.github.javarushcommunity.jrtb.javarushclient.dto;
    
    /**
    * Group Info type;
    */
    public enum GroupInfoType {
       UNKNOWN, CITY, COMPANY, COLLEGE, TECH, SPECIAL, COUNTRY
    }

  • Maklumat Perbincangan Pengguna :

    package com.github.javarushcommunity.jrtb.javarushclient.dto;
    
    import lombok.Data;
    
    /**
    * DTO for User discussion info.
    */
    @Data
    public class UserDiscussionInfo {
    
       private Boolean isBookmarked;
       private Integer lastTime;
       private Integer newCommentsCount;
    }

  • GroupVisibilitysdtatus :

    package com.github.javarushcommunity.jrtb.javarushclient.dto;
    
    /**
    * Group Visibility status.
    */
    public enum GroupVisibilityStatus {
       UNKNOWN, RESTRICTED, PUBLIC, PROTECTED, PRIVATE, DISABLED, DELETED
    }

  • Kemudian - GroupInfo :

    package com.github.javarushcommunity.jrtb.javarushclient.dto;
    
    import lombok.Data;
    import lombok.ToString;
    
    /**
    * Group Info DTO class.
    */
    @Data
    @ToString
    public class GroupInfo {
    
       private Integer id;
       private String avatarUrl;
       private String createTime;
       private String description;
       private String key;
       private Integer levelToEditor;
       private MeGroupInfo meGroupInfo;
       private String pictureUrl;
       private String title;
       private GroupInfoType type;
       private Integer userCount;
       private GroupVisibilityStatus visibilityStatus;
    }

Memandangkan GroupInfo dan GroupDiscussionInfo hampir sama sepenuhnya, mari kita hubungkan mereka dalam warisan - GroupDiscusionInfo :
package com.github.javarushcommunity.jrtb.javarushclient.dto;

import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.ToString;

/**
* Group discussion info class.
*/
@EqualsAndHashCode(callSuper = true)
@Data
@ToString(callSuper = true)
public class GroupDiscussionInfo extends GroupInfo {

   private UserDiscussionInfo userDiscussionInfo;
   private Integer commentsCount;
}
Kami juga memerlukan penapis untuk permintaan GroupFilter :
package com.github.javarushcommunity.jrtb.javarushclient.dto;

/**
* Filters for group requests.
*/
public enum GroupFilter {

   UNKNOWN, MY, ALL
}
Dalam permintaan untuk mendapatkan melalui ID, ia mengembalikan GroupDiscussionInfo dan dalam permintaan untuk koleksi kumpulan, anda boleh mendapatkan kedua-dua GroupInfo dan GroupDiscussionInfo. Memandangkan permintaan boleh mempunyai jenis, pertanyaan, penapis, mengimbangi dan had, mari buat kelas GroupRequestArgs yang berasingan dan jadikan ia kelas pembina (baca apakah corak pembina):
package com.github.javarushcommunity.jrtb.javarushclient.dto;

import lombok.*;

import java.util.HashMap;
import java.util.Map;

import static java.util.Objects.nonNull;

/**
* Request arguments for group requests.
*/
@Builder
@Getter
public class GroupRequestArgs {

   private final String query;
   private final GroupInfoType type;
   private final GroupFilter filter;

   /**
    * specified where to start getting groups
    */
   private final Integer offset;
   /**
    * Limited number of groups.
    */
   private final Integer limit;

   public Map populateQueries() {
       Map queries = new HashMap<>();
       if(nonNull(query)) {
           queries.put("query", query);
       }
       if(nonNull(type)) {
           queries.put("type", type);
       }
       if(nonNull(filter)) {
           queries.put("filter", filter);
       }
       if(nonNull(offset)) {
           queries.put("offset", offset);
       }
       if(nonNull(limit)) {
           queries.put("limit", limit);
       }
       return queries;
   }
}
Untuk mencari bilangan kumpulan, ia sedikit berbeza. Ia hanya mempunyai pertanyaan, jenis dan penapis. Dan nampaknya anda tidak mahu menduplikasi kod tersebut. Pada masa yang sama, jika anda mula menggabungkannya, ternyata hodoh apabila bekerja dengan pembina. Jadi saya memutuskan untuk memisahkannya dan mengulangi kod tersebut. Inilah rupa GroupCountRequestArgs :
package com.github.javarushcommunity.jrtb.javarushclient.dto;

import lombok.Builder;
import lombok.Getter;

import java.util.HashMap;
import java.util.Map;

import static java.util.Objects.nonNull;

/**
* Request arguments for group count requests.
*/
@Builder
@Getter
public class GroupsCountRequestArgs {
   private final String query;
   private final GroupInfoType type;
   private final GroupFilter filter;

   public Map populateQueries() {
       Map queries = new HashMap<>();
       if (nonNull(query)) {
           queries.put("query", query);
       }
       if (nonNull(type)) {
           queries.put("type", type);
       }
       if (nonNull(filter)) {
           queries.put("filter", filter);
       }
       return queries;
   }
}
Ya, saya tidak menyebut bahawa dua kelas terakhir mempunyai kaedah populateQueries, yang akan menyediakan peta untuk membuat pertanyaan (anda akan melihatnya kemudian). Berdasarkan kelas yang diterangkan di atas, mari buat antara muka untuk JavaRushGroupClient :
package com.github.javarushcommunity.jrtb.javarushclient;

import com.github.javarushcommunity.jrtb.javarushclient.dto.GroupDiscussionInfo;
import com.github.javarushcommunity.jrtb.javarushclient.dto.GroupInfo;
import com.github.javarushcommunity.jrtb.javarushclient.dto.GroupRequestArgs;
import com.github.javarushcommunity.jrtb.javarushclient.dto.GroupsCountRequestArgs;

import java.util.List;

/**
 * Client for Javarush Open API corresponds to Groups.
 */
public interface JavaRushGroupClient {

    /**
     * Get all the {@link GroupInfo} filtered by provided {@link GroupRequestArgs}.
     *
     * @param requestArgs provided {@link GroupRequestArgs}.
     * @return the collection of the {@link GroupInfo} objects.
     */
    List<GroupInfo> getGroupList(GroupRequestArgs requestArgs);

    /**
     * Get all the {@link GroupDiscussionInfo} filtered by provided {@link GroupRequestArgs}.
     *
     * @param requestArgs provided {@link GroupRequestArgs}
     * @return the collection of the {@link GroupDiscussionInfo} objects.
     */
    List<GroupDiscussionInfo> getGroupDiscussionList(GroupRequestArgs requestArgs);

    /**
     * Get count of groups filtered by provided {@link GroupRequestArgs}.
     *
     * @param countRequestArgs provided {@link GroupsCountRequestArgs}.
     * @return the count of the groups.
     */
    Integer getGroupCount(GroupsCountRequestArgs countRequestArgs);

    /**
     * Get {@link GroupDiscussionInfo} by provided ID.
     *
     * @param id provided ID.
     * @return {@link GroupDiscussionInfo} object.
     */
    GroupDiscussionInfo getGroupById(Integer id);
}
Dua permintaan berbeza untuk kes apabila kami ingin mendapatkan maklumat GroupInfo atau GroupDiscussionInfo ditambah. Jika tidak, permintaan ini adalah sama, dan satu-satunya perbezaan adalah bahawa dalam satu bendera includeDiscussion akan menjadi benar, dan dalam satu lagi ia akan palsu. Oleh itu, terdapat 4 kaedah, bukan tiga. Sekarang mari kita mula melaksanakan:
package com.github.javarushcommunity.jrtb.javarushclient;

import com.github.javarushcommunity.jrtb.javarushclient.dto.GroupDiscussionInfo;
import com.github.javarushcommunity.jrtb.javarushclient.dto.GroupInfo;
import com.github.javarushcommunity.jrtb.javarushclient.dto.GroupRequestArgs;
import com.github.javarushcommunity.jrtb.javarushclient.dto.GroupsCountRequestArgs;
import kong.unirest.GenericType;
import kong.unirest.Unirest;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

import java.util.List;

/**
* Implementation of the {@link JavaRushGroupClient} interface.
*/
@Component
public class JavaRushGroupClientImpl implements JavaRushGroupClient {

   private final String javarushApiGroupPath;

   public JavaRushGroupClientImpl(@Value("${javarush.api.path}") String javarushApi) {
       this.javarushApiGroupPath = javarushApi + "/groups";
   }

   @Override
   public List<GroupInfo> getGroupList(GroupRequestArgs requestArgs) {
       return Unirest.get(javarushApiGroupPath)
               .queryString(requestArgs.populateQueries())
               .asObject(new GenericType<list<GroupInfo>>() {
               })
               .getBody();
   }

   @Override
   public List<GroupDiscussionInfo> getGroupDiscussionList(GroupRequestArgs requestArgs) {
       return Unirest.get(javarushApiGroupPath)
               .queryString(requestArgs.populateQueries())
               .asObject(new GenericType<list<GroupDiscussionInfo>>() {
               })
               .getBody();
   }

   @Override
   public Integer getGroupCount(GroupsCountRequestArgs countRequestArgs) {
       return Integer.valueOf(
               Unirest.get(String.format("%s/count", javarushApiGroupPath))
                       .queryString(countRequestArgs.populateQueries())
                       .asString()
                       .getBody()
       );
   }

   @Override
   public GroupDiscussionInfo getGroupById(Integer id) {
       return Unirest.get(String.format("%s/group%s", javarushApiGroupPath, id.toString()))
               .asObject(GroupDiscussionInfo.class)
               .getBody();
   }


}
Saya menambah laluan ke API dalam pembina menggunakan anotasi Nilai yang sudah biasa. Ini menunjukkan bahawa nilai dalam anotasi sepadan dengan medan dalam fail sifat. Oleh itu, mari tambah baris baharu pada application.properties:
javarush.api.path=https://javarush.com/api/1.0/rest
Nilai ini kini akan berada di satu tempat untuk semua klien API, dan jika laluan API berubah, kami akan mengemas kininya dengan cepat. Sebelum ini, saya memalu paku dengan mikroskop, menerima respons daripada permintaan http melalui Unirest, menterjemahkannya ke dalam rentetan dan kemudian menghuraikan rentetan ini melalui Jackson... Ia menakutkan, membosankan dan memerlukan banyak perkara tambahan. Di perpustakaan ini anda boleh melihat rupanya. Sebaik sahaja saya mendapatkannya, saya akan memfaktorkan semula segala-galanya.
Sesiapa sahaja yang ingin mencuba mengemas kini perpustakaan ini - tambah objek penerima hanya menggunakan alatan perpustakaan unirest - tulis dalam mesej peribadi atau sebagai isu baharu dalam perpustakaan itu sendiri. Ini akan menjadi pengalaman kerja sebenar untuk anda, tetapi saya tidak kisah. Saya akan menjalankan semakan kod penuh dan bantuan jika perlu.
Sekarang persoalannya ialah: adakah kod kami berfungsi seperti yang kami harapkan? Jawapannya mudah: anda hanya perlu menulis ujian untuk mereka. Seperti yang saya katakan lebih daripada sekali, pembangun mesti boleh menulis ujian. Oleh itu, menggunakan UI Swagger kami, kami akan menghantar permintaan, melihat respons dan menggantikannya ke dalam ujian sebagai hasil yang dijangkakan. Anda mungkin menyedari dengan segera bahawa bilangan kumpulan tidak statik dan boleh berubah. Dan awak betul. Satu-satunya soalan ialah berapa kerap nombor ini berubah? Sangat jarang, jadi dalam tempoh beberapa bulan kita boleh mengatakan bahawa nilai ini akan menjadi statik. Dan jika sesuatu berubah, kami akan mengemas kini ujian. Bertemu - JavaRushGroupClientTest:
package com.github.javarushcommunity.jrtb.javarushclient;

import com.github.javarushcommunity.jrtb.javarushclient.dto.GroupDiscussionInfo;
import com.github.javarushcommunity.jrtb.javarushclient.dto.GroupInfo;
import com.github.javarushcommunity.jrtb.javarushclient.dto.GroupRequestArgs;
import com.github.javarushcommunity.jrtb.javarushclient.dto.GroupsCountRequestArgs;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;

import java.util.List;

import static com.github.javarushcommunity.jrtb.javarushclient.dto.GroupInfoType.TECH;

@DisplayName("Integration-level testing for JavaRushGroupClientImplTest")
class JavaRushGroupClientTest {

   private final JavaRushGroupClient groupClient = new JavaRushGroupClientImpl("https://javarush.com/api/1.0/rest");

   @Test
   public void shouldProperlyGetGroupsWithEmptyArgs() {
       //given
       GroupRequestArgs args = GroupRequestArgs.builder().build();

       //when
       List<GroupInfo> groupList = groupClient.getGroupList(args);

       //then
       Assertions.assertNotNull(groupList);
       Assertions.assertFalse(groupList.isEmpty());
   }

   @Test
   public void shouldProperlyGetWithOffSetAndLimit() {
       //given
       GroupRequestArgs args = GroupRequestArgs.builder()
               .offset(1)
               .limit(3)
               .build();

       //when
       List<GroupInfo> groupList = groupClient.getGroupList(args);

       //then
       Assertions.assertNotNull(groupList);
       Assertions.assertEquals(3, groupList.size());
   }

   @Test
   public void shouldProperlyGetGroupsDiscWithEmptyArgs() {
       //given
       GroupRequestArgs args = GroupRequestArgs.builder().build();

       //when
       List<GroupDiscussionInfo> groupList = groupClient.getGroupDiscussionList(args);

       //then
       Assertions.assertNotNull(groupList);
       Assertions.assertFalse(groupList.isEmpty());
   }

   @Test
   public void shouldProperlyGetGroupDiscWithOffSetAndLimit() {
       //given
       GroupRequestArgs args = GroupRequestArgs.builder()
               .offset(1)
               .limit(3)
               .build();

       //when
       List<GroupDiscussionInfo> groupList = groupClient.getGroupDiscussionList(args);

       //then
       Assertions.assertNotNull(groupList);
       Assertions.assertEquals(3, groupList.size());
   }

   @Test
   public void shouldProperlyGetGroupCount() {
       //given
       GroupsCountRequestArgs args = GroupsCountRequestArgs.builder().build();

       //when
       Integer groupCount = groupClient.getGroupCount(args);

       //then
       Assertions.assertEquals(30, groupCount);
   }

   @Test
   public void shouldProperlyGetGroupTECHCount() {
       //given
       GroupsCountRequestArgs args = GroupsCountRequestArgs.builder()
               .type(TECH)
               .build();

       //when
       Integer groupCount = groupClient.getGroupCount(args);

       //then
       Assertions.assertEquals(7, groupCount);
   }

   @Test
   public void shouldProperlyGetGroupById() {
       //given
       Integer androidGroupId = 16;

       //when
       GroupDiscussionInfo groupById = groupClient.getGroupById(androidGroupId);

       //then
       Assertions.assertNotNull(groupById);
       Assertions.assertEquals(16, groupById.getId());
       Assertions.assertEquals(TECH, groupById.getType());
       Assertions.assertEquals("android", groupById.getKey());
   }
}
Ujian ditulis dalam gaya yang sama seperti sebelum ini. Terdapat beberapa ujian untuk setiap permintaan. Tidak ada gunanya menguji segala-galanya, kerana saya berpendapat bahawa API ini telah pun diuji dengan cara yang terbaik.

Kesimpulan

Sebagai sebahagian daripada artikel ini, kami menambahkan klien Java untuk kumpulan pada API JavaRush. Seperti yang mereka katakan, hidup dan belajar. Semasa saya menulis pelanggan ini, saya mengambil kesempatan daripada dokumentasi mereka dan dengan mudah menggunakan kerja dengan objek yang mereka sediakan. Saya menarik perhatian anda kepada tugas yang saya cadangkan. Jika sesiapa berminat, tulis mesej peribadi kepada saya, saya lebih pasti bahawa ia akan menjadi pengalaman yang sangat menarik. Ini adalah bahagian pertama. Dalam yang kedua, kami akan melaksanakan secara langsung arahan menambah dan (jika kami memasukkannya ke dalam satu artikel) kami akan menambah mendapatkan senarai kumpulan yang dilanggan oleh pengguna. Seterusnya, sesiapa yang mempunyai keinginan dan bakat untuk menulis teks untuk bot, sila tulis kepada saya dalam PM. Saya bukan pakar dalam perkara ini dan sebarang bantuan akan sangat membantu. Mari kita rasmikan semua ini sebagai pembangunan sumber terbuka, ia akan menjadi menarik! Baiklah, seperti biasa - suka, langgan, loceng , berikan projek kami bintang , tulis komen dan nilaikan artikel itu!
pautan yang berguna

Senarai semua bahan dalam siri ini adalah pada permulaan artikel ini.

Komen
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION