Friday, May 8, 2020

How to configure Solr Index Sharding in Alfresco

How to configure Solr Index Sharding in Alfresco 

Objectives 

Solr is used as search engine in Alfresco. Overview concept is describe here: solr-overview.html. There are two cores:


  • alfresco - used for searching all live content
  • archive - used for searching content that has been marked as deleted

So, when there is too much documents stored in Alfresco i.e. over 100 million of documents, then one instance of Solr could works slowly. There is a few possibility to change Alfresco architecture to increase performance:
  • Enterprise
  • Enterprise - scaled
  • Replicated Index
  • Shared Index

All types of architecture is described in document:

I'm going to use my previous post "Alfresco new-project" as a base to this post and build Shared Index architecture.

For simplification I've changed approach to selected architecture - I focus on Solr but web layer and DB layer are designed in my example without replication and failover. I have only Alfresco Content Repository and only One Alfresco Share.





The most important thing is how does it work? Usually one request is executed in one core. Big Lucene query can be executed very long time. If we use index shards the query will be executed in defined numbers of separated processes across the Nodes. 

Finally you can read more about "Creating Solr shards" here: https://docs.alfresco.com/search-community/tasks/solr-hash-shard.html


The definition of Index Shards

At the end I would like to have 4 Nodes, 8 index shards, 3 replicas of each index shard. So, definition of index shards should be similar to below table:


Node 1
Node 2
Node 3
Node 4
0
1
0
0
1
2
2
1
2
3
3
3
4
5
4
4
5
6
6
5
6
7
7
7
  


Lets start

 At the beginning download Alfresco Search Services - Solr 6 (https://download.alfresco.com/cloudfront/release/community/201806-GA-build-00113/alfresco-search-services-1.1.1.zip)  

Then unzip archive and copy them to the four separate folders












Go to each Solr and run the instance:
·         Solr start -p 8091
·         Solr start -p 8092
·         Solr start -p 8093
·         Solr start -p 8094



Next call the configuration using URL requests:






Each request should present response similar to below output:


The default index sharding method is DB_ID. You can read more about available methods here: https://docs.alfresco.com/6.0/concepts/solr-shard-approaches.html 
It is necessary to check Solrcore.properties  






Next step is to switch to Alfresco application and configure 
alfresco-global.properties to use previously created Solr Nodes















The results

It is necessary to test our new configuration. Lets add a few new documents to Alfresco using Share





























Lets examine our indexes in Solr


















































There are created cores and indexes. 
 
So, everything works as we want to :)

Thursday, May 7, 2020

Alfresco - new project

Alfresco - new project

Objectives

Alfresco is ECM (Enterprise Content Management) - the system to store, manage and care about documents in their full live cycle. I've prepared high level conceptual diagram but the I won't describe the details because there are exists a lot of pages like i.e. https://docs.alfresco.com/6.1/concepts/welcome.html or Jeff Potts blog https://ecmarchitect.com/

     


The main goal is to create new project - add data model with fields, new forms to visualize new attributes and new search form


Lets start

I based on Jeff's tutorial and I prepared my own functionality. I added below prepared screens:

  • Search Form:
  


  • Edit form:




  • Attributes panel:



Preparation of the functionality

At the beginning you have to use Alfresco SDK and create new project from archtype:
https://docs.alfresco.com/5.1/tasks/alfresco-sdk-tutorials-all-in-one-archetype.html


I added my own model like below (content-model.xml):

















If you create new model you have to register it in bootstrap-context.xml:














It is necessary to register new forms and fields in share-config-custom.xml:






















Last element is very important. It is necessary to cast object to previously created model and create new rule.












That's all! :)

Saturday, December 29, 2018

Security - Ciphers


Introduction


Today I'm going to briefly present you a topic witch points to part of security - the ciphers. At the beginning I'll show you main classification of the ciphers and next in separately chapters will describe each kind of.





As You can see there is three main type of ciphers:

  1. Simple
  2. Asymmetric
  3. Symmetric

 Simple ciphers

Substitution ciphers generally replace each character with other one or some groups of charactes are replaced with other characrer or groups of characters. Depending of compexity and advanced of ciphers one character could be replaced by single character or character wich belongs to defined list of elements.

Transposition ciphers based on changing position of the characters. There could be build appropriate matrix where number of columns could depend on secret key. Changing the order of the columns generate unreadable text for the others.

Modern ciphers are mix of Substitution and Transposition ciphers. Lets see next categories.



Asymmetric ciphers

Asymmetric cryptography is kind of cryptography wich base on private and public key. One key is used to encrypt a message and the other key is used to decrypt the message. Is not possible to execute both operation encryption and decryption using one key. Usually is recommended to use public key for encryption so only owner of the private key can decrypt the message.
Encryption algorithm should be used with padding PKCS#1 or OEAP because of security.

The asymmetric algorithms:

  • RSA
  • Diffie–Hellman (one of the key-agreement algorithms)


Below is example of using RSA algorithm with OAEP padding. I use bouncycastle library (https://www.bouncycastle.org/)



package security;

import java.security.InvalidKeyException;
import java.security.Key;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
import java.security.SecureRandom;
import java.security.Security;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;

/**
 *
 * @author Artur
 */
public class AsyncCipher {
    private static final String incommingMessage = "message for encryption";
   
    public static void main(String[] args)
            throws InvalidKeyException, NoSuchAlgorithmException,
            IllegalBlockSizeException, BadPaddingException,
            NoSuchProviderException, NoSuchPaddingException {
       
        long startTime = System.currentTimeMillis();
        asyncCipher(incommingMessage);
        long endTime = System.currentTimeMillis();
        System.out.println("Execution time: " + ((endTime - startTime)) + " milliseconds");
    } 
   
    public static void asyncCipher(final String inMessage)
            throws InvalidKeyException, NoSuchAlgorithmException,
            IllegalBlockSizeException, BadPaddingException,
            NoSuchProviderException, NoSuchPaddingException {
       
        Security.addProvider(new org.bouncycastle.jce.provider.BouncyCastleProvider());

        byte[] message = inMessage.getBytes();
        Cipher rsaOaep = Cipher.getInstance("RSA/None/OAEPWithSHA1AndMGF1Padding", "BC");
        SecureRandom sr = new SecureRandom();
        KeyPairGenerator generator = KeyPairGenerator.getInstance("RSA", "BC");

        generator.initialize(2048, sr);

        KeyPair pair = generator.generateKeyPair();
        Key pubKey = pair.getPublic();
        Key privKey = pair.getPrivate();

        rsaOaep.init(Cipher.ENCRYPT_MODE, pubKey, sr);        
                    long startTime = System.currentTimeMillis();
                    byte[] encryptedMessage = rsaOaep.doFinal(message);
                    long endTime = System.currentTimeMillis(); 
                    System.out.println("Encryption Execution Time: " + ((endTime - startTime)) + " milliseconds; Encrypted                                message: ".concat(Base64.encodeBase64String(encryptedMessage)));

        rsaOaep.init(Cipher.DECRYPT_MODE, privKey);
        byte[] decryptedMessage = rsaOaep.doFinal(encryptedMessage);
        System.out.println("Decrypted message : ".concat(new String(decryptedMessage))); 
    }
}

Output:

Encryption Execution Time: 1 milliseconds; Encrypted message: Xby9krU99bBxdtr3ea+0tSlP0j7IySiysTmT4PTc/e2L6yIsulnagsrWYNRm59q1Vpt1fOZFJA/7IO+hwngY2ghWihhcGFRfrtPqzWp5Xc6afhg1u4iHO9RhOQc+s2Kfvg+4O7/+zNuCL4HTB4wkEdMS3qU4MPGtaD1o+MygsjPFJJoJZ/ZBn5wbIttjQGpE+Bp6ECQL70D9X8RGyYuuGvaPK9csP7ENMCCmtk0G0uPy7AKpe77A5xEOfUC9K68+af4XMHXeg+0PuhHVlsVSCG972cqlCz1vmKayumnDyzbK/eARrDL3xW54RqVAQlWQgeolcEHzapKcAc4qWuLf9Q==
Decrypted message : message for encryption
Execution time: 1584 milliseconds


I mentioned about key-agreement algorithms . This is base functionality used in SSL/TLS protocols.

Symmetric ciphers

Symmetric ciphers use the same key for encryption and decryption. There are two types of symmetric ciphers - stream and block ciphers. Below is attached diagram with the structure of symmetric ciphers.




Block ciphers split plain text in blocks which have the same size and next encrypt them one by one. Algorithm AES with CBC mode is able to mix data from different blocks. Stream ciphers work on stream of plaintext data and they do not divide it into smaller blocks.


Below is example of using AES algorithm with CBC mode. For authenticated encryption I recommend to use GCM mode.


package security;

import com.google.api.client.repackaged.org.apache.commons.codec.binary.Base64;
import java.io.UnsupportedEncodingException;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import org.bouncycastle.util.encoders.Hex;


/**
 *
 * @author Artur
 */
public class SyncCipher {
     private static final String incommingMessage = "message for encryption";
    
    public static void main(String[] args) 
            throws InvalidKeyException, NoSuchAlgorithmException, 
            IllegalBlockSizeException, BadPaddingException, 
            NoSuchProviderException, NoSuchPaddingException, InvalidAlgorithmParameterException, UnsupportedEncodingException {
        
        long startTime = System.currentTimeMillis();
        syncCipher(incommingMessage);
        long endTime = System.currentTimeMillis();
        System.out.println("Total Execution Time: " + ((endTime - startTime)) + " milliseconds");
    }  
    
    public static void syncCipher(final String inMessage) 
            throws InvalidKeyException, NoSuchAlgorithmException, 
            IllegalBlockSizeException, BadPaddingException, 
            NoSuchProviderException, NoSuchPaddingException, InvalidAlgorithmParameterException, UnsupportedEncodingException {
        
        java.security.Security.addProvider(new org.bouncycastle.jce.provider.BouncyCastleProvider());

        IvParameterSpec iv = new IvParameterSpec("myEncIntVector_1".getBytes("UTF-8"));
        SecretKeySpec skeySpec = new SecretKeySpec("myEncryptionKey1".getBytes("UTF-8"), "AES");

        Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5PADDING");
        cipher.init(Cipher.ENCRYPT_MODE, skeySpec, iv);
        long startTime = System.currentTimeMillis();
        byte[] encryptedMessage = cipher.doFinal(inMessage.getBytes());  
        long endTime = System.currentTimeMillis();
        System.out.println("Encryption Execution Time: " + ((endTime - startTime)) + " milliseconds; Encrypted message: ".concat(Base64.encodeBase64String(encryptedMessage)));

        cipher.init(Cipher.DECRYPT_MODE, skeySpec, iv);
        byte[] decryptedMessage = cipher.doFinal(encryptedMessage);
        System.out.println("Decrypted message : ".concat(new String(decryptedMessage)));  
    }
}


Encryption Execution Time: 0 milliseconds; Encrypted message: 5YWJup+o7SRd3o1rtqS1OtTCx7RpY8og8WDBZVXRNZM=
Decrypted message : message for encryption
Total Execution Time: 339 milliseconds


Tuesday, November 6, 2018

XML Schema

Objectives


Today I'm going to introduce you in subject of creating, visualization and using XSD. XML Schema was created by W3C for creating XML structure definition. This is a kind of contract which could be used in many solutions. One of the best usages is creating data model in WSDL (Web service contract)

Below is personal data's XSD structure which contains simple and complex types. There are diverse types of restrictions:

  • simple:  minOccurs, maxOccurs
  • complex: <xsd:restriction base="xsd:string"> 

There is also example of inheritance:  <xsd:extension base="as:document">

<?xml version="1.0" encoding="UTF-8"?>
<xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema"
            xmlns:as="https://www.linkedin.com/in/artur-scigajlo-9144092/"
            targetNamespace="https://www.linkedin.com/in/artur-scigajlo-9144092/"
            xmlns:iso639-2="http://lcweb.loc.gov/standards/iso639-2/"
            elementFormDefault="qualified">
           
    <xsd:import namespace="http://lcweb.loc.gov/standards/iso639-2/"  schemaLocation="http://files.dnb.de/standards/xmetadiss/iso639-2.xsd"/>
               
               
   <xsd:complexType name="personalData">
        <xsd:sequence>
            <xsd:element name="workerId" type="xsd:string" minOccurs="1" maxOccurs="1"/>
            <xsd:element name="worker" type="as:worker" minOccurs="1" maxOccurs="1"/>
            <xsd:element name="partA" type="as:documentPartA" minOccurs="1" maxOccurs="unbounded"/>
            <xsd:element name="partB" type="as:documentPartB" minOccurs="0" maxOccurs="unbounded"/>
        </xsd:sequence>
    </xsd:complexType>
             
  <xsd:complexType name="worker">
        <xsd:sequence>
            <xsd:element name="lastName" type="xsd:string" minOccurs="1" maxOccurs="1"/>
            <xsd:element name="firstName" type="xsd:string" minOccurs="1" maxOccurs="1"/>
            <xsd:choice>
                <xsd:element name="personId">
                   <xsd:simpleType>
                    <xsd:restriction base="xsd:string">
                      <xsd:minLength value="10"/>
                      <xsd:maxLength value="26"/>
                    </xsd:restriction>
                  </xsd:simpleType>
                </xsd:element>
                <xsd:element name="otherTypeOfDocument">
                    <xsd:complexType>
                        <xsd:sequence>
                          <xsd:element name="type" type="xsd:string" minOccurs="1" maxOccurs="1"/>
                          <xsd:element name="number" type="xsd:string" minOccurs="1" maxOccurs="1"/>
                        </xsd:sequence>
                    </xsd:complexType>
                </xsd:element>
            </xsd:choice>
        </xsd:sequence>
    </xsd:complexType>
   
        <xsd:complexType name="document">
            <xsd:sequence>
                <xsd:element name="documentId" type="xsd:string" minOccurs="1" maxOccurs="1"/>
                <xsd:element name="title" type="as:title" minOccurs="0" maxOccurs="1"/>
                <xsd:element name="createdDate" type="xsd:date" minOccurs="1" maxOccurs="1"/>
                <xsd:element name="documentType" type="as:documentType" minOccurs="0" maxOccurs="1"/>
            </xsd:sequence>
    </xsd:complexType>
   
       <xsd:complexType name="title">
        <xsd:simpleContent>
            <xsd:extension base="xsd:string">
                <xsd:attribute name="language" type="iso639-2:RegisteredCodeType" use="optional"/>
            </xsd:extension>
        </xsd:simpleContent>
    </xsd:complexType>
   
      <xsd:complexType name="documentType">
        <xsd:sequence>
            <xsd:element name="type" minOccurs="1" maxOccurs="1">
              <xsd:simpleType>
                <xsd:restriction base="xsd:string">
                  <xsd:enumeration value="driverLicence"/>
                  <xsd:enumeration value="passport"/>
                  <xsd:enumeration value="insuranceNo"/>
                  <xsd:enumeration value="other"/>
                </xsd:restriction>
              </xsd:simpleType>
            </xsd:element>
            <xsd:element name="numer" type="xsd:string" minOccurs="1" maxOccurs="1"/>
        </xsd:sequence>
      </xsd:complexType>
   
    <xsd:complexType name="documentPartA">
        <xsd:complexContent>
            <xsd:extension base="as:document">
                <xsd:attribute name="subType" use="required">
                    <xsd:simpleType>
                        <xsd:restriction base="xsd:string">
                          <xsd:pattern value="^[ABC]\d{2}$"/>
                        </xsd:restriction>
                    </xsd:simpleType>
                </xsd:attribute>
            </xsd:extension>
        </xsd:complexContent>
    </xsd:complexType>
   
    <xsd:complexType name="documentPartB">
        <xsd:complexContent>
            <xsd:extension base="as:document">
                <xsd:attribute name="subType" use="required">
                    <xsd:simpleType>
                        <xsd:restriction base="xsd:string">
                          <xsd:pattern value="^[XYZ]\d{2}$"/>
                        </xsd:restriction>
                    </xsd:simpleType>
                </xsd:attribute>
            </xsd:extension>
        </xsd:complexContent>
    </xsd:complexType>   

</xsd:schema>




Data model visualization

There are existing useful applications to help creating XSD. Some of them have a plugins to generate visualization diagrams. Below is our schema's diagram.





Genarating XML file base on XSD Schema


I created project in Netbeans.





I imported XSD and generated java classes automatically.







We can see the generated classes





The main class is used to generate XML file









Below is java class

/*
 * To change this license header, choose License Headers in Project Properties.
 * To change this template file, choose Tools | Templates
 * and open the template in the editor.
 */
package artur.scigajlo;

import artur.scigajlo.personalData.DocumentPartA;
import artur.scigajlo.personalData.DocumentType;
import artur.scigajlo.personalData.PersonalData;
import artur.scigajlo.personalData.Title;
import artur.scigajlo.personalData.Worker;
import artur.scigajlo.personalData.Worker.OtherTypeOfDocument;
import com.sun.org.apache.xerces.internal.jaxp.datatype.XMLGregorianCalendarImpl;
import java.io.File;
import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBException;
import javax.xml.bind.Marshaller;
import javax.xml.bind.PropertyException;

/**
 *
 * @author a.scigajlo
 */
public class PersonalDataProject {

    /**
     * @param args the command line arguments
     */
    public static void main(String[] args) throws PropertyException, JAXBException {
              PersonalData ao = new PersonalData();
        ao.setWorkerId("worker_01");
     
        Worker worker = new Worker();
        worker.setFirstName("Jon");
        worker.setLastName("Black");
     
        OtherTypeOfDocument otherDoc = new OtherTypeOfDocument();
        otherDoc.setNumber("AAA123456");
        otherDoc.setType("otherPersonId"); 
        worker.setOtherTypeOfDocument(otherDoc);     
     
        ao.setWorker(worker);
     
     
        DocumentPartA docA1 = new DocumentPartA();
        docA1.setCreatedDate(XMLGregorianCalendarImpl.createDate(2018,3,4,0));
        docA1.setDocumentId("docID-714");
     
        Title t1 = new Title();
        t1.setLanguage("pol");
        t1.setValue("hello.pdf");
        docA1.setTitle(t1);
     
        DocumentType dt1 = new DocumentType();
        dt1.setNumer("AEC12");
        dt1.setType("driverLicence");
        docA1.setDocumentType(dt1);
     
        ao.getPartA().add(docA1);
     
        DocumentPartA docA2 = new DocumentPartA();
        docA2.setCreatedDate(XMLGregorianCalendarImpl.createDate(2018,5,14,0));
        docA2.setDocumentId("annexID-318");
     
        Title t2 = new Title();
        t2.setLanguage("pol");
        t2.setValue("test.pdf");
        docA2.setTitle(t2);
     
        DocumentType dt2 = new DocumentType();
        dt2.setNumer("AEC12");
        dt2.setType("driverLicence");
        docA2.setDocumentType(dt2);
     
        ao.getPartA().add(docA2);
         
        JAXBContext jaxbContext = JAXBContext.newInstance(PersonalData.class);
        Marshaller jaxbMarshaller = jaxbContext.createMarshaller();
        jaxbMarshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true);
     
     
        //ZAPIS DO PLIKU
        jaxbMarshaller.marshal(ao, new File("c:\\tmp\\PersonalData.xml"));
    }
 
}


Finally we get XML file




Thursday, August 16, 2018

LinkedList custom implementation in Java

Objectives


Today i'm going to introduce you in my custom LinkedList implementation.Below is prepared diagram which shows internal class's structure. Every Node handles reference to next Node. The last Node has an empty reference. There are two fields which hold reference to first and last Nodes.




Below is internal Node's structure which holds items and references.


private class Node<T extends Object>{
        Node<T> next;
        T object;
     
        public Node(T object){
            this.next = null;
            this.object = object;
        }
     
        public Node(T object, Node<T> next){
            next.setNext(this);
            this.next = null;
            this.object = object;
        }

        public Node<T> getNext() {
            return next;
        }

        public void setNext(Node<T> next) {
            this.next = next;
        }

        public T getObject() {
            return object;
        }

        public void setObject(T object) {
            this.object = object;
        }     
     
    }

Thursday, July 19, 2018

HashMap custom implementation

Objectives


HashMap is array of buckets to store linkedList's simple structure. Each linkedList consists of agregated in key-value ernty objects with additional references to next entry object. Very important is to create method to point correct Bucket.

Below is diagram which shows inserting entries to HashMap process.




Entry structure


Below is entry structure which implements Map.Entry interface.


    static class Entry<K,V> implements Map.Entry<K,V> {
        private K key;
        private V value;
        private Entry<K,V> next;
        public Entry(K key, V value){
            this.key = key;
            this.value = value;       
        }
        public Entry(K key, V value, Entry<K,V> next){
            this.key = key;
            this.value = value;
            this.next = next;
        }
        @Override
        public K getKey() {
            return key;
        }
        @Override
        public V getValue() {
            return value;
        }
        @Override
        public V setValue(V v) {
            this.value = v;
            return this.value;
        }
        public Entry<K, V> getNext() {
            return next;
        }
        public void setNext(Entry<K, V> next) {
            this.next = next;
        }         
    }


Bucket

Very important aspect is to create appropriate method to calculate bucket. The method has only one incoming parameter - the calculated hashCode base on key.

public static int calculateIndex(int hash) {
        return hash & (capacity-1);
    }

Main Class

Below is full source code.  
package artsci.pl;

import java.util.Map;

/**
 *
 * @author Artur
 */
public class MyHashMap <K, V> {
    private Entry<K,V>[] table;
    private final static int capacity = 16;

    public MyHashMap(){
       table = new Entry[capacity];
    }


    public void put(K key, V value){
       if(key==null)
           return;
   
       int hash=key.hashCode();
       int index = calculateIndex(hash);
   
       if(table[index] == null){
           table[index] = new Entry(key, value);
       }else{
           boolean isNodeExists = true;
           Entry currentNode = table[index];
       
           while (currentNode != null && isNodeExists) {
                if(currentNode.getKey().hashCode() == hash && key.equals(currentNode.getKey())){
                    currentNode.setValue(value);
                    break;
                }           
           
                if(currentNode.getNext() != null){
                    currentNode = currentNode.getNext();
                }else{
                    isNodeExists = false;
                    table[index] = new Entry(key, value, table[index]);
                }
            }       
       }
    }

   

    public V get(K key){
       if(key==null)
           return null;
   
       int hash=key.hashCode();
       int index = calculateIndex(hash);
       boolean isNodeExists = true;
       V out = null;
   
       if(table[index] != null){
           Entry currentNode = table[index];
   
           while (currentNode != null && isNodeExists) {
                if(currentNode.getKey().hashCode() == hash && key.equals(currentNode.getKey())){
                    out = (V)currentNode.getValue();
                }           
           
                if(currentNode.getNext() != null){
                    currentNode = currentNode.getNext();
                }else{
                    isNodeExists = false;
                }
            }   
       }
       return out;
    }

    public static int calculateIndex(int hash) {
        return hash & (capacity-1);
    }

    public String toString(){
        String out = "";           
        for(Entry currentNode : table){
            boolean isNodeExists = true;       
       
            while (currentNode != null && isNodeExists) {
                out += "["+currentNode.getKey()+";"+currentNode.getValue()+"]";
                if(currentNode.getNext() != null){
                    currentNode = currentNode.getNext();
                }else{
                    isNodeExists = false;
                }
            }   
        }   
        return out;
    }

    public int size(){
        int out = 0;       
        for(Entry currentNode : table){
            boolean isNodeExists = true;       
       
            while (currentNode != null && isNodeExists) {
                out++;           
                if(currentNode.getNext() != null){
                    currentNode = currentNode.getNext();
                }else{
                    isNodeExists = false;
                }
            }         
        }           
        return out;
    }


    static class Entry<K,V> implements Map.Entry<K,V> {
        private K key;
        private V value;
        private Entry<K,V> next;

        public Entry(K key, V value){
            this.key = key;
            this.value = value;         
        }

        public Entry(K key, V value, Entry<K,V> next){
            this.key = key;
            this.value = value;
            this.next = next;
        }

        @Override
        public K getKey() {
            return key;
        }

        @Override
        public V getValue() {
            return value;
        }

        @Override
        public V setValue(V v) {
            this.value = v;
            return this.value;
        }

        public Entry<K, V> getNext() {
            return next;
        }

        public void setNext(Entry<K, V> next) {
            this.next = next;
        }   
   
    }
}

Test Class

package artsci.pl;

import org.junit.Test;
import static org.junit.Assert.*;

/**
 *
 * @author Artur
 */
public class MyHashMapTest {

    public MyHashMapTest() {
    }

    @Test
    public void testMethod() {
        MyHashMap <String, String> map = new MyHashMap <String, String>();
   
        for(int i = 0; i< 30; i++){   
            String key = "KEY_"+i;
            map.put(key, "VALUE_".concat(String.valueOf(i)));
            System.out.println("Key: "+key+" is in bucket: "+MyHashMap.calculateIndex(key.hashCode()) );       
        }           
   
        System.out.println("Size: "+map.size());   
        assertTrue(map.size() == 30);
   
    }

}


And the result is below:

Testsuite: artsci.pl.MyHashMapTest
Key: KEY_0 is in bucket: 0
Key: KEY_1 is in bucket: 1
Key: KEY_2 is in bucket: 2
Key: KEY_3 is in bucket: 3
Key: KEY_4 is in bucket: 4
Key: KEY_5 is in bucket: 5
Key: KEY_6 is in bucket: 6
Key: KEY_7 is in bucket: 7
Key: KEY_8 is in bucket: 8
Key: KEY_9 is in bucket: 9
Key: KEY_10 is in bucket: 15
Key: KEY_11 is in bucket: 0
Key: KEY_12 is in bucket: 1
Key: KEY_13 is in bucket: 2
Key: KEY_14 is in bucket: 3
Key: KEY_15 is in bucket: 4
Key: KEY_16 is in bucket: 5
Key: KEY_17 is in bucket: 6
Key: KEY_18 is in bucket: 7
Key: KEY_19 is in bucket: 8
Key: KEY_20 is in bucket: 14
Key: KEY_21 is in bucket: 15
Key: KEY_22 is in bucket: 0
Key: KEY_23 is in bucket: 1
Key: KEY_24 is in bucket: 2
Key: KEY_25 is in bucket: 3
Key: KEY_26 is in bucket: 4
Key: KEY_27 is in bucket: 5
Key: KEY_28 is in bucket: 6
Key: KEY_29 is in bucket: 7
Size: 30
Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0,071 sec