/** * * Copyright * 2009-2015 Jayway Products AB * 2016-2017 Föreningen Sambruk * * Licensed under AGPL, Version 3.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.gnu.org/licenses/agpl.txt * * 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 se.streamsource.streamflow.web.domain.entity.user; import org.qi4j.api.injection.scope.Structure; import org.qi4j.api.injection.scope.This; import org.qi4j.api.mixin.Mixins; import org.qi4j.api.query.Query; import org.qi4j.api.specification.Specification; import org.qi4j.api.structure.Module; import org.qi4j.api.unitofwork.NoSuchEntityException; import org.qi4j.api.unitofwork.UnitOfWork; import org.qi4j.api.util.DateFunctions; import org.qi4j.api.util.Iterables; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import se.streamsource.streamflow.web.domain.Removable; import se.streamsource.streamflow.web.domain.entity.casetype.CaseTypeEntity; import se.streamsource.streamflow.web.domain.entity.label.LabelEntity; import se.streamsource.streamflow.web.domain.entity.note.NotesTimeLineEntity; import se.streamsource.streamflow.web.domain.entity.project.ProjectEntity; import se.streamsource.streamflow.web.domain.interaction.security.PermissionType; import se.streamsource.streamflow.web.domain.structure.caze.Case; import se.streamsource.streamflow.web.domain.structure.user.UserAuthentication; import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Arrays; import java.util.Calendar; import java.util.Collections; import java.util.Date; import java.util.List; import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.regex.PatternSyntaxException; /** * JAVADOC */ @Mixins(SearchCaseQueries.Mixin.class) public interface SearchCaseQueries { Query<Case> search( String query, boolean includeNotesInSearch ); abstract class Mixin implements SearchCaseQueries { @Structure Module module; private Logger log = LoggerFactory.getLogger( SearchCaseQueries.Mixin.class ); @This UserAuthentication.Data user; public Query<Case> search( String query, boolean includeNotesInSearch ) { UnitOfWork uow = module.unitOfWorkFactory().currentUnitOfWork(); String includeNotesQuery = ""; String queryString = query.trim(); if (queryString.length() > 0) { StringBuilder queryBuilder = new StringBuilder(); List<SubQuery> searches = extractSubQueries( queryString ); for (int i = 0; i < searches.size(); i++) { SubQuery search = searches.get( i ); if (search.hasName("status")) { queryBuilder.append( " status:(" ); int count = 0; for (String status : Arrays.asList(search.getValue().split(","))) { if (count == 0) { queryBuilder.append(status); } else { queryBuilder.append(" OR ").append(status); } count++; } queryBuilder.append(")"); } else if (search.hasName( "label" )) { List<LabelEntity> labels = new ArrayList<LabelEntity>(); for (String label : search.getValue().split(",")) { try { labels.add( module.unitOfWorkFactory().currentUnitOfWork().get( LabelEntity.class, label.replace( "\\", "" ) ) ); } catch (NoSuchEntityException e) { StringBuilder labelQueryBuilder = new StringBuilder( "type:se.streamsource.streamflow.web.domain.entity.label.LabelEntity" ); labelQueryBuilder.append( " (description:" ).append( label ); labelQueryBuilder.append( " OR ntext:" ).append( label ).append( ")" ); Iterables.addAll( labels, module.queryBuilderFactory() .newNamedQuery( LabelEntity.class, uow, "solrquery" ) .setVariable( "query", labelQueryBuilder.toString() ) ); } } if (labels.iterator().hasNext()) { queryBuilder.append(" labels:("); int count = 0; for (LabelEntity labelEntity : labels) { if (count == 0) { queryBuilder.append(labelEntity.identity().get()); } else { queryBuilder.append(" OR ").append(labelEntity.identity().get()); } count++; } queryBuilder.append(")"); } else { // dismiss search - no label/s with given name exist. // Return empty search return module.queryBuilderFactory().newQueryBuilder(Case.class) .newQuery( Collections.<Case>emptyList() ); } } else if (search.hasName( "caseType" )) { List<CaseTypeEntity> caseTypes = new ArrayList<CaseTypeEntity>(); for (String caseType : search.getValue().split(",")) { try { caseTypes.add( module.unitOfWorkFactory().currentUnitOfWork().get( CaseTypeEntity.class, caseType.replace( "\\", "" ) ) ); } catch ( NoSuchEntityException e ) { StringBuilder caseTypeQueryBuilder = new StringBuilder( "type:se.streamsource.streamflow.web.domain.entity.casetype.CaseTypeEntity" ); caseTypeQueryBuilder.append( " (description:" ).append( caseType ); caseTypeQueryBuilder.append( " OR ntext:" ).append( caseType ).append( ")" ); Iterables.addAll( caseTypes, module.queryBuilderFactory().newNamedQuery( CaseTypeEntity.class, uow, "solrquery" ) .setVariable( "query", caseTypeQueryBuilder.toString() ) ); } } if (caseTypes.iterator().hasNext()) { queryBuilder.append( " caseType:(" ); int count = 0; for (CaseTypeEntity caseType : caseTypes) { if (count == 0) { queryBuilder.append( caseType.identity().get() ); } else { queryBuilder.append( " OR " ).append( caseType.identity().get() ); } count++; } queryBuilder.append( ")" ); } else { // dismiss search - no case type/s for given name exists. Return empty search return module.queryBuilderFactory().newQueryBuilder( Case.class ).newQuery( Collections.<Case>emptyList() ); } } else if (search.hasName( "project" )) { List<ProjectEntity> projects = new ArrayList<ProjectEntity>(); for (String project : search.getValue().split(",")) { try { projects.add( module.unitOfWorkFactory().currentUnitOfWork().get( ProjectEntity.class, project.replace( "\\", "" ) ) ); } catch ( NoSuchEntityException e ) { StringBuilder projectQueryBuilder = new StringBuilder( "type:se.streamsource.streamflow.web.domain.entity.project.ProjectEntity" ); projectQueryBuilder.append( " ( description:" ).append( project ); projectQueryBuilder.append( " OR ntext:" ).append( project ).append( ")" ); Iterables.addAll( projects, module.queryBuilderFactory().newNamedQuery( ProjectEntity.class, uow, "solrquery" ) .setVariable( "query", projectQueryBuilder.toString() ) ); } } if (projects.iterator().hasNext()) { queryBuilder.append( " owner:(" ); int count = 0; for (ProjectEntity project : projects) { if (count == 0) { queryBuilder.append( project.identity().get() ); } else { queryBuilder.append( " OR " ).append( project.identity().get() ); } count++; } queryBuilder.append( ")" ); } else { // dismiss search - no project/s for given name exists. Return empty search return module.queryBuilderFactory().newQueryBuilder( Case.class ).newQuery( Collections.<Case>emptyList() ); } } else if (search.hasName( "notes" )) { List<NotesTimeLineEntity> notes = new ArrayList<NotesTimeLineEntity>(); for (String note : search.getValue().split(",")) { StringBuilder projectQueryBuilder = new StringBuilder( "type:se.streamsource.streamflow.web.domain.entity.note.NotesTimeLineEntity" ); projectQueryBuilder.append( " ( note:" ).append( note ).append( ")" ); Iterables.addAll( notes, module.queryBuilderFactory().newNamedQuery( NotesTimeLineEntity.class, uow, "solrquery" ) .setVariable( "query", projectQueryBuilder.toString() ) ); } if (notes.iterator().hasNext()) { queryBuilder.append( " notes:(" ); int count = 0; for (NotesTimeLineEntity note : notes) { if (count == 0) { queryBuilder.append( note.identity().get() ); } else { queryBuilder.append( " OR " ).append( note.identity().get() ); } count++; } queryBuilder.append( ")" ); } else { // dismiss search - no notes for given name exists. Return empty search return module.queryBuilderFactory().newQueryBuilder( Case.class ).newQuery( Collections.<Case>emptyList() ); } } else if (search.hasName( "createdBy" )) { List<UserEntity> users = new ArrayList<UserEntity>(); String userName = user.userName().get(); for (String user : search.getValue().split(",")) { try { users.add( module.unitOfWorkFactory().currentUnitOfWork().get( UserEntity.class, user.replace( "\\", "" ) ) ); } catch ( NoSuchEntityException e ) { StringBuilder creatorQueryBuilder = new StringBuilder( "type:se.streamsource.streamflow.web.domain.entity.user.UserEntity" ); creatorQueryBuilder.append( " (id:" ).append( getUserInSearch( user, userName ) ) .append( " OR " ).append( " description:" ).append( getUserInSearch( user, userName ) ) .append( " OR " ).append( " ntext:" ).append( getUserInSearch( user, userName ) ).append( ")" ); Iterables.addAll( users, module.queryBuilderFactory() .newNamedQuery( UserEntity.class, uow, "solrquery" ).setVariable( "query", creatorQueryBuilder.toString() ) ); } } int count = 0; for (UserEntity user : users) { if (count == 0) { queryBuilder.append( " createdBy:(" ).append( user.identity().get() ); } else { queryBuilder.append( " OR " ).append( user.identity().get() ); } count++; } if (count > 0) queryBuilder.append( ")" ); else { // dismiss search - no user/s for given name exists. Return empty search return module.queryBuilderFactory().newQueryBuilder( Case.class ).newQuery( Collections.<Case>emptyList() ); } } else if (search.hasName( "description", "note", "name", "contactId", "phoneNumber", "emailAddress" )) { queryBuilder.append( " " ).append( search.getName() ).append( ":" ).append( search.getQuotedValue() ); } else if (search.hasName( "createdOn" ) || search.hasName( "dueOn" ) ) { buildDateQuery( queryBuilder, search ); } else if (search.hasName( "assignedTo" )) { List<UserEntity> users = new ArrayList<UserEntity>(); String userName = user.userName().get(); for (String user : search.getValue().split(",")) { try { users.add( module.unitOfWorkFactory().currentUnitOfWork().get( UserEntity.class, user.replace( "\\", "" ) ) ); } catch ( NoSuchEntityException e ) { StringBuilder creatorQueryBuilder = new StringBuilder( "type:se.streamsource.streamflow.web.domain.entity.user.UserEntity" ); creatorQueryBuilder.append( " (id:" ).append( getUserInSearch( user, userName ) ).append( " OR " ) .append( " description:" ).append( getUserInSearch( user, userName ) ).append( " OR " ) .append( " ntext:" ).append( getUserInSearch( user, userName ) ).append( ")" ); Iterables.addAll( users, module.queryBuilderFactory().newNamedQuery( UserEntity.class, uow, "solrquery" ) .setVariable( "query", creatorQueryBuilder.toString() ) ); } } int count = 0; for (UserEntity userItem : users) { if (count == 0) { queryBuilder.append( " assignedTo:(" ).append( userItem.identity().get() ); } else { queryBuilder.append( " OR " ).append( userItem.identity().get() ); } count++; } if (count > 0) queryBuilder.append( ")" ); else { // dismiss search - no user/s for given name exists. Return empty search return module.queryBuilderFactory().newQueryBuilder( Case.class ).newQuery( Collections.<Case>emptyList() ); } } else { if (queryBuilder.length() > 0) { queryBuilder.append( " " ); } if( includeNotesInSearch ) { includeNotesQuery = " ( _query_:\"{!join from=id to=notes } type:se.streamsource.streamflow.web.domain.entity.note.NotesTimeLineEntity note:" + search.getValue() + "\" OR text:" + search.getValue() + ")"; queryBuilder.append( " " ); } else { queryBuilder.append( search.getValue() ); } } } if (queryBuilder.length() != 0) { queryBuilder.append( " type:se.streamsource.streamflow.web.domain.entity.caze.CaseEntity" ); queryBuilder.append( " !status:DRAFT" ); if( includeNotesInSearch ) { queryBuilder.append( includeNotesQuery ); } log.debug( "Executing solr query: " + queryBuilder.toString() ); Query<Case> cases = module.queryBuilderFactory() .newNamedQuery( Case.class, uow, "solrquery" ).setVariable( "query", queryBuilder.toString() ); return module.queryBuilderFactory().newQueryBuilder( Case.class ).newQuery( Iterables.filter( new Specification<Case>() { public boolean satisfiedBy( Case item ) { if( user.isAdministrator() ) return true; else return !((Removable.Data)item).removed().get() && item.hasPermission( user.userName().get(), PermissionType.read.name() ); } }, cases) ); } } return module.queryBuilderFactory().newQueryBuilder( Case.class ).newQuery( Collections.<Case>emptyList() ); } private void buildDateQuery( StringBuilder queryBuilder, SubQuery search ) { String name = search.getName(); String searchDateFrom = search.getValue(); String searchDateTo = search.getValue(); if (occurrancesOfInString( "-", searchDateFrom ) == 1) { // Substring and remove escape chars searchDateFrom = searchDateFrom.substring( 0, searchDateFrom.indexOf( "-" ) ).replace( "\\", "" ); searchDateTo = searchDateTo.substring( searchDateTo.indexOf( "-" ) + 1, searchDateTo.length() ); } Date referenceDate = new Date(); Date lowerBoundDate = getLowerBoundDate( searchDateFrom, referenceDate ); Date upperBoundDate = getUpperBoundDate( searchDateTo, referenceDate ); if (lowerBoundDate == null || upperBoundDate == null) { return; } queryBuilder.append( " " ).append( name ).append( ":[" ). append( DateFunctions.toUtcString( lowerBoundDate ) ). append( " TO " ). append( DateFunctions.toUtcString( upperBoundDate ) ). append( "]" ); } protected List<SubQuery> extractSubQueries( String query ) { List<SubQuery> subQueries = new ArrayList<SubQuery>(); // TODO: Extract regular expression to resource file. String regExp = "((\\w+)\\:)?((\\\"([^\\\"]*)\\\")|([^\\s]+))"; // old "(?:\\w+\\:)?(?:\\\"[^\\\"]*?\\\")|(?:[^\\s]+)"; Pattern p; try { p = Pattern.compile( regExp ); } catch (PatternSyntaxException e) { return subQueries; } Matcher m = p.matcher( query ); while (m.find()) { String value = m.group( 5 ); if (value == null) value = m.group( 3 ); subQueries.add( new SubQuery( m.group( 2 ), value ) ); } if (subQueries.isEmpty()) { if (query.length() > 0) subQueries.add( new SubQuery( null, query ) ); } return subQueries; } /** * Get the calling user from the access controller roleMap. * * @param * @return */ protected String getUserInSearch( String userName, String user ) { if (UserSearchKeyword.ME.toString().equalsIgnoreCase( userName )) { return user; } else { return "\"" + userName + "\""; } } protected Date getLowerBoundDate( String dateAsString, Date referenceDate ) { Calendar calendar = Calendar.getInstance(); calendar.setTime( referenceDate ); Date lowerBoundDate = null; // TODAY, YESTERDAY, HOUR, WEEK, if (DateSearchKeyword.TODAY.toString().equalsIgnoreCase( dateAsString )) { calendar.set( Calendar.HOUR_OF_DAY, 0 ); calendar.set( Calendar.MINUTE, 0 ); calendar.set( Calendar.SECOND, 0 ); lowerBoundDate = calendar.getTime(); } else if (DateSearchKeyword.YESTERDAY.toString().equalsIgnoreCase( dateAsString )) { calendar.add( Calendar.DAY_OF_MONTH, -1 ); calendar.set( Calendar.HOUR_OF_DAY, 0 ); calendar.set( Calendar.MINUTE, 0 ); calendar.set( Calendar.SECOND, 0 ); lowerBoundDate = calendar.getTime(); } else if (DateSearchKeyword.HOUR.toString().equalsIgnoreCase( dateAsString )) { calendar.add( Calendar.HOUR_OF_DAY, -1 ); lowerBoundDate = calendar.getTime(); } else if (DateSearchKeyword.WEEK.toString().equalsIgnoreCase( dateAsString )) { calendar.add( Calendar.WEEK_OF_MONTH, -1 ); lowerBoundDate = calendar.getTime(); } else { try { lowerBoundDate = parseToDate( dateAsString ); calendar.setTime( lowerBoundDate ); calendar.set( Calendar.HOUR_OF_DAY, 0 ); calendar.set( Calendar.MINUTE, 0 ); calendar.set( Calendar.SECOND, 0 ); lowerBoundDate = calendar.getTime(); } catch (ParseException e) { // Skip the "created:" search as the input query can not be // interpreted. lowerBoundDate = null; } catch (IllegalArgumentException e) { lowerBoundDate = null; } } return lowerBoundDate; } protected Date getUpperBoundDate( String dateAsString, Date referenceDate ) { Calendar calendar = Calendar.getInstance(); calendar.setTime( referenceDate ); Date upperBoundDate = calendar.getTime(); // TODAY, YESTERDAY, HOUR, WEEK, if (DateSearchKeyword.TODAY.toString().equalsIgnoreCase( dateAsString )) { calendar.set( Calendar.HOUR_OF_DAY, 23 ); calendar.set( Calendar.MINUTE, 59 ); calendar.set( Calendar.SECOND, 59 ); upperBoundDate = calendar.getTime(); } else if (DateSearchKeyword.YESTERDAY.toString().equalsIgnoreCase( dateAsString )) { calendar.add( Calendar.DAY_OF_MONTH, -1 ); calendar.set( Calendar.HOUR_OF_DAY, 23 ); calendar.set( Calendar.MINUTE, 59 ); calendar.set( Calendar.SECOND, 59 ); upperBoundDate = calendar.getTime(); } else if (DateSearchKeyword.HOUR.toString().equalsIgnoreCase( dateAsString )) { // Do nothing } else if (DateSearchKeyword.WEEK.toString().equalsIgnoreCase( dateAsString )) { // Do nothing } else { try { upperBoundDate = parseToDate( dateAsString ); calendar.setTime( upperBoundDate ); calendar.set( Calendar.HOUR_OF_DAY, 23 ); calendar.set( Calendar.MINUTE, 59 ); calendar.set( Calendar.SECOND, 59 ); upperBoundDate = calendar.getTime(); } catch (ParseException e) { upperBoundDate = null; } catch (IllegalArgumentException e) { upperBoundDate = null; } } return upperBoundDate; } private Date parseToDate( String dateAsString ) throws ParseException, IllegalArgumentException { // Formats that should pass: yyyy-MM-dd, yyyyMMdd. // TODO: Should we also support yyyy? SimpleDateFormat dateFormat = null; if (dateAsString == null) { throw new ParseException( "Date string can not be null!", 0 ); } dateAsString = dateAsString.replaceAll( "-", "" ); if (dateAsString.length() != 8) { throw new IllegalArgumentException( "Date format not supported!" ); } if (dateAsString.length() == 8) { // TODO: Extract date format to resource file. dateFormat = new SimpleDateFormat( "yyyyMMdd" ); } return dateFormat.parse( dateAsString ); } protected int occurrancesOfInString( String pattern, String source ) { Pattern p = Pattern.compile( pattern ); Matcher m = p.matcher( source ); int count = 0; while (m.find()) { count++; System.out.println( "Match number " + count ); System.out.println( "start(): " + m.start() ); System.out.println( "end(): " + m.end() ); } return count; } class SubQuery { String name; String value; public SubQuery( String name, String value ) { this.name = name; this.value = value; } public String getName() { return name; } public String getValue() { return escapeLuceneCharacters( value ); } public String getQuotedValue() { return "\""+escapeLuceneCharacters( value )+"\""; } public boolean hasName( String... names ) { return name == null ? false : Arrays.asList( names ).contains( name ); } private String escapeLuceneCharacters( String query ) { // DO NOT escape wildcard characters!! "*", "?" List<String> specialChars = Arrays.asList( "+", "-", "&&", "||", "!", "(", ")", "{", "}", "[", "]", "^", "\"", "~", ":" ); for ( String str : specialChars ) { if( query.contains( str ) ) { char[] escaped = new char[str.length()*2]; int count = 0; for( Character c : str.toCharArray() ) { escaped[count] = '\\'; count++; escaped[count] = c; count++; } query = query.replace( str, new String( escaped ) ); } } return query; } } } }