/** * JBoss, Home of Professional Open Source * Copyright Red Hat, Inc., and individual contributors. * * 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 org.jboss.aerogear.unifiedpush.jpa.dao.impl; import org.hibernate.Query; import org.hibernate.ScrollMode; import org.hibernate.ScrollableResults; import org.jboss.aerogear.unifiedpush.api.Installation; import org.jboss.aerogear.unifiedpush.dao.InstallationDao; import org.jboss.aerogear.unifiedpush.dao.PageResult; import org.jboss.aerogear.unifiedpush.dao.ResultStreamException; import org.jboss.aerogear.unifiedpush.dao.ResultsStream; import org.jboss.aerogear.unifiedpush.dto.Count; import javax.persistence.TypedQuery; import java.util.Collection; import java.util.Collections; import java.util.HashSet; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Set; public class JPAInstallationDao extends JPABaseDao<Installation, String> implements InstallationDao { private static final String FIND_ALL_DEVICES_FOR_VARIANT_QUERY = "select distinct installation.deviceToken" + " from Installation installation" + " left join installation.categories c " + " join installation.variant abstractVariant where abstractVariant.variantID = :variantID AND installation.enabled = true"; private static final String FIND_ALL_DEVICES_FOR_VARIANT_QUERY_LEGACY = "select distinct installation.deviceToken" + " from Installation installation" + " left join installation.categories c " + " join installation.variant abstractVariant where abstractVariant.variantID = :variantID AND installation.enabled = true AND locate(':', installation.deviceToken) = 0"; private static final String FIND_INSTALLATIONS = "FROM Installation installation" + " JOIN installation.variant v" + " WHERE v.variantID = :variantID"; @Override public PageResult<Installation, Count> findInstallationsByVariantForDeveloper( String variantID, String developer, Integer page, Integer pageSize, String search) { final StringBuilder jpqlBase = new StringBuilder(FIND_INSTALLATIONS); final Map<String, Object> parameters = new LinkedHashMap<>(); parameters.put("variantID", variantID); if (developer != null) { jpqlBase.append(" AND v.developer = :developer"); parameters.put("developer", developer); } if (search != null) { jpqlBase.append(" AND ( installation.deviceToken LIKE :search" + " OR installation.deviceType LIKE :search" + " OR installation.platform LIKE :search" + " OR installation.operatingSystem LIKE :search" + " OR installation.osVersion LIKE :search" + " OR installation.alias LIKE :search )"); parameters.put("search", "%" + search + "%"); } String jpqlStr = jpqlBase.toString(); TypedQuery<Long> countQuery = createQuery("SELECT COUNT(installation) " + jpqlStr, Long.class); TypedQuery<Installation> query = createQuery("SELECT installation " + jpqlStr + " ORDER BY installation.id") .setFirstResult(page * pageSize) .setMaxResults(pageSize); List<Installation> resultList = setParameters(query, parameters).getResultList(); Long count = setParameters(countQuery, parameters).getSingleResult(); return new PageResult<>(resultList, new Count(count)); } private static <X> TypedQuery<X> setParameters(TypedQuery<X> query, Map<String, Object> parameters) { parameters.forEach(query::setParameter); return query; } @Override public PageResult<Installation, Count> findInstallationsByVariant(String variantID, Integer page, Integer pageSize, String search) { return findInstallationsByVariantForDeveloper(variantID, null, page, pageSize, search); } @Override public Installation findInstallationForVariantByDeviceToken(String variantID, String deviceToken) { return getSingleResultForQuery(createQuery("select installation from Installation installation " + " join installation.variant abstractVariant" + " where abstractVariant.variantID = :variantID" + " and installation.deviceToken = :deviceToken") .setParameter("variantID", variantID) .setParameter("deviceToken", deviceToken)); } @Override public List<Installation> findInstallationsForVariantByDeviceTokens(String variantID, Set<String> deviceTokens) { // if there are no device-tokens, no need to bug the database if (deviceTokens == null || deviceTokens.isEmpty()) { // be nice and return an empty list... return Collections.emptyList(); } return createQuery("select installation from Installation installation " + " join installation.variant abstractVariant " + " where abstractVariant.variantID = :variantID" + " and installation.deviceToken IN :deviceTokens") .setParameter("variantID", variantID) .setParameter("deviceTokens", deviceTokens) .getResultList(); } @Override public Set<String> findAllDeviceTokenForVariantID(String variantID) { TypedQuery<String> query = createQuery(FIND_ALL_DEVICES_FOR_VARIANT_QUERY, String.class); query.setParameter("variantID", variantID); return new HashSet<>(query.getResultList()); } @Override public ResultsStream.QueryBuilder<String> findAllDeviceTokenForVariantIDByCriteria(String variantID, List<String> categories, List<String> aliases, List<String> deviceTypes, final int maxResults, String lastTokenFromPreviousBatch, boolean oldGCM) { // the required part: Join + all tokens for variantID; final StringBuilder jpqlString = oldGCM ? new StringBuilder(FIND_ALL_DEVICES_FOR_VARIANT_QUERY_LEGACY) : new StringBuilder(FIND_ALL_DEVICES_FOR_VARIANT_QUERY); final Map<String, Object> parameters = new LinkedHashMap<>(); parameters.put("variantID", variantID); // apend query conditions based on specified message parameters appendDynamicQuery(jpqlString, parameters, categories, aliases, deviceTypes); // sort on ids so that we can handle paging properly if (lastTokenFromPreviousBatch != null) { jpqlString.append(" AND installation.deviceToken > :lastTokenFromPreviousBatch"); parameters.put("lastTokenFromPreviousBatch", lastTokenFromPreviousBatch); } jpqlString.append(" ORDER BY installation.deviceToken ASC"); return new ResultsStream.QueryBuilder<String>() { private Integer fetchSize; @Override public ResultsStream.QueryBuilder<String> fetchSize(int fetchSize) { this.fetchSize = fetchSize; return this; } @Override public ResultsStream<String> executeQuery() { Query hibernateQuery = JPAInstallationDao.this.createHibernateQuery(jpqlString.toString()); hibernateQuery.setMaxResults(maxResults); parameters.forEach((k,v) -> { if (v instanceof Collection<?>) { hibernateQuery.setParameterList(k, (Collection<?>) v); } else { hibernateQuery.setParameter(k, v); } }); hibernateQuery.setReadOnly(true); if (fetchSize != null) { hibernateQuery.setFetchSize(fetchSize); } final ScrollableResults results = hibernateQuery.scroll(ScrollMode.FORWARD_ONLY); return new ResultsStream<String>() { @Override public boolean next() throws ResultStreamException { return results.next(); } @Override public String get() throws ResultStreamException { return (String) results.get()[0]; } }; } }; } @Override public long getNumberOfDevicesForLoginName(String loginName) { return createQuery("select count(installation) from Installation installation, Variant t where installation.variant = t.variantID and t.developer = :developer ", Long.class) .setParameter("developer", loginName).getSingleResult(); } @Override public Class<Installation> getType() { return Installation.class; } //Admin query @Override public long getTotalNumberOfDevices() { return createQuery("select count(installation) from Installation installation", Long.class) .getSingleResult(); } @Override public long getNumberOfDevicesForVariantID(String variantId) { return createQuery("select count(installation) from Installation installation join installation.variant abstractVariant where abstractVariant.variantID = :variantId ", Long.class) .setParameter("variantId", variantId) .getSingleResult(); } /** * * A dynamic finder for all sorts of queries around selecting Device-Token, based on different criterias. * The method appends different criterias to the given JPQL string, IF PRESENT. * * Done in one method, instead of having similar, but error-thrown Strings, in different methods. * * TODO: perhaps moving to Criteria API for this later */ private static void appendDynamicQuery(final StringBuilder jpqlString, final Map<String, Object> parameters, List<String> categories, List<String> aliases, List<String> deviceTypes) { // OPTIONAL query arguments, as provided..... // are aliases present ?? if (isListEmpty(aliases)) { // append the string: jpqlString.append(" AND installation.alias IN :aliases"); // add the params: parameters.put("aliases", aliases); } // are devices present ?? if (isListEmpty(deviceTypes)) { // append the string: jpqlString.append(" AND installation.deviceType IN :deviceTypes"); // add the params: parameters.put("deviceTypes", deviceTypes); } // is a category present ? if (isListEmpty(categories)) { jpqlString.append(" AND ( c.name in (:categories))"); parameters.put("categories", categories); } } /** * Checks if the list is empty, and not null */ private static boolean isListEmpty(List list) { return list != null && !list.isEmpty(); } }