ac.simons.tweetarchive.tweets.TweetRepositoryImpl.java Source code

Java tutorial

Introduction

Here is the source code for ac.simons.tweetarchive.tweets.TweetRepositoryImpl.java

Source

/*
 * Copyright 2016 michael-simons.eu.
 *
 * 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 ac.simons.tweetarchive.tweets;

import static ac.simons.tweetarchive.db.tables.Tweets.TWEETS;
import java.time.LocalDate;
import java.time.ZoneId;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import javax.persistence.EntityManager;
import javax.persistence.Query;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.lucene.queryparser.classic.ParseException;
import org.apache.lucene.queryparser.classic.QueryParser;
import org.hibernate.search.jpa.FullTextEntityManager;
import org.hibernate.search.jpa.Search;
import org.hibernate.search.query.dsl.BooleanJunction;
import org.hibernate.search.query.dsl.QueryBuilder;
import org.jooq.DSLContext;
import org.jooq.Record;
import org.jooq.SelectQuery;
import org.jooq.conf.ParamType;
import static org.jooq.impl.DSL.field;
import static org.jooq.impl.DSL.name;
import static org.jooq.impl.DSL.select;
import org.springframework.transaction.annotation.Transactional;

/**
 * Implementation of our extended repository.
 *
 * @author Michael J. Simons, 2016-09-06
 */
@RequiredArgsConstructor
@Slf4j
public class TweetRepositoryImpl implements TweetRepositoryExt {

    private static final ZoneId UTC = ZoneId.of("UTC");

    private final EntityManager entityManager;

    private final DSLContext create;

    @Override
    @Transactional(readOnly = true)
    public List<TweetEntity> searchByKeyword(final String keywords, final LocalDate from, final LocalDate to) {
        // Must be retrieved inside a transaction to take part of
        final FullTextEntityManager fullTextEntityManager = Search.getFullTextEntityManager(entityManager);

        // Prepare a search query builder
        final QueryBuilder queryBuilder = fullTextEntityManager.getSearchFactory().buildQueryBuilder()
                .forEntity(TweetEntity.class).get();

        // This is a boolean junction... I'll add at least a keyword query
        final BooleanJunction<BooleanJunction> outer = queryBuilder.bool();
        outer.must(queryBuilder.keyword().onFields("content").matching(keywords).createQuery());

        // And then 2 range queries if from and to are not null
        Optional.ofNullable(from).map(f -> f.atStartOfDay(UTC)) // Must be a zoned date time to fit the field
                .map(f -> queryBuilder.range().onField("created_at").above(f).createQuery())
                .ifPresent(q -> outer.must(q));
        Optional.ofNullable(to).map(f -> f.plusDays(1).atStartOfDay(UTC)) // Same here, but a day later
                .map(f -> queryBuilder.range().onField("created_at").below(f).excludeLimit().createQuery()) // which i exclude
                .ifPresent(q -> outer.must(q));
        return fullTextEntityManager.createFullTextQuery(outer.createQuery(), TweetEntity.class).getResultList();
    }

    @Override
    @Transactional(readOnly = true)
    public List<TweetEntity> searchByQuery(final String query) {
        final FullTextEntityManager fullTextEntityManager = Search.getFullTextEntityManager(entityManager);
        List<TweetEntity> rv;
        try {
            final QueryParser queryParser = new QueryParser("content",
                    fullTextEntityManager.getSearchFactory().getAnalyzer(TweetEntity.class));
            rv = fullTextEntityManager.createFullTextQuery(queryParser.parse(query), TweetEntity.class)
                    .getResultList();
        } catch (ParseException e) {
            log.error("Could not parse query", e);
            rv = new ArrayList<>();
        }
        return rv;
    }

    /**
     * Creates a query like
     * <pre>
     * WITH RECURSIVE cte AS (
     *      SELECT id, content FROM tweets WHERE id = ?
     *      UNION ALL
     *      SELECT t.id, t.content
     *      FROM cte c
     *      JOIN tweets t on t.in_reply_to_status_id = c.id
     * ) SELECT * FROM cte
     * </pre>
     *
     * @param id The id of the tweet starting the hierarchy, i.e. 726762141064286208
     * @return
     */
    @Override
    public List<TweetEntity> getTweetHierarchy(final long id) {
        final SelectQuery<Record> sqlGenerator = this.create.withRecursive("cte")
                .as(select().from(TWEETS).where(TWEETS.ID.eq(id))
                        .unionAll(select().from(name("cte")).join(TWEETS)
                                .on(TWEETS.IN_REPLY_TO_STATUS_ID.eq(field(name("cte", "id"), Long.class)))))
                .select().from(name("cte")).orderBy(field(name("cte", TWEETS.CREATED_AT.getName()))).getQuery();

        // Retrieve sql with named parameter
        final String sql = sqlGenerator.getSQL(ParamType.NAMED);
        // and create actual hibernate query
        final Query query = this.entityManager.createNativeQuery(sql, TweetEntity.class);
        // fill in parameter
        sqlGenerator.getParams().forEach((n, v) -> query.setParameter(n, v.getValue()));
        // execute query
        return query.getResultList();
    }
}