Thursday, May 28, 2020

Token JWT and Spring Boot

Objectives

Today I would like to extend my previous post (https://java-architect.blogspot.com/2020/05/spring-boot-rest-api-with-security.html) and add protection API based on JWT Token. At the beginning I would like to briefly describe the idea of tokens. Below is added BPMN process flow which shows the end-to-end path (this is a simple process flow without filters, authentication controllers or authentication managers)












The token structure
Token consists of three elements:
  • Header (algorithm and type) - {"alg":"HS256"}
  • Payload  - {"sub":"user","exp":1590655192}
  • Signature (defined secret was used to create signature)


Application

I created application based on my previous post (https://java-architect.blogspot.com/2020/05/spring-boot-rest-api-with-security.html).  The main goal is to create JWT token to protect communication between server and clients. 




I selected files which I added or replaced. Let's examine the application's code. 


pom.xml

  <dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<scope>test</scope>
</dependency>

<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-config</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-web</artifactId>
</dependency>

<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-core</artifactId>  
</dependency>

<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-openid</artifactId>  
</dependency>

<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-ldap</artifactId>  
</dependency>


<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>  
</dependency>


<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt</artifactId>
    <version>0.9.1</version>
</dependency>



UserDTO:

@Data
public class UserDTO {
    private String username;
    private String password;
}



SecurityConfig.class


@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
        public static final String prefix = "Bearer";
public static final String header = "Authorization";
        public static final String secret = Base64.getEncoder().encodeToString("artsci".getBytes());;
        public static final Long expir = new Long(3600000);

@SuppressWarnings("deprecation")
@Bean
    public UserDetailsService userDetailsService() {
        User.UserBuilder users = User.withDefaultPasswordEncoder();
        InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
        manager.createUser(users.username("user").password("user").roles("USER").build());
        manager.createUser(users.username("admin").password("admin").roles("USER", "ADMIN").build());
        return manager;
    }

   @Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
         auth.userDetailsService(userDetailsService());
}
   
    @Override
    protected void configure(HttpSecurity http) throws Exception {
     
        http.csrf().disable()   
        .authorizeRequests()
        .antMatchers(HttpMethod.POST, "/login").permitAll()
        .anyRequest().authenticated()
        .and().addFilterBefore(new LoginFilter("/login", authenticationManager()),   
                 UsernamePasswordAuthenticationFilter.class)
        .addFilterBefore(new AuthJwtFilter(), UsernamePasswordAuthenticationFilter.class);
        http.headers().cacheControl();
    }


LoginFilter.class

public class LoginFilter extends AbstractAuthenticationProcessingFilter {
    public LoginFilter(String url, AuthenticationManager authenticationManager) {
        super(new AntPathRequestMatcher(url));
        setAuthenticationManager(authenticationManager);
   }

   @Override

public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse
           response)
throws AuthenticationException, IOException, ServletException {
    UserDTO user = new ObjectMapper().readValue(request.getInputStream(), UserDTO.class);
     UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(
                                                                                                 user.getUsername(),
                                                                                                 user.getPassword(),
                                                                                                 Collections.emptyList());

     return getAuthenticationManager().authenticate(token);
}

   @Override

protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response,
          FilterChain chain,
Authentication authResult) throws IOException, ServletException {

     String token = Jwts.builder()
            .setSubject(authResult.getName())         
            .signWith(SignatureAlgorithm.HS256, secret)
            .setExpiration(new Date(System.currentTimeMillis() + expir))
            .compact();
     response.addHeader(header, prefix + " " + token);    
     response.setContentType("application/json");
     response.setCharacterEncoding("UTF-8");
     response.getWriter().write("{\"" + header + "\":\"" + prefix + " " + token + "\"}");     
}
}

AuthJwtFilter.class

public class AuthJwtFilter extends GenericFilterBean{
@Override
public void doFilter(ServletRequest request, ServletResponse response,
                             FilterChain chain) throws IOException, ServletException {
Authentication auth = null;
         if(!((HttpServletRequest) request).getHeader(header).isEmpty()) {
String claim = ((HttpServletRequest)request).getHeader(header) 
                             .replace(prefix,"").substring(1);
String user = Jwts.parser().setSigningKey(secret).parseClaimsJws(claim)
                        .getBody().getSubject();
auth = new UsernamePasswordAuthenticationToken(user, null,                                      
                             Collections.emptyList());

}
       
         SecurityContextHolder.getContext().setAuthentication(auth);
         chain.doFilter(request,response);
}
}

The results

So, our application should work correctly. Let's try to call protected API. At the beginning it is necessary to to generate token. To achieve this goal I attempt to use URL:  "/login" and pass my credential. Below is example of CURL's request.


curl --location --request POST http://localhost:8080/login --header "Content-Type: application/json" --data-raw "{\"username\":\"user\",\"password\":\"user\"}"
{"Authorization":"Bearer eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJ1c2VyIiwiZXhwIjoxNTkwNjU1MTkyfQ.lKZMYWVKd9OZu2nOwMjRxXjewQ-zYKQWWB4wIp1Zhi8"}

Fantastic, JWT Token was generated. Let's try to call "/regions" service using previously generated token.


curl -H "Authorization":"Bearer eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJ1c2VyIiwiZXhwIjoxNTkwNjU1MTkyfQ.lKZMYWVKd9OZu2nOwMjRxXjewQ-zYKQWWB4wIp1Zhi8" --request GET http://localhost:8080/regions/
[{"regionId":1,"name":"Europe"},{"regionId":2,"name":"Americas"},{"regionId":3,"name":"Asia"},{"regionId":4,"name":"Middle East and Africa"}]

It seems that the solution works exactly as I expected :)

Wednesday, May 27, 2020

Spring Security with REST API

Spring Security with REST API 

Objectives

Today I would like to dive into security topic in Spring boot application. I base on my previous post (https://java-architect.blogspot.com/2020/05/spring-boot-rest-api-with-jpa.html) with the sources. I'm going to change and add protection to the API. There are two important aspect:

  • Authentication - process of identifying the user who calls the API
  • Authorization -  process of checking user's permission to call resources

The Authentication process can base on plain text password, digest method, JWT (Java WEB Token), OAuth, SAML or other method to identify user. Besides previously mentioned methods, very often applications are protected by certificates.


The simplest way to authenticate 

So, due the topic, the simplest way to enable user authentication is to add the appropriate configuration.



























For simplification I defined users and roles in memory. Of course in production environment that data should be fetched form LDAP, Data Base or other identity server.. 


package com.main.artsci.configutarion;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpMethod;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
       @SuppressWarnings("deprecation")
       @Bean
       public UserDetailsService userDetailsService() {
              User.UserBuilder users = User.withDefaultPasswordEncoder();
              InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
              manager.createUser(users.username("user").password("user").roles("USER").build());
              manager.createUser(users.username("admin").password("admin").roles("USER", "ADMIN").build());
               return manager;
        }
@Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception { auth .userDetailsService(userDetailsService());
}
@Override
protected void configure(HttpSecurity http) throws Exception { http.authorizeRequests()
.antMatchers(HttpMethod.GET, "/regions/complex/**").hasRole("ADMIN")
.anyRequest()
.authenticated()
.and().httpBasic();
}
}

In "userDetailsService()" method I defined all necessary users for this test case. In "configure()" method I enabled authentication for every request and all users can have access to all resources except resource defined by path "/regions/complex/" where access has only administrator.

In addition I created SecurityWebApplicationInitializer 

public class SecurityWebApplicationInitializer
   extends AbstractSecurityWebApplicationInitializer {
}

Let's see the pom's changes. I added security libraries:

<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-config</artifactId>
     </dependency>
     <dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-web</artifactId>
      </dependency>
<dependency>
        <groupId>org.springframework.security</groupId>
        <artifactId>spring-security-core</artifactId>  
</dependency>
<dependency>
        <groupId>org.springframework.security</groupId>
        <artifactId>spring-security-openid</artifactId>  
</dependency>
<dependency>
        <groupId>org.springframework.security</groupId>
        <artifactId>spring-security-ldap</artifactId>  
</dependency>

  
Finally it is necessary to checks is requested resources are protected in a proper way.  

Let's see the results after fetching resource available to everyone who pass true the authentication process.
curl --user user:user http://localhost:8080/regions
[{"regionId":1,"name":"Europe"},{"regionId":2,"name":"Americas"},{"regionId":3,"name":"Asia"},{"regionId":4,"name":"Middle East and Africa"}]
The results are exactly we expect to achieve. Next what does happen if we try to fetch protected data? 
curl --user user:user http://localhost:8080/regions/complex/1
{"timestamp":"2020-05-27T07:20:00.487+0000","status":403,"error":"Forbidden", "message":"Forbidden","path":"/regions/complex/1"} 

The error occurs. There is no permission. Let's change user to admin.

curl --user admin:admin http://localhost:8080/regions/complex/1
{"regionId":1,"name":"Europe","countries":[{"countryId":"BE","name":"Belgium"},{"countryId":"CH","name":"Switzerland"},{"countryId":"DE","name":"Germany"},{"countryId":"DK","name":"Denmark"},{"countryId":"FR","name":"France"},{"countryId":"IT","name":"Italy"},{"countryId":"NL","name":"Netherlands"},{"countryId":"UK","name":"United Kingdom"}]}
And everything is correct :)

Monday, May 25, 2020

Spring boot REST API with JPA

Spring boot REST API with JPA

Objectives 

The main goal is to create small application which could evolve to microservice. The application gather data from Oracle database then expose  that data by REST service with JSON objects. There were used a few additional libraries:

  • HikariCP - additional library to manage connection pool to database    
  • Swagger - libraries to create documentation in OpenAPI standard
  • Lombok - library to reduce boilerplate code in java classes


Below is the structure of application:














































The application's code 

The model

Based on Oracle database image which was created in my previous post (https://java-architect.blogspot.com/2020/05/oracle-plsql-part-1.html) I build selected java Entities. Lombok library was very helpful to significantly reduce boilerplate java code. It was necessary to use annotation "@JsonManagedReference" and "@JsonBackReference" for protecting against getting an errors.


The Region class:
@Getter
@Setter
@ToString
@EqualsAndHashCode
@Entity
@Table(name = "REGIONS")
public class Region  {
    @Id
    @Column(name="REGION_ID")
    private Long regionId;
 
    @NotNull
    @Column(name = "REGION_NAME")
    private String name;
 
    @OneToMany(fetch = FetchType.LAZY, mappedBy = "region")
    @JsonManagedReference
    private List<Country> countries;
 
    public Region(){
        }     
}

The Country class:
@Getter
@Setter
@ToString
@EqualsAndHashCode
@Entity
@Table(name = "COUNTRIES")
public class Country {
    @Id
    @Column(name="COUNTRY_ID")
    private String countryId;
 
    @Column(name = "COUNTRY_NAME")
    private String name; 
 
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "REGION_ID", nullable = false)
    @JsonBackReference
    private Region region;
 
    public Country() {
        }
}

The repository

@Repository
public interface RegionRepository extends CrudRepository<Region, Long> { Optional<Region> findById(Long id);
}

@Repository
public interface CountryRepository extends CrudRepository<Country, String>{
    Optional<Country> findById(String countryId);   

    @Query("select c from Country c where c.name = :name")
           Stream<Country> findByNameReturnStream(@Param("name") String name);
}


The service

@Transactional
@Service("regionService")
public class RegionServiceImpl implements RegionService{
@Autowired
CountryRepository countryRepository;

@Autowired
RegionRepository regionRepository;

public List<RegionDTO> getRegions() {
                List<RegionDTO> list = new ArrayList<RegionDTO>();

for(Region r : regionRepository.findAll()) {
list.add(new RegionDTO(r));
}
return list;
}

        public RegionDTO getRegionByIdv1(Long id) {
return new RegionDTO(regionRepository.findById(id).get());

}

        public RegionDTOv2 getRegionByIdv2(Long id) {

Region region = regionRepository.findById(id).get();
region.getCountries();
return  new RegionDTOv2(region);
}
}


The RestController

@RestController
@EnableSwagger2
public class BaseController  {
@Autowired
@Qualifier("regionService")
RegionService regionService;

       @RequestMapping(method = RequestMethod.GET, value = "/regions")
        public ResponseEntity<List<RegionDTO>> regions() {           
               return new ResponseEntity<List<RegionDTO>>(regionService.getRegions(), HttpStatus.OK);

        }
 
       @RequestMapping(value = "/regions/simple/{regionId}", method = RequestMethod.GET)
  public ResponseEntity<RegionDTO> getRegionById(@PathVariable("regionId") long regionId) {
RegionDTO regionDTO = regionService.getRegionByIdv1(regionId);
return new ResponseEntity<RegionDTO>(regionDTO, HttpStatus.OK);
}
 
        @RequestMapping(value = "/regions/complex/{regionId}", method = RequestMethod.GET)
public ResponseEntity<RegionDTOv2> getRegionByIdV2(@PathVariable("regionId") long             regionId) {
    RegionDTOv2 regionDTOv2 = regionService.getRegionByIdv2(regionId);
    return new ResponseEntity<RegionDTOv2>(regionDTOv2, HttpStatus.OK);
}
}


Application.properties

#spring boot configuration for Oracle
spring.datasource.url=jdbc:oracle:thin:@localhost:49161:xe
spring.datasource.username=hr
spring.datasource.password=hr
spring.datasource.driver-class-oracle.jdbc.driver.OracleDriver

#hibernate dialect config
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.Oracle10gDialect
spring.jackson.serialization.fail-on-empty-beans=false
spring.jpa.show-sql=true
spring.datasource.testWhileIdle=true
spring.datasource.validationQuery=SELECT 1

# HikariCP settings
spring.datasource.hikari.*
spring.datasource.hikari.connection-timeout=60000
spring.datasource.hikari.maximum-pool-size=10



The results

Specification

The results can be visualized in web browser. Let's see the API's documentation.





































The output of method "/regions"

The base output of method "/regions" has:

JSON representation:























Data representation:























And the Headers:
































The output of method "/regions/complex/{regionId}"

























What else

The CRUD operations are usually mapped to HTTP method in REST web services like below:

  • CREATE -> POST
  • READ -> GET
  • UPDATE -> PUT
  • DELETE -> DELETE


Tuesday, May 19, 2020

Oracle PL/SQL PART 1

Oracle PL/SQL Part 1

Overview

What is PL/SQL? PL/SQL is a database procedural language which can extend functionality of SQL. It gives to the developers ability to handle events from higher layers of application, executes procedures or functions and store the results in appropriate structures. Many organizations like banks, insurance company etc. uses that language. Some of them still uses old technology like i.e Oracle Forms which can execute also PL/SQL blocks of code.

Preliminary phase

Before we start exploring the PL/SQL functionality we have to prepare our environment.
The simplest way is to use docker image to run Oracle DB.  I found documentation on https://hub.docker.com/r/oracleinanutshell/oracle-xe-11g. Lets try to use that  image.

Lets run the image














Unlock the HR user
















Download SQLDeveloper and check connection

PL/SQL structure


 It is time to start. The structure of PL/SQL block should be similar to below structure.


Header - block of code could be anonymous or named. Anonymous block of code starts from key word "DECLARE" and it is not stored in DB's structures. Named block of code are i.e. functions, procedures, packages of code, triggers, etc.

Declaration section - this section contains definition of data types, structures and variables which could be used to assign references or store calculated values.

Execution section - this section is always required. It consists of execution code which cover business logic.

Exception section - this section is optional and it is appropriate place to handle errors and process them.


Basic anonymous block of code

Lets create simple block of code. I based on HR example schema. The block of code contains an anonymous cursor and loop. In the declaration section are defined variables: counter v_cnt and new table v_tab which base on new definition of type t_tab;    

SET SERVEROUTPUT ON
DECLARE 
    v_cnt PLS_INTEGER :=0;
 
    --declare new type - table of records
    TYPE t_tab IS TABLE OF departments.department_id%TYPE INDEX BY PLS_INTEGER;
 
    --create variable base on table type
    v_tab t_tab;
BEGIN
    DBMS_OUTPUT.PUT_LINE('START');
 
    --create anonymous cursor AND fill the table with records
    FOR dep IN (SELECT department_id FROM departments) LOOP
      v_tab(v_cnt) := dep.department_id;     
      v_cnt := v_cnt + 1;   
    END LOOP;
 
    --inerate over the table and show the departments
    FOR i in v_tab.FIRST .. v_tab.LAST LOOP
        DBMS_OUTPUT.PUT_LINE('DEPARTMENT_ID: '||v_tab(i));         
    END LOOP;
 
    DBMS_OUTPUT.PUT_LINE('STOP');
END;
/

BULK COLLECT

If there was a big number of record I could be used 'BULK COLLECT' statement to have better performance (there is no necessary to switch between SQL and PL/SQL engines so frequently like in cursor statements). Additionally there could be used also Limit statement for limiting returned number of rows - this prevents form consume all available memory in session.

SET SERVEROUTPUT ON
DECLARE     
    --declare new type - table of records
    TYPE t_tab IS TABLE OF departments.department_id%TYPE INDEX BY PLS_INTEGER;
 
    --create variable base on table type
    v_tab t_tab;
BEGIN
    DBMS_OUTPUT.PUT_LINE('START');
 
    --create 'select' statement with 'BULK COLLECT' phrase
    SELECT department_id
    BULK COLLECT INTO v_tab 
    FROM departments;
 
    --inerate over the table and show the departments
    FOR i in v_tab.FIRST .. v_tab.LAST LOOP
        DBMS_OUTPUT.PUT_LINE('DEPARTMENT_ID: '||v_tab(i));         
    END LOOP;
 
    DBMS_OUTPUT.PUT_LINE('STOP');
END;
/

FORALL LOOP

Similar to previous example we can increase performance with updating big number of rows without switching context between SQL and PL/SQL engines. I used additionally SAVEPOINT statement to control how changes will be propagate to persistent layer. 


SET SERVEROUTPUT ON
DECLARE     
    --declare new type - table of records
    TYPE t_tab IS TABLE OF departments.department_id%TYPE INDEX BY PLS_INTEGER;
 
    --create variable base on table type
    v_tab t_tab;
BEGIN
    SAVEPOINT s_dep;
    DBMS_OUTPUT.PUT_LINE('START');
 
    --create 'select' statement with 'BULK COLLECT' phrase
    SELECT department_id
    BULK COLLECT INTO v_tab 
    FROM departments;
 
 
    --update all select attributes in one statement
    FORALL j IN v_tab.FIRST..v_tab.LAST
        UPDATE departments SET department_name = department_name || '(OLD)'
        WHERE department_id = v_tab(j);
 
    --rollback all changes 
    ROLLBACK TO s_dep; 
         
    DBMS_OUTPUT.PUT_LINE('STOP');
END;
/  


CURSORS LOOP

In this example I created named cursor c_cur.  Before fetching rows is necessary to open cursor and after processing is necessary to close this cursor. There are many attributes on cursors:
  • FOUND
  • NOTFOUND
  • ISOPEN
  • ROWCOUNT

SET SERVEROUTPUT ON
DECLARE     
    --declare cursor
    CURSOR c_cur is
        SELECT department_id, department_name
        FROM departments;
 
    --declare row base on cursor
    r_rec c_cur%ROWTYPE;
BEGIN
    DBMS_OUTPUT.PUT_LINE('START');
 
    --open cursor
    OPEN c_cur;
 
    --fetch and process each row from cursor
    LOOP
        FETCH c_cur INTO r_rec;
        EXIT WHEN c_cur%NOTFOUND;
        DBMS_OUTPUT.PUT_LINE('DEP_ID:'||r_rec.department_id||' DEP_NAME:'||r_rec.department_name);
    END LOOP;
 
    --close cursor
    CLOSE c_cur;
         
    DBMS_OUTPUT.PUT_LINE('STOP');
END;
/



EXCEPTIONS

Resilient and exception handling is very important in block of code because if something goes wrong It  would be captured and fixed. In addition that type of event should be logged. Below is simple definition of few errors in the Exception section. Pre-defined exceptions are described in many pages i.e. https://docs.oracle.com/cd/B10501_01/appdev.920/a96624/07_errs.htm

SET SERVEROUTPUT ON
DECLARE     
    v_dep_id departments.department_id%TYPE;
BEGIN
    DBMS_OUTPUT.PUT_LINE('START');
 
    SELECT department_id INTO v_dep_id   
    FROM departments;
         
    DBMS_OUTPUT.PUT_LINE('STOP');
EXCEPTION
    WHEN NO_DATA_FOUND THEN
        DBMS_OUTPUT.PUT_LINE('NO_DATA_FOUND:'||SQLCODE||'; '||SQLERRM);
    WHEN TOO_MANY_ROWS THEN
        DBMS_OUTPUT.PUT_LINE('TOO_MANY_ROWS:'||SQLCODE||'; '||SQLERRM);
    WHEN OTHERS THEN
        DBMS_OUTPUT.PUT_LINE('ERROR:'||SQLCODE||'; '||SQLERRM);         
END;
/

It is possible to define your own simple Exception e_my_own . In addition it is possible to assign error number by using  PRAGMA EXCEPTION_INIT.

SET SERVEROUTPUT ON
DECLARE     
    v_dep_id departments.department_id%TYPE; 
    e_my_own EXCEPTION;
    PRAGMA EXCEPTION_INIT(e_my_own,-777);
BEGIN
    DBMS_OUTPUT.PUT_LINE('START');
 
    SELECT department_id INTO v_dep_id   
    FROM departments where department_id = 10;
 
    RAISE e_my_own;
         
    DBMS_OUTPUT.PUT_LINE('STOP');
EXCEPTION
    WHEN e_my_own THEN
        DBMS_OUTPUT.PUT_LINE('e_my_own:'||SQLCODE||'; '||SQLERRM); 
    WHEN OTHERS THEN
        DBMS_OUTPUT.PUT_LINE('ERROR:'||SQLCODE||'; '||SQLERRM);         
END;
/


WHAT ELSE

In next part I describe Types, procedures, objects, anonymous transactions and cache in functions etc.




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 :)