diff --git a/gemma-web/src/main/java/ubic/gemma/web/controller/BaseFormController.java b/gemma-web/src/main/java/ubic/gemma/web/controller/BaseFormController.java deleted file mode 100644 index 487e5a7ebb..0000000000 --- a/gemma-web/src/main/java/ubic/gemma/web/controller/BaseFormController.java +++ /dev/null @@ -1,177 +0,0 @@ -/* - * The Gemma project - * - * Copyright (c) 2006 University of British Columbia - * - * 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 ubic.gemma.web.controller; - -import org.apache.commons.lang3.StringUtils; -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.beans.propertyeditors.CustomNumberEditor; -import org.springframework.validation.BindException; -import org.springframework.validation.ObjectError; -import org.springframework.web.bind.WebDataBinder; -import org.springframework.web.bind.annotation.InitBinder; -import org.springframework.web.multipart.support.ByteArrayMultipartFileEditor; -import org.springframework.web.servlet.ModelAndView; -import org.springframework.web.servlet.mvc.SimpleFormController; -import ubic.gemma.core.util.MailEngine; -import ubic.gemma.web.util.MessageUtil; - -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; -import javax.servlet.http.HttpSession; -import java.text.NumberFormat; -import java.util.Locale; - -/** - * Implementation of SimpleFormController that contains convenience methods for subclasses. For - * example, getting the current user and saving messages/errors. This class is intended to be a base class for all Form - * controllers. - * @deprecated {@link SimpleFormController} is deprecated, use annotations-based GET/POST mapping instead. - * - * @author pavlidis (originally based on Appfuse code) - */ -@Deprecated -public abstract class BaseFormController extends SimpleFormController { - protected static final Log log = LogFactory.getLog( BaseFormController.class.getName() ); - - @Autowired - private MessageUtil messageUtil; - - @Autowired - private MailEngine mailEngine; - - /** - * @return the messageUtil - */ - public MessageUtil getMessageUtil() { - return this.messageUtil; - } - - /** - * @param messageUtil the messageUtil to set - */ - public void setMessageUtil( MessageUtil messageUtil ) { - this.messageUtil = messageUtil; - } - - /** - * @see ubic.gemma.web.util.MessageUtilImpl#getText(java.lang.String, java.util.Locale) - */ - public String getText( String msgKey, Locale locale ) { - return this.messageUtil.getText( msgKey, locale ); - } - - /** - * @see ubic.gemma.web.util.MessageUtilImpl#saveMessage(javax.servlet.http.HttpServletRequest, java.lang.String) - */ - public void saveMessage( HttpServletRequest request, String msg ) { - this.messageUtil.saveMessage( request, msg ); - } - - /** - * @see ubic.gemma.web.util.MessageUtilImpl#saveMessage(javax.servlet.http.HttpServletRequest, java.lang.String, java.lang.Object, java.lang.String) - */ - public void saveMessage( HttpServletRequest request, String key, Object parameter, String defaultMessage ) { - this.messageUtil.saveMessage( request, key, parameter, defaultMessage ); - } - - /** - * @see ubic.gemma.web.util.MessageUtilImpl#saveMessage(javax.servlet.http.HttpServletRequest, java.lang.String, java.lang.Object[], java.lang.String) - */ - public void saveMessage( HttpServletRequest request, String key, Object[] parameters, String defaultMessage ) { - this.messageUtil.saveMessage( request, key, parameters, defaultMessage ); - } - - /** - * @see ubic.gemma.web.util.MessageUtilImpl#saveMessage(javax.servlet.http.HttpServletRequest, java.lang.String, java.lang.String) - */ - public void saveMessage( HttpServletRequest request, String key, String defaultMessage ) { - this.messageUtil.saveMessage( request, key, defaultMessage ); - } - - /** - * @see ubic.gemma.web.util.MessageUtilImpl#saveMessage(javax.servlet.http.HttpSession, java.lang.String) - */ - public void saveMessage( HttpSession session, String msg ) { - this.messageUtil.saveMessage( session, msg ); - } - - public void setMailEngine( MailEngine mailEngine ) { - this.mailEngine = mailEngine; - } - - /** - * Override this to control which cancelView is used. The default behavior is to go to the success view if there is - * no cancel view defined; otherwise, get the cancel view. - * - * @param request can be used to control which cancel view to use. (This is not used in the default implementation) - * @return the view to use. - */ - protected ModelAndView getCancelView( HttpServletRequest request ) { - return new ModelAndView( WebConstants.HOME_PAGE ); - } - - /** - * Set up a custom property editor for converting form inputs to real objects. Override this to add additional - * custom editors (call super.initBinder() in your implementation) - */ - @InitBinder - protected void initBinder( WebDataBinder binder ) { - NumberFormat nf = NumberFormat.getNumberInstance(); - binder.registerCustomEditor( Integer.class, null, new CustomNumberEditor( Integer.class, nf, true ) ); - binder.registerCustomEditor( Long.class, null, new CustomNumberEditor( Long.class, nf, true ) ); - binder.registerCustomEditor( byte[].class, new ByteArrayMultipartFileEditor() ); - } - - /** - * Convenience method to get the user object from the session - * - * @param request the current request - * @return the user's populated object from the session - */ - - protected ModelAndView processErrors( HttpServletRequest request, HttpServletResponse response, Object command, - BindException errors, String message ) throws Exception { - if ( !StringUtils.isEmpty( message ) ) { - log.error( message ); - if ( command == null ) { - errors.addError( new ObjectError( "nullCommand", null, null, message ) ); - } else { - errors.addError( new ObjectError( command.toString(), null, null, message ) ); - } - } - - return this.processFormSubmission( request, response, command, errors ); - } - - /** - * Default behavior for FormControllers - redirect to the successView when the cancel button has been pressed. - */ - @Override - protected ModelAndView processFormSubmission( HttpServletRequest request, HttpServletResponse response, - Object command, BindException errors ) throws Exception { - if ( request.getParameter( "cancel" ) != null ) { - messageUtil.saveMessage( request, "errors.cancel", "Cancelled" ); - return getCancelView( request ); - } - - return super.processFormSubmission( request, response, command, errors ); - } -} \ No newline at end of file diff --git a/gemma-web/src/main/java/ubic/gemma/web/controller/GeneralSearchController.java b/gemma-web/src/main/java/ubic/gemma/web/controller/GeneralSearchController.java index 119839373a..58d4e2fb09 100644 --- a/gemma-web/src/main/java/ubic/gemma/web/controller/GeneralSearchController.java +++ b/gemma-web/src/main/java/ubic/gemma/web/controller/GeneralSearchController.java @@ -1,61 +1,393 @@ /* * The Gemma project * - * Copyright (c) 2012 University of British Columbia + * Copyright (c) 2007 Columbia University * - * 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 + * 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 + * 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. * - * 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 ubic.gemma.web.controller; import lombok.Value; +import lombok.extern.apachecommons.CommonsLog; +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.exception.ExceptionUtils; +import org.apache.commons.lang3.time.StopWatch; +import org.apache.lucene.analysis.Analyzer; +import org.apache.lucene.document.Document; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.propertyeditors.CustomNumberEditor; +import org.springframework.context.MessageSource; import org.springframework.stereotype.Controller; -import org.springframework.validation.BindException; +import org.springframework.web.bind.WebDataBinder; +import org.springframework.web.bind.annotation.InitBinder; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.multipart.support.ByteArrayMultipartFileEditor; import org.springframework.web.servlet.ModelAndView; +import org.springframework.web.servlet.support.ServletUriComponentsBuilder; +import org.springframework.web.util.UriComponentsBuilder; +import ubic.gemma.core.search.DefaultHighlighter; +import ubic.gemma.core.search.SearchException; import ubic.gemma.core.search.SearchResult; +import ubic.gemma.core.search.SearchService; +import ubic.gemma.model.analysis.expression.ExpressionExperimentSet; +import ubic.gemma.model.blacklist.BlacklistedEntity; +import ubic.gemma.model.common.Identifiable; import ubic.gemma.model.common.IdentifiableValueObject; import ubic.gemma.model.common.search.SearchSettings; import ubic.gemma.model.common.search.SearchSettingsValueObject; +import ubic.gemma.model.expression.arrayDesign.ArrayDesign; +import ubic.gemma.model.expression.designElement.CompositeSequence; +import ubic.gemma.model.expression.experiment.ExpressionExperiment; +import ubic.gemma.model.genome.Gene; +import ubic.gemma.model.genome.Taxon; +import ubic.gemma.model.genome.gene.GeneSet; +import ubic.gemma.persistence.service.genome.taxon.TaxonService; +import ubic.gemma.web.propertyeditor.TaxonPropertyEditor; import ubic.gemma.web.remote.JsonReaderResponse; +import ubic.gemma.web.util.MessageUtil; +import javax.annotation.Nullable; +import javax.annotation.ParametersAreNonnullByDefault; import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; +import java.net.URI; +import java.text.NumberFormat; +import java.util.*; import java.util.stream.Collectors; +import static org.apache.commons.text.StringEscapeUtils.escapeHtml4; +import static ubic.gemma.core.search.lucene.LuceneQueryUtils.prepareTermUriQuery; + /** * Note: do not use parametrized collections as parameters for ajax methods in this class! Type information is lost * during proxy creation so DWR can't figure out what type of collection the method should take. See bug 2756. Use * arrays instead. * - * @author paul + * @author klc */ +@CommonsLog @Controller -public interface GeneralSearchController { +public class GeneralSearchController { /** - * AJAX-flavoured search. - * Mapped by DWR. + * Maximum number of highlighted documents. */ - @SuppressWarnings("unused") - JsonReaderResponse> ajaxSearch( SearchSettingsValueObject settings ); + private static final int MAX_HIGHLIGHTED_DOCUMENTS = 500; + + @Value + private static class Scope { + char scope; + Class resultType; + } + + /** + * List of supported scopes (or result types) for searching. + */ + private static final Scope[] scopes = { + new Scope( 'G', Gene.class ), + new Scope( 'E', ExpressionExperiment.class ), + new Scope( 'P', CompositeSequence.class ), + new Scope( 'A', ArrayDesign.class ), + new Scope( 'M', GeneSet.class ), + new Scope( 'N', ExpressionExperimentSet.class ) + }; + + @Autowired + private SearchService searchService; + @Autowired + private TaxonService taxonService; + @Autowired + private MessageSource messageSource; + @Autowired + private MessageUtil messageUtil; + + @Autowired + private HttpServletRequest request; + + /** + * Set up a custom property editor for converting form inputs to real objects. Override this to add additional + * custom editors (call super.initBinder() in your implementation) + */ + @InitBinder + protected void initBinder( WebDataBinder binder ) { + NumberFormat nf = NumberFormat.getNumberInstance(); + binder.registerCustomEditor( Integer.class, null, new CustomNumberEditor( Integer.class, nf, true ) ); + binder.registerCustomEditor( Long.class, null, new CustomNumberEditor( Long.class, nf, true ) ); + binder.registerCustomEditor( byte[].class, new ByteArrayMultipartFileEditor() ); + binder.registerCustomEditor( Taxon.class, new TaxonPropertyEditor( this.taxonService ) ); + } + + @RequestMapping(value = "/searcher.html", method = RequestMethod.GET) + public ModelAndView showForm( HttpServletRequest request ) { + if ( request.getParameter( "query" ) != null || request.getParameter( "termUri" ) != null ) { + return this.doSearch( request ); + } + + return new ModelAndView( "generalSearch" ); + } @RequestMapping(value = "/searcher.html", method = RequestMethod.POST) - ModelAndView doSearch( HttpServletRequest request, HttpServletResponse response, SearchSettings command, - BindException errors ) throws Exception; + public ModelAndView doSearch( HttpServletRequest request ) { + SearchSettings command = formBackingObject( request ); + + command.setQuery( StringUtils.trim( command.getQuery().trim() ) ); + + ModelAndView mav = new ModelAndView( "generalSearch" ); + + if ( !searchStringValidator( command.getQuery() ) ) { + throw new IllegalArgumentException( "Invalid query" ); + } + + // Need this for the bookmarkable links + mav.addObject( "SearchString", command.getQuery() ); + try { + URI termUri = prepareTermUriQuery( command ); + mav.addObject( "SearchURI", termUri != null ? termUri.toString() : null ); + } catch ( SearchException e ) { + mav.addObject( "SearchURI", null ); + } + if ( ( command.getTaxon() != null ) && ( command.getTaxon().getId() != null ) ) + mav.addObject( "searchTaxon", command.getTaxon().getScientificName() ); + + return mav; + } + + @SuppressWarnings("unused") + public JsonReaderResponse> ajaxSearch( SearchSettingsValueObject settingsValueObject ) { + StopWatch timer = new StopWatch(); + StopWatch searchTimer = new StopWatch(); + StopWatch fillVosTimer = new StopWatch(); + + if ( settingsValueObject == null || StringUtils.isBlank( settingsValueObject.getQuery() ) || StringUtils + .isBlank( settingsValueObject.getQuery().replaceAll( "\\*", "" ) ) ) { + // FIXME validate input better, and return error. + GeneralSearchController.log.info( "No query or invalid." ); + // return new ListRange( finalResults ); + throw new IllegalArgumentException( "Query '" + settingsValueObject + "' was invalid" ); + } + + timer.start(); + + SearchSettings searchSettings = searchSettingsFromVo( settingsValueObject ) + .withHighlighter( new Highlighter( scopeFromVo( settingsValueObject ), request.getLocale() ) ); + + searchTimer.start(); + SearchService.SearchResultMap searchResults; + try { + searchResults = searchService.search( searchSettings ); + } catch ( SearchException e ) { + throw new IllegalArgumentException( String.format( "Invalid search settings: %s.", ExceptionUtils.getRootCause( e ) ), e ); + } + searchTimer.stop(); + + // FIXME: sort by the number of hits per class, so the smallest number of hits is at the top. + fillVosTimer.start(); + List> finalResults = new ArrayList<>(); + for ( Class clazz : searchResults.getResultTypes() ) { + List> results = searchResults.getByResultType( clazz ); + + if ( results.size() == 0 ) + continue; + + GeneralSearchController.log.debug( String.format( "Search for: %s; result: %d %ss", + searchSettings, results.size(), clazz.getSimpleName() ) ); + + /* + * Now put the valueObjects inside the SearchResults in score order. + */ + searchService.loadValueObjects( results ).stream() + .sorted() + .map( SearchResultValueObject::new ) + .forEachOrdered( finalResults::add ); + } + + fillVosTimer.stop(); + + timer.stop(); + + if ( timer.getTime() > 500 ) { + GeneralSearchController.log + .warn( "Searching for query: " + searchSettings + " took " + timer.getTime() + " ms (" + + "searching: " + searchTimer.getTime() + " ms, " + + "filling VOs: " + fillVosTimer.getTime() + " ms)." ); + } - ModelAndView processFormSubmission( HttpServletRequest request, HttpServletResponse response, - SearchSettings command, BindException errors ) throws Exception; + return new JsonReaderResponse<>( finalResults ); + } + + @ParametersAreNonnullByDefault + private class Highlighter extends DefaultHighlighter { + + @Nullable + private final String scope; + private final Locale locale; + + private int highlightedDocuments = 0; + + private Highlighter( @Nullable String scope, Locale locale ) { + this.scope = scope; + this.locale = locale; + } + + @Override + public Map highlightTerm( @Nullable String uri, String value, String field ) { + // some of the incoming requests are from AJAX, so we cannot use fromRequest + UriComponentsBuilder builder = ServletUriComponentsBuilder.fromContextPath( request ) + .scheme( null ).host( null ).port( -1 ) + .path( "/searcher.html" ) + .queryParam( "query", uri != null ? uri : value ); + if ( scope != null ) { + builder.queryParam( "scope", scope ); + } + String searchUrl = builder.build().toUriString(); + String matchedText = "" + escapeHtml4( value ) + " "; + return Collections.singletonMap( localizeField( "ExpressionExperiment", field ), matchedText ); + } + + @Override + public Map highlightDocument( Document document, org.apache.lucene.search.highlight.Highlighter highlighter, Analyzer analyzer ) { + if ( highlightedDocuments >= MAX_HIGHLIGHTED_DOCUMENTS ) { + return Collections.emptyMap(); + } + highlightedDocuments++; + return super.highlightDocument( document, highlighter, analyzer ) + .entrySet().stream() + .collect( Collectors.toMap( e -> localizeField( StringUtils.substringAfterLast( document.get( "_hibernate_class" ), '.' ), e.getKey() ), Map.Entry::getValue, ( a, b ) -> b ) ); + } + + private String localizeField( String className, String field ) { + return messageSource.getMessage( className + "." + field, null, field, locale ); + } + } + + /** + * This is needed or you will have to specify a commandClass in the DispatcherServlet's context + */ + protected SearchSettings formBackingObject( HttpServletRequest request ) { + SearchSettings.SearchSettingsBuilder csc = SearchSettings.builder(); + csc.query( !StringUtils.isBlank( request.getParameter( "query" ) ) ? request.getParameter( "query" ) : request.getParameter( "termUri" ) ); + String taxon = request.getParameter( "taxon" ); + if ( taxon != null ) + csc.taxon( taxonService.findByScientificName( taxon ) ); + String scope = request.getParameter( "scope" ); + csc.highlighter( new Highlighter( scope, request.getLocale() ) ); + if ( StringUtils.isNotBlank( scope ) ) { + char[] scopes = scope.toCharArray(); + for ( char s : scopes ) { + boolean found = false; + for ( Scope s2 : GeneralSearchController.scopes ) { + if ( s2.scope == s ) { + csc.resultType( s2.resultType ); + found = true; + break; + } + } + if ( !found ) { + throw new IllegalArgumentException( String.format( "Unsupported value for scope: %c.", s ) ); + } + } + } + return csc.build(); + } + + protected Map> referenceData( HttpServletRequest request ) { + Map> mapping = new HashMap<>(); + + // add species + this.populateTaxonReferenceData( mapping ); + + return mapping; + } + + // private Collection filterEE( + // final Collection toFilter, SearchSettings settings ) { + // Taxon tax = settings.getTaxon(); + // if ( tax == null ) + // return toFilter; + // Collection filtered = new HashSet<>(); + // for ( ExpressionExperimentValueObject eevo : toFilter ) { + // if ( eevo.getTaxon().equalsIgnoreCase( tax.getCommonName() ) ) + // filtered.add( eevo ); + // } + // + // return filtered; + // } + + private void populateTaxonReferenceData( Map> mapping ) { + List taxa = new ArrayList<>( taxonService.loadAll() ); + taxa.sort( Comparator.comparing( Taxon::getScientificName ) ); + mapping.put( "taxa", taxa ); + } + + private static boolean searchStringValidator( String query ) { + return !StringUtils.isBlank( query ) && !( ( query.charAt( 0 ) == '%' ) || ( query.charAt( 0 ) == '*' ) ); + } + + private static SearchSettings searchSettingsFromVo( SearchSettingsValueObject settingsValueObject ) { + return SearchSettings.builder() + .query( !StringUtils.isBlank( settingsValueObject.getQuery() ) ? settingsValueObject.getQuery() : settingsValueObject.getTermUri() ) + .platformConstraint( settingsValueObject.getPlatformConstraint() ) + .taxon( settingsValueObject.getTaxon() ) + .maxResults( settingsValueObject.getMaxResults() != null ? settingsValueObject.getMaxResults() : SearchSettings.DEFAULT_MAX_RESULTS_PER_RESULT_TYPE ) + .resultTypes( resultTypesFromVo( settingsValueObject ) ) + .resultType( BlacklistedEntity.class ) + .useIndices( settingsValueObject.getUseIndices() ) + .useDatabase( settingsValueObject.getUseDatabase() ) + .useCharacteristics( settingsValueObject.getUseCharacteristics() ) + .useGo( settingsValueObject.getUseGo() ) + .build(); + } + + private String scopeFromVo( SearchSettingsValueObject settingsValueObject ) { + Set> resultTypes = resultTypesFromVo( settingsValueObject ); + StringBuilder scope = new StringBuilder(); + for ( Class resultType : resultTypes ) { + for ( Scope s : scopes ) { + if ( resultType.equals( s.resultType ) ) { + scope.append( s.scope ); + break; + } + } + } + return scope.toString(); + } + + private static Set> resultTypesFromVo( SearchSettingsValueObject valueObject ) { + Set> ret = new HashSet<>(); + if ( valueObject.getSearchExperiments() ) { + ret.add( ExpressionExperiment.class ); + } + if ( valueObject.getSearchGenes() ) { + ret.add( Gene.class ); + } + if ( valueObject.getSearchPlatforms() ) { + ret.add( ArrayDesign.class ); + } + if ( valueObject.getSearchExperimentSets() ) { + ret.add( ExpressionExperimentSet.class ); + } + if ( valueObject.getSearchProbes() ) { + ret.add( CompositeSequence.class ); + } + if ( valueObject.getSearchGeneSets() ) { + ret.add( GeneSet.class ); + } + return ret; + } @Value - class SearchResultValueObject> { + public static class SearchResultValueObject> { Class resultClass; double score; @@ -75,4 +407,4 @@ public SearchResultValueObject( SearchResult result ) { this.resultObject = result.getResultObject(); } } -} \ No newline at end of file +} diff --git a/gemma-web/src/main/java/ubic/gemma/web/controller/GeneralSearchControllerImpl.java b/gemma-web/src/main/java/ubic/gemma/web/controller/GeneralSearchControllerImpl.java deleted file mode 100644 index e4a2549f08..0000000000 --- a/gemma-web/src/main/java/ubic/gemma/web/controller/GeneralSearchControllerImpl.java +++ /dev/null @@ -1,399 +0,0 @@ -/* - * The Gemma project - * - * Copyright (c) 2007 Columbia University - * - * 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 ubic.gemma.web.controller; - -import lombok.Value; -import org.apache.commons.lang3.StringUtils; -import org.apache.commons.lang3.exception.ExceptionUtils; -import org.apache.commons.lang3.time.StopWatch; -import org.apache.lucene.analysis.Analyzer; -import org.apache.lucene.document.Document; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.context.MessageSource; -import org.springframework.stereotype.Controller; -import org.springframework.validation.BindException; -import org.springframework.web.bind.WebDataBinder; -import org.springframework.web.bind.annotation.InitBinder; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestMethod; -import org.springframework.web.servlet.ModelAndView; -import org.springframework.web.servlet.support.ServletUriComponentsBuilder; -import org.springframework.web.servlet.view.RedirectView; -import org.springframework.web.util.UriComponentsBuilder; -import ubic.gemma.core.search.DefaultHighlighter; -import ubic.gemma.core.search.SearchException; -import ubic.gemma.core.search.SearchResult; -import ubic.gemma.core.search.SearchService; -import ubic.gemma.model.analysis.expression.ExpressionExperimentSet; -import ubic.gemma.model.common.Identifiable; -import ubic.gemma.model.common.search.SearchSettings; -import ubic.gemma.model.common.search.SearchSettingsValueObject; -import ubic.gemma.model.blacklist.BlacklistedEntity; -import ubic.gemma.model.expression.arrayDesign.ArrayDesign; -import ubic.gemma.model.expression.designElement.CompositeSequence; -import ubic.gemma.model.expression.experiment.ExpressionExperiment; -import ubic.gemma.model.genome.Gene; -import ubic.gemma.model.genome.Taxon; -import ubic.gemma.model.genome.gene.GeneSet; -import ubic.gemma.persistence.service.genome.taxon.TaxonService; -import ubic.gemma.web.propertyeditor.TaxonPropertyEditor; -import ubic.gemma.web.remote.JsonReaderResponse; - -import javax.annotation.Nullable; -import javax.annotation.ParametersAreNonnullByDefault; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; -import java.net.URI; -import java.util.*; -import java.util.stream.Collectors; - -import static org.apache.commons.text.StringEscapeUtils.escapeHtml4; -import static ubic.gemma.core.search.lucene.LuceneQueryUtils.prepareTermUriQuery; - -/** - * Note: do not use parametrized collections as parameters for ajax methods in this class! Type information is lost - * during proxy creation so DWR can't figure out what type of collection the method should take. See bug 2756. Use - * arrays instead. - * - * @author klc - */ -@Controller -public class GeneralSearchControllerImpl extends BaseFormController implements GeneralSearchController { - - /** - * Maximum number of highlighted documents. - */ - private static final int MAX_HIGHLIGHTED_DOCUMENTS = 500; - - @Value - private static class Scope { - char scope; - Class resultType; - } - - /** - * List of supported scopes (or result types) for searching. - */ - private static final Scope[] scopes = { - new Scope( 'G', Gene.class ), - new Scope( 'E', ExpressionExperiment.class ), - new Scope( 'P', CompositeSequence.class ), - new Scope( 'A', ArrayDesign.class ), - new Scope( 'M', GeneSet.class ), - new Scope( 'N', ExpressionExperimentSet.class ) - }; - - @Autowired - private SearchService searchService; - @Autowired - private TaxonService taxonService; - @Autowired - private MessageSource messageSource; - - @Autowired - private HttpServletRequest request; - - @Override - public JsonReaderResponse> ajaxSearch( SearchSettingsValueObject settingsValueObject ) { - StopWatch timer = new StopWatch(); - StopWatch searchTimer = new StopWatch(); - StopWatch fillVosTimer = new StopWatch(); - - if ( settingsValueObject == null || StringUtils.isBlank( settingsValueObject.getQuery() ) || StringUtils - .isBlank( settingsValueObject.getQuery().replaceAll( "\\*", "" ) ) ) { - // FIXME validate input better, and return error. - BaseFormController.log.info( "No query or invalid." ); - // return new ListRange( finalResults ); - throw new IllegalArgumentException( "Query '" + settingsValueObject + "' was invalid" ); - } - - timer.start(); - - SearchSettings searchSettings = searchSettingsFromVo( settingsValueObject ) - .withHighlighter( new Highlighter( scopeFromVo( settingsValueObject ), request.getLocale() ) ); - - searchTimer.start(); - SearchService.SearchResultMap searchResults; - try { - searchResults = searchService.search( searchSettings ); - } catch ( SearchException e ) { - throw new IllegalArgumentException( String.format( "Invalid search settings: %s.", ExceptionUtils.getRootCause( e ) ), e ); - } - searchTimer.stop(); - - // FIXME: sort by the number of hits per class, so the smallest number of hits is at the top. - fillVosTimer.start(); - List> finalResults = new ArrayList<>(); - for ( Class clazz : searchResults.getResultTypes() ) { - List> results = searchResults.getByResultType( clazz ); - - if ( results.size() == 0 ) - continue; - - BaseFormController.log.debug( String.format( "Search for: %s; result: %d %ss", - searchSettings, results.size(), clazz.getSimpleName() ) ); - - /* - * Now put the valueObjects inside the SearchResults in score order. - */ - searchService.loadValueObjects( results ).stream() - .sorted() - .map( SearchResultValueObject::new ) - .forEachOrdered( finalResults::add ); - } - - fillVosTimer.stop(); - - timer.stop(); - - if ( timer.getTime() > 500 ) { - BaseFormController.log - .warn( "Searching for query: " + searchSettings + " took " + timer.getTime() + " ms (" - + "searching: " + searchTimer.getTime() + " ms, " - + "filling VOs: " + fillVosTimer.getTime() + " ms)." ); - } - - return new JsonReaderResponse<>( finalResults ); - } - - @ParametersAreNonnullByDefault - private class Highlighter extends DefaultHighlighter { - - @Nullable - private final String scope; - private final Locale locale; - - private int highlightedDocuments = 0; - - private Highlighter( @Nullable String scope, Locale locale ) { - this.scope = scope; - this.locale = locale; - } - - @Override - public Map highlightTerm( @Nullable String uri, String value, String field ) { - // some of the incoming requests are from AJAX, so we cannot use fromRequest - UriComponentsBuilder builder = ServletUriComponentsBuilder.fromContextPath( request ) - .scheme( null ).host( null ).port( -1 ) - .path( "/searcher.html" ) - .queryParam( "query", uri != null ? uri : value ); - if ( scope != null ) { - builder.queryParam( "scope", scope ); - } - String searchUrl = builder.build().toUriString(); - String matchedText = "" + escapeHtml4( value ) + " "; - return Collections.singletonMap( localizeField( "ExpressionExperiment", field ), matchedText ); - } - - @Override - public Map highlightDocument( Document document, org.apache.lucene.search.highlight.Highlighter highlighter, Analyzer analyzer ) { - if ( highlightedDocuments >= MAX_HIGHLIGHTED_DOCUMENTS ) { - return Collections.emptyMap(); - } - highlightedDocuments++; - return super.highlightDocument( document, highlighter, analyzer ) - .entrySet().stream() - .collect( Collectors.toMap( e -> localizeField( StringUtils.substringAfterLast( document.get( "_hibernate_class" ), '.' ), e.getKey() ), Map.Entry::getValue, ( a, b ) -> b ) ); - } - - private String localizeField( String className, String field ) { - return messageSource.getMessage( className + "." + field, null, field, locale ); - } - } - - @Override - public ModelAndView doSearch( HttpServletRequest request, HttpServletResponse response, SearchSettings command, - BindException errors ) { - - command.setQuery( StringUtils.trim( command.getQuery().trim() ) ); - - ModelAndView mav = new ModelAndView( "generalSearch" ); - - if ( !searchStringValidator( command.getQuery() ) ) { - throw new IllegalArgumentException( "Invalid query" ); - } - - // Need this for the bookmarkable links - mav.addObject( "SearchString", command.getQuery() ); - try { - URI termUri = prepareTermUriQuery( command ); - mav.addObject( "SearchURI", termUri != null ? termUri.toString() : null ); - } catch ( SearchException e ) { - mav.addObject( "SearchURI", null ); - } - if ( ( command.getTaxon() != null ) && ( command.getTaxon().getId() != null ) ) - mav.addObject( "searchTaxon", command.getTaxon().getScientificName() ); - - return mav; - } - - @Override - public ModelAndView processFormSubmission( HttpServletRequest request, HttpServletResponse response, - SearchSettings command, BindException errors ) throws Exception { - - if ( request.getParameter( "query" ) != null ) { - ModelAndView mav = new ModelAndView(); - mav.addObject( "query", request.getParameter( "query" ) ); - return mav; - } - - if ( request.getParameter( "cancel" ) != null ) { - this.saveMessage( request, "Cancelled Search" ); - return new ModelAndView( new RedirectView( WebConstants.HOME_PAGE, true ) ); - } - - return this.doSearch( request, response, command, errors ); - } - - /** - * This is needed or you will have to specify a commandClass in the DispatcherServlet's context - */ - @Override - protected SearchSettings formBackingObject( HttpServletRequest request ) { - SearchSettings.SearchSettingsBuilder csc = SearchSettings.builder(); - csc.query( !StringUtils.isBlank( request.getParameter( "query" ) ) ? request.getParameter( "query" ) : request.getParameter( "termUri" ) ); - String taxon = request.getParameter( "taxon" ); - if ( taxon != null ) - csc.taxon( taxonService.findByScientificName( taxon ) ); - String scope = request.getParameter( "scope" ); - csc.highlighter( new Highlighter( scope, request.getLocale() ) ); - if ( StringUtils.isNotBlank( scope ) ) { - char[] scopes = scope.toCharArray(); - for ( char s : scopes ) { - boolean found = false; - for ( Scope s2 : GeneralSearchControllerImpl.scopes ) { - if ( s2.scope == s ) { - csc.resultType( s2.resultType ); - found = true; - break; - } - } - if ( !found ) { - throw new IllegalArgumentException( String.format( "Unsupported value for scope: %c.", s ) ); - } - } - } - return csc.build(); - } - - @Override - @InitBinder - protected void initBinder( WebDataBinder binder ) { - super.initBinder( binder ); - binder.registerCustomEditor( Taxon.class, new TaxonPropertyEditor( this.taxonService ) ); - } - - @Deprecated - @Override - @RequestMapping(value = "/searcher.html", method = RequestMethod.GET) - public ModelAndView showForm( HttpServletRequest request, HttpServletResponse response, BindException errors ) { - if ( request.getParameter( "query" ) != null || request.getParameter( "termUri" ) != null ) { - SearchSettings searchSettings = this.formBackingObject( request ); - return this.doSearch( request, response, searchSettings, errors ); - } - - return new ModelAndView( "generalSearch" ); - } - - @Override - protected Map> referenceData( HttpServletRequest request ) { - Map> mapping = new HashMap<>(); - - // add species - this.populateTaxonReferenceData( mapping ); - - return mapping; - } - - // private Collection filterEE( - // final Collection toFilter, SearchSettings settings ) { - // Taxon tax = settings.getTaxon(); - // if ( tax == null ) - // return toFilter; - // Collection filtered = new HashSet<>(); - // for ( ExpressionExperimentValueObject eevo : toFilter ) { - // if ( eevo.getTaxon().equalsIgnoreCase( tax.getCommonName() ) ) - // filtered.add( eevo ); - // } - // - // return filtered; - // } - - private void populateTaxonReferenceData( Map> mapping ) { - List taxa = new ArrayList<>( taxonService.loadAll() ); - taxa.sort( Comparator.comparing( Taxon::getScientificName ) ); - mapping.put( "taxa", taxa ); - } - - private static boolean searchStringValidator( String query ) { - return !StringUtils.isBlank( query ) && !( ( query.charAt( 0 ) == '%' ) || ( query.charAt( 0 ) == '*' ) ); - } - - private static SearchSettings searchSettingsFromVo( SearchSettingsValueObject settingsValueObject ) { - return SearchSettings.builder() - .query( !StringUtils.isBlank( settingsValueObject.getQuery() ) ? settingsValueObject.getQuery() : settingsValueObject.getTermUri() ) - .platformConstraint( settingsValueObject.getPlatformConstraint() ) - .taxon( settingsValueObject.getTaxon() ) - .maxResults( settingsValueObject.getMaxResults() != null ? settingsValueObject.getMaxResults() : SearchSettings.DEFAULT_MAX_RESULTS_PER_RESULT_TYPE ) - .resultTypes( resultTypesFromVo( settingsValueObject ) ) - .resultType( BlacklistedEntity.class ) - .useIndices( settingsValueObject.getUseIndices() ) - .useDatabase( settingsValueObject.getUseDatabase() ) - .useCharacteristics( settingsValueObject.getUseCharacteristics() ) - .useGo( settingsValueObject.getUseGo() ) - .build(); - } - - private String scopeFromVo( SearchSettingsValueObject settingsValueObject ) { - Set> resultTypes = resultTypesFromVo( settingsValueObject ); - StringBuilder scope = new StringBuilder(); - for ( Class resultType : resultTypes ) { - for ( Scope s : scopes ) { - if ( resultType.equals( s.resultType ) ) { - scope.append( s.scope ); - break; - } - } - } - return scope.toString(); - } - - private static Set> resultTypesFromVo( SearchSettingsValueObject valueObject ) { - Set> ret = new HashSet<>(); - if ( valueObject.getSearchExperiments() ) { - ret.add( ExpressionExperiment.class ); - } - if ( valueObject.getSearchGenes() ) { - ret.add( Gene.class ); - } - if ( valueObject.getSearchPlatforms() ) { - ret.add( ArrayDesign.class ); - } - if ( valueObject.getSearchExperimentSets() ) { - ret.add( ExpressionExperimentSet.class ); - } - if ( valueObject.getSearchProbes() ) { - ret.add( CompositeSequence.class ); - } - if ( valueObject.getSearchGeneSets() ) { - ret.add( GeneSet.class ); - } - return ret; - } -} diff --git a/gemma-web/src/main/java/ubic/gemma/web/controller/expression/arrayDesign/ArrayDesignEditController.java b/gemma-web/src/main/java/ubic/gemma/web/controller/expression/arrayDesign/ArrayDesignEditController.java new file mode 100644 index 0000000000..abb1cd0922 --- /dev/null +++ b/gemma-web/src/main/java/ubic/gemma/web/controller/expression/arrayDesign/ArrayDesignEditController.java @@ -0,0 +1,120 @@ +/* + * The Gemma project + * + * Copyright (c) 2006 University of British Columbia + * + * 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 ubic.gemma.web.controller.expression.arrayDesign; + +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.propertyeditors.CustomNumberEditor; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.WebDataBinder; +import org.springframework.web.bind.annotation.InitBinder; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.multipart.support.ByteArrayMultipartFileEditor; +import org.springframework.web.servlet.ModelAndView; +import org.springframework.web.servlet.view.RedirectView; +import ubic.gemma.model.expression.arrayDesign.ArrayDesign; +import ubic.gemma.model.expression.arrayDesign.ArrayDesignValueObject; +import ubic.gemma.model.expression.arrayDesign.TechnologyType; +import ubic.gemma.persistence.service.expression.arrayDesign.ArrayDesignService; +import ubic.gemma.web.util.EntityNotFoundException; +import ubic.gemma.web.util.MessageUtil; + +import javax.servlet.http.HttpServletRequest; +import java.text.NumberFormat; +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; + +/** + * Controller for editing basic information about array designs. + * + * @author keshav + */ +@Controller +public class ArrayDesignEditController { + + private static final List TECHNOLOGY_TYPES = Arrays.stream( TechnologyType.values() ) + .map( TechnologyType::name ) + .sorted() + .collect( Collectors.toList() ); + + @Autowired + private ArrayDesignService arrayDesignService; + + @Autowired + private MessageUtil messageUtil; + + /** + * Set up a custom property editor for converting form inputs to real objects. Override this to add additional + * custom editors (call super.initBinder() in your implementation) + */ + @InitBinder + protected void initBinder( WebDataBinder binder ) { + NumberFormat nf = NumberFormat.getNumberInstance(); + binder.registerCustomEditor( Integer.class, null, new CustomNumberEditor( Integer.class, nf, true ) ); + binder.registerCustomEditor( Long.class, null, new CustomNumberEditor( Long.class, nf, true ) ); + binder.registerCustomEditor( byte[].class, new ByteArrayMultipartFileEditor() ); + } + + @RequestMapping(value = "/arrayDesign/editArrayDesign.html", method = RequestMethod.GET) + public ModelAndView getArrayDesign( @RequestParam("id") Long id ) { + return new ModelAndView( "arrayDesign.edit" ) + .addObject( "arrayDesign", formBackingObject( id ) ) + .addObject( "technologyTypes", TECHNOLOGY_TYPES ); + } + + @RequestMapping(value = "/arrayDesign/editArrayDesign.html", method = RequestMethod.POST) + public ModelAndView updateArrayDesign( ArrayDesignValueObject ad, HttpServletRequest request ) { + ArrayDesign existing = arrayDesignService.loadOrFail( ad.getId(), EntityNotFoundException::new, "No platform with ID " + ad.getId() ); + + // existing = arrayDesignService.thawLite( existing ); + existing.setDescription( ad.getDescription() ); + existing.setName( ad.getName() ); + existing.setShortName( ad.getShortName() ); + String technologyType = ad.getTechnologyType(); + if ( StringUtils.isNotBlank( technologyType ) ) { + existing.setTechnologyType( TechnologyType.valueOf( technologyType ) ); + } + + arrayDesignService.update( existing ); + + messageUtil.saveMessage( request, "object.updated", + new Object[] { ad.getClass().getSimpleName().replaceFirst( "Impl", "" ), ad.getName() }, "Saved" ); + + // go back to the array we just edited. + return new ModelAndView( new RedirectView( "/arrays/showArrayDesign.html?id=" + ad.getId(), true ) ); + } + + /** + * Case = GET: Step 1 - return instance of command class (from database). This is not called in the POST case + * because the sessionForm is set to 'true' in the constructor. This means the command object was already bound to + * the session in the GET case. + * + * @return Object + */ + protected Object formBackingObject( Long id ) { + ArrayDesignValueObject arrayDesign = arrayDesignService.loadValueObjectById( id ); + if ( arrayDesign == null ) { + throw new EntityNotFoundException( "No platform with ID " + id ); + } + return arrayDesign; + } +} diff --git a/gemma-web/src/main/java/ubic/gemma/web/controller/expression/arrayDesign/ArrayDesignFormController.java b/gemma-web/src/main/java/ubic/gemma/web/controller/expression/arrayDesign/ArrayDesignFormController.java deleted file mode 100644 index a5ad4e0c66..0000000000 --- a/gemma-web/src/main/java/ubic/gemma/web/controller/expression/arrayDesign/ArrayDesignFormController.java +++ /dev/null @@ -1,150 +0,0 @@ -/* - * The Gemma project - * - * Copyright (c) 2006 University of British Columbia - * - * 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 ubic.gemma.web.controller.expression.arrayDesign; - -import org.apache.commons.lang3.StringUtils; -import org.springframework.validation.BindException; -import org.springframework.validation.ObjectError; -import org.springframework.web.servlet.ModelAndView; -import org.springframework.web.servlet.view.RedirectView; -import ubic.gemma.model.expression.arrayDesign.ArrayDesign; -import ubic.gemma.model.expression.arrayDesign.ArrayDesignValueObject; -import ubic.gemma.model.expression.arrayDesign.TechnologyType; -import ubic.gemma.persistence.service.expression.arrayDesign.ArrayDesignService; -import ubic.gemma.web.controller.BaseFormController; - -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; -import java.util.*; -import java.util.stream.Collectors; - -import static java.util.Objects.requireNonNull; - -/** - * Controller for editing basic information about array designs. - * - * @author keshav - */ -public class ArrayDesignFormController extends BaseFormController { - - private ArrayDesignService arrayDesignService = null; - - @Override - public ModelAndView onSubmit( HttpServletRequest request, HttpServletResponse response, Object command, - BindException errors ) throws Exception { - ArrayDesignValueObject ad = ( ArrayDesignValueObject ) command; - - ArrayDesign existing = arrayDesignService.load( ad.getId() ); - - if ( existing == null ) { - errors.addError( - new ObjectError( command.toString(), null, null, "No such platform with id=" + ad.getId() ) ); - return processFormSubmission( request, response, command, errors ); - } - - // existing = arrayDesignService.thawLite( existing ); - existing.setDescription( ad.getDescription() ); - existing.setName( ad.getName() ); - existing.setShortName( ad.getShortName() ); - String technologyType = ad.getTechnologyType(); - if ( StringUtils.isNotBlank( technologyType ) ) { - existing.setTechnologyType( TechnologyType.valueOf( technologyType ) ); - } - - arrayDesignService.update( existing ); - - saveMessage( request, "object.updated", - new Object[] { ad.getClass().getSimpleName().replaceFirst( "Impl", "" ), ad.getName() }, "Saved" ); - - // go back to the aray we just edited. - return new ModelAndView( new RedirectView( "/arrays/showArrayDesign.html?id=" + ad.getId(), true ) ); - } - - @Override - public ModelAndView processFormSubmission( HttpServletRequest request, HttpServletResponse response, Object command, - BindException errors ) throws Exception { - - log.debug( "entering processFormSubmission" ); - - return super.processFormSubmission( request, response, command, errors ); - } - - public void setArrayDesignService( ArrayDesignService arrayDesignService ) { - this.arrayDesignService = arrayDesignService; - } - - /** - * Case = GET: Step 1 - return instance of command class (from database). This is not called in the POST case - * because the sessionForm is set to 'true' in the constructor. This means the command object was already bound to - * the session in the GET case. - * - * @param request http request - * @return Object - */ - @Override - protected Object formBackingObject( HttpServletRequest request ) { - - String idString = request.getParameter( "id" ); - - Long id; - ArrayDesignValueObject arrayDesign = null; - - // should be caught by validation. - if ( idString != null ) { - try { - id = Long.parseLong( idString ); - } catch ( NumberFormatException e ) { - throw new IllegalArgumentException( "Invalid ID for platform.", e ); - } - Collection ids = new HashSet(); - ids.add( id ); - Collection arrayDesigns = arrayDesignService.loadValueObjectsByIds( ids ); - if ( arrayDesigns.size() > 0 ) - arrayDesign = arrayDesigns.iterator().next(); - - } - - if ( arrayDesign == null ) { - return new ArrayDesignValueObject( -1L ); - } - return arrayDesign; - } - - @Override - protected ModelAndView getCancelView( HttpServletRequest request ) { - long id = Long.parseLong( requireNonNull( request.getParameter( "id" ), - "The 'id' query parameter is required." ) ); - // go back to the aray we just edited. - return new ModelAndView( - new RedirectView( "/arrays/showArrayDesign.html?id=" + id, true ) ); - } - - private static final List TECHNOLOGY_TYPES = Arrays.stream( TechnologyType.values() ) - .map( TechnologyType::name ) - .sorted() - .collect( Collectors.toList() ); - - @Override - protected Map> referenceData( HttpServletRequest request ) { - Map> mapping = new HashMap<>(); - mapping.put( "technologyTypes", new ArrayList<>( TECHNOLOGY_TYPES ) ); - return mapping; - } - -} diff --git a/gemma-web/src/main/java/ubic/gemma/web/controller/expression/experiment/ExpressionExperimentFormController.java b/gemma-web/src/main/java/ubic/gemma/web/controller/expression/experiment/ExpressionExperimentEditController.java similarity index 71% rename from gemma-web/src/main/java/ubic/gemma/web/controller/expression/experiment/ExpressionExperimentFormController.java rename to gemma-web/src/main/java/ubic/gemma/web/controller/expression/experiment/ExpressionExperimentEditController.java index 99b2dea2cb..a4194a8e15 100644 --- a/gemma-web/src/main/java/ubic/gemma/web/controller/expression/experiment/ExpressionExperimentFormController.java +++ b/gemma-web/src/main/java/ubic/gemma/web/controller/expression/experiment/ExpressionExperimentEditController.java @@ -19,15 +19,22 @@ package ubic.gemma.web.controller.expression.experiment; import gemma.gsec.util.SecurityUtil; -import org.apache.commons.text.StringEscapeUtils; +import lombok.extern.apachecommons.CommonsLog; import org.json.JSONObject; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.propertyeditors.CustomNumberEditor; import org.springframework.security.access.AccessDeniedException; -import org.springframework.validation.BindException; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.WebDataBinder; +import org.springframework.web.bind.annotation.InitBinder; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.multipart.support.ByteArrayMultipartFileEditor; import org.springframework.web.servlet.ModelAndView; import org.springframework.web.servlet.view.RedirectView; import ubic.gemma.core.analysis.preprocess.PreprocessingException; import ubic.gemma.core.analysis.preprocess.PreprocessorService; -import ubic.gemma.model.common.auditAndSecurity.eventType.AuditEventType; import ubic.gemma.model.common.auditAndSecurity.eventType.BioMaterialMappingUpdate; import ubic.gemma.model.common.description.DatabaseType; import ubic.gemma.model.common.description.ExternalDatabase; @@ -44,63 +51,79 @@ import ubic.gemma.persistence.service.expression.bioAssay.BioAssayService; import ubic.gemma.persistence.service.expression.biomaterial.BioMaterialService; import ubic.gemma.persistence.service.expression.experiment.ExpressionExperimentService; -import ubic.gemma.web.controller.BaseFormController; import ubic.gemma.web.util.EntityNotFoundException; +import ubic.gemma.web.util.MessageUtil; import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; +import java.text.NumberFormat; import java.util.*; import java.util.Map.Entry; import java.util.stream.Collectors; +import static org.apache.commons.text.StringEscapeUtils.escapeHtml4; + /** * Handle editing of expression experiments. * * @author keshav */ -public class ExpressionExperimentFormController extends BaseFormController { +@CommonsLog +@Controller +public class ExpressionExperimentEditController { - private AuditTrailService auditTrailService; - private BioAssayService bioAssayService = null; - private BioMaterialService bioMaterialService = null; - private ExpressionExperimentService expressionExperimentService = null; - private ExternalDatabaseService externalDatabaseService = null; - private Persister persisterHelper = null; + private static final List + STANDARD_QUANTITATION_TYPES = Arrays.stream( StandardQuantitationType.values() ).map( Enum::name ).sorted().collect( Collectors.toList() ), + SCALE_TYPES = Arrays.stream( ScaleType.values() ).map( Enum::name ).sorted().collect( Collectors.toList() ), + GENERAL_QUANTITATION_TYPES = Arrays.stream( GeneralType.values() ).map( Enum::name ).sorted().collect( Collectors.toList() ), + REPRESENTATIONS = Arrays.stream( PrimitiveType.values() ).map( Enum::name ).sorted().collect( Collectors.toList() ); + @Autowired + private AuditTrailService auditTrailService; + @Autowired + private BioAssayService bioAssayService; + @Autowired + private BioMaterialService bioMaterialService; + @Autowired + private ExpressionExperimentService expressionExperimentService; + @Autowired + private ExternalDatabaseService externalDatabaseService; + @Autowired + private Persister persisterHelper; + @Autowired private PreprocessorService preprocessorService; + @Autowired private QuantitationTypeService quantitationTypeService; + @Autowired + private MessageUtil messageUtil; - @SuppressWarnings("deprecation") - public ExpressionExperimentFormController() { - /* - * if true, reuses the same command object across the edit-submit-process (get-post-process). - */ - this.setSessionForm( true ); - this.setCommandClass( ExpressionExperimentEditValueObject.class ); + /** + * Set up a custom property editor for converting form inputs to real objects. Override this to add additional + * custom editors (call super.initBinder() in your implementation) + */ + @InitBinder + protected void initBinder( WebDataBinder binder ) { + NumberFormat nf = NumberFormat.getNumberInstance(); + binder.registerCustomEditor( Integer.class, null, new CustomNumberEditor( Integer.class, nf, true ) ); + binder.registerCustomEditor( Long.class, null, new CustomNumberEditor( Long.class, nf, true ) ); + binder.registerCustomEditor( byte[].class, new ByteArrayMultipartFileEditor() ); } - @Override - public ModelAndView processFormSubmission( HttpServletRequest request, HttpServletResponse response, Object command, - BindException errors ) throws Exception { - - BaseFormController.log.debug( "entering processFormSubmission" ); + @RequestMapping(value = "/expressionExperiment/editExpressionExperiment.html", method = RequestMethod.GET) + public ModelAndView getExpressionExperiment( @RequestParam("id") Long id, HttpServletRequest request ) { + ExpressionExperimentValueObject command = this.formBackingObject( id, request ); - Long id = ( ( ExpressionExperimentValueObject ) command ).getId(); + ExpressionExperimentEditController.log.debug( "entering processFormSubmission" ); if ( request.getParameter( "cancel" ) != null ) { if ( id != null ) { - return new ModelAndView( new RedirectView( - "http://" + request.getServerName() + ":" + request.getServerPort() + request.getContextPath() - + "/expressionExperiment/showExpressionExperiment.html?id=" + id ) ); + return new ModelAndView( new RedirectView( "/expressionExperiment/showExpressionExperiment.html?id=" + id, true ) ); } - BaseFormController.log.warn( "Cannot find details view due to null id. Redirecting to overview" ); - return new ModelAndView( new RedirectView( - "http://" + request.getServerName() + ":" + request.getServerPort() + request.getContextPath() - + "/expressionExperiment/showAllExpressionExperiments.html" ) ); + ExpressionExperimentEditController.log.warn( "Cannot find details view due to null id. Redirecting to overview" ); + return new ModelAndView( new RedirectView( "/expressionExperiment/showAllExpressionExperiments.html", true ) ); } - ModelAndView mav = super.processFormSubmission( request, response, command, errors ); + ModelAndView mav = new ModelAndView( "expressionExperiment.edit" ); ExpressionExperiment ee = expressionExperimentService.loadOrFail( id ); @@ -111,101 +134,7 @@ public ModelAndView processFormSubmission( HttpServletRequest request, HttpServl // add count of designElementDataVectors mav.addObject( "designElementDataVectorCount", expressionExperimentService.getDesignElementDataVectorCount( ee ) ); - return mav; - } - - /** - * @param auditTrailService the auditTrailService to set - */ - public void setAuditTrailService( AuditTrailService auditTrailService ) { - this.auditTrailService = auditTrailService; - } - - /** - * @param bioAssayService the bioAssayService to set - */ - public void setBioAssayService( BioAssayService bioAssayService ) { - this.bioAssayService = bioAssayService; - } - - /** - * @param bioMaterialService the bioMaterialService to set - */ - public void setBioMaterialService( BioMaterialService bioMaterialService ) { - this.bioMaterialService = bioMaterialService; - } - - public void setExpressionExperimentService( ExpressionExperimentService expressionExperimentService ) { - this.expressionExperimentService = expressionExperimentService; - } - - public void setExternalDatabaseService( ExternalDatabaseService externalDatabaseService ) { - this.externalDatabaseService = externalDatabaseService; - } - - /** - * @param persisterHelper the persisterHelper to set - */ - public void setPersisterHelper( Persister persisterHelper ) { - this.persisterHelper = persisterHelper; - } - - public void setPreprocessorService( PreprocessorService preprocessorService ) { - this.preprocessorService = preprocessorService; - } - - public void setQuantitationTypeService( QuantitationTypeService quantitationTypeService ) { - this.quantitationTypeService = quantitationTypeService; - } - - @Override - protected Object formBackingObject( HttpServletRequest request ) { - if ( !SecurityUtil.isUserLoggedIn() ) { - throw new AccessDeniedException( "User does not have access to experiment management" ); - } - - Long id; - try { - id = Long.parseLong( request.getParameter( "id" ) ); - } catch ( NumberFormatException e ) { - this.saveMessage( request, "Id was not a number " + request.getParameter( "id" ) ); - throw new IllegalArgumentException( "Id was not a number " + request.getParameter( "id" ) ); - } - - BaseFormController.log.debug( id ); - ExpressionExperimentEditValueObject obj; - - ExpressionExperiment ee = expressionExperimentService.loadAndThawLiteOrFail( id, - EntityNotFoundException::new, String.format( "No experiment with ID %d", id ) ); - List qts = new ArrayList<>( - quantitationTypeService.loadValueObjects( expressionExperimentService.getQuantitationTypes( ee ) ) ); - - ExpressionExperimentValueObject vo = expressionExperimentService.loadValueObject( ee ); - - if ( vo == null ) { - throw new EntityNotFoundException( String.format( "Could load experiment VO with ID %d", id ) ); - } - - obj = new ExpressionExperimentEditValueObject( vo ); - - obj.setQuantitationTypes( qts ); - obj.setBioAssays( BioAssayValueObject.convert2ValueObjects( ee.getBioAssays() ) ); - - this.saveMessage( request, "Editing dataset" ); - - return obj; - } - - private static final List - STANDARD_QUANTITATION_TYPES = Arrays.stream( StandardQuantitationType.values() ).map( Enum::name ).sorted().collect( Collectors.toList() ), - SCALE_TYPES = Arrays.stream( ScaleType.values() ).map( Enum::name ).sorted().collect( Collectors.toList() ), - GENERAL_QUANTITATION_TYPES = Arrays.stream( GeneralType.values() ).map( Enum::name ).sorted().collect( Collectors.toList() ), - REPRESENTATIONS = Arrays.stream( PrimitiveType.values() ).map( Enum::name ).sorted().collect( Collectors.toList() ); - - @Override - protected Map referenceData( HttpServletRequest request ) { - Map referenceData = new HashMap<>(); Collection edCol = externalDatabaseService.loadAll(); Collection keepers = new HashSet<>(); @@ -217,27 +146,25 @@ protected Map referenceData( HttpServletRequest request ) { } } - referenceData.put( "externalDatabases", keepers ); + mav.addObject( "expressionExperiment", command ); + mav.addObject( "externalDatabases", keepers ); + mav.addObject( "standardQuantitationTypes", new ArrayList<>( STANDARD_QUANTITATION_TYPES ) ); + mav.addObject( "scaleTypes", new ArrayList<>( SCALE_TYPES ) ); + mav.addObject( "generalQuantitationTypes", new ArrayList<>( GENERAL_QUANTITATION_TYPES ) ); + mav.addObject( "representations", new ArrayList<>( REPRESENTATIONS ) ); - referenceData.put( "standardQuantitationTypes", new ArrayList<>( STANDARD_QUANTITATION_TYPES ) ); - referenceData.put( "scaleTypes", new ArrayList<>( SCALE_TYPES ) ); - referenceData.put( "generalQuantitationTypes", new ArrayList<>( GENERAL_QUANTITATION_TYPES ) ); - referenceData.put( "representations", new ArrayList<>( REPRESENTATIONS ) ); - return referenceData; + return mav; } - @Override - public ModelAndView onSubmit( HttpServletRequest request, HttpServletResponse response, Object command, - BindException errors ) { - - ExpressionExperimentEditValueObject eeCommand = ( ExpressionExperimentEditValueObject ) command; + @RequestMapping(value = "/expressionExperiment/editExpressionExperiment.html", method = RequestMethod.POST) + public ModelAndView updateExpressionExperiment( ExpressionExperimentEditValueObject eeCommand, HttpServletRequest request ) { ExpressionExperiment expressionExperiment = expressionExperimentService.loadAndThawLiteOrFail( eeCommand.getId(), EntityNotFoundException::new, String.format( "No experiment with ID %d", eeCommand.getId() ) ); /* * Much more complicated */ - boolean changedQT = this.updateQuantTypes( request, expressionExperiment, eeCommand.getQuantitationTypes() ); + boolean changedQT = this.updateQuantTypes( expressionExperiment, eeCommand.getQuantitationTypes() ); boolean changedBMM = this.updateBioMaterialMap( request, expressionExperiment ); if ( changedQT || changedBMM ) { @@ -249,17 +176,36 @@ public ModelAndView onSubmit( HttpServletRequest request, HttpServletResponse re } } - return new ModelAndView( new RedirectView( - "http://" + request.getServerName() + ":" + request.getServerPort() + request.getContextPath() - + "/expressionExperiment/showExpressionExperiment.html?id=" + eeCommand.getId() ) ); + return new ModelAndView( new RedirectView( "/expressionExperiment/showExpressionExperiment.html?id=" + eeCommand.getId(), true ) ); } - private void audit( ExpressionExperiment ee, Class eventType, String note ) { - auditTrailService.addUpdateEvent( ee, eventType, note ); - } + private ExpressionExperimentEditValueObject formBackingObject( Long id, HttpServletRequest request ) { + if ( !SecurityUtil.isUserLoggedIn() ) { + throw new AccessDeniedException( "User does not have access to experiment management" ); + } + + ExpressionExperimentEditController.log.debug( id ); + + ExpressionExperiment ee = expressionExperimentService.loadAndThawLiteOrFail( id, + EntityNotFoundException::new, String.format( "No experiment with ID %d", id ) ); + + List qts = new ArrayList<>( + quantitationTypeService.loadValueObjects( expressionExperimentService.getQuantitationTypes( ee ) ) ); - private String scrub( String s ) { - return StringEscapeUtils.escapeHtml4( s ); + ExpressionExperimentValueObject vo = expressionExperimentService.loadValueObject( ee ); + + if ( vo == null ) { + throw new EntityNotFoundException( String.format( "Could load experiment VO with ID %d", id ) ); + } + + ExpressionExperimentEditValueObject obj = new ExpressionExperimentEditValueObject( vo ); + + obj.setQuantitationTypes( qts ); + obj.setBioAssays( BioAssayValueObject.convert2ValueObjects( ee.getBioAssays() ) ); + + messageUtil.saveMessage( request, "Editing dataset" ); + + return obj; } /** @@ -323,7 +269,7 @@ private boolean updateBioMaterialMap( HttpServletRequest request, ExpressionExpe "BioMaterial with id=" + newMaterialId + " could not be loaded" ); } anyChanges = true; - BaseFormController.log.info( "Associating " + bioAssay + " with " + newMaterial ); + ExpressionExperimentEditController.log.info( "Associating " + bioAssay + " with " + newMaterial ); bioAssayService.addBioMaterialAssociation( bioAssay, newMaterial ); } @@ -332,9 +278,9 @@ private boolean updateBioMaterialMap( HttpServletRequest request, ExpressionExpe * FIXME Decide if we need to remove the biomaterial -> factor value associations, it could be completely * fouled up. */ - BaseFormController.log.info( "There were changes to the BioMaterial -> BioAssay map" ); - this.audit( expressionExperiment, BioMaterialMappingUpdate.class, - newBioMaterialCount + " biomaterials" ); // remove unnecessary biomaterial associations + ExpressionExperimentEditController.log.info( "There were changes to the BioMaterial -> BioAssay map" ); + // remove unnecessary biomaterial associations + auditTrailService.addUpdateEvent( expressionExperiment, BioMaterialMappingUpdate.class, newBioMaterialCount + " biomaterials" ); Collection deleteKeys = deleteAssociations.keySet(); for ( BioAssay assay : deleteKeys ) { /* @@ -343,7 +289,7 @@ private boolean updateBioMaterialMap( HttpServletRequest request, ExpressionExpe bioAssayService.removeBioMaterialAssociation( assay, deleteAssociations.get( assay ) ); } } else { - BaseFormController.log.info( "There were no changes to the BioMaterial -> BioAssay map" ); + ExpressionExperimentEditController.log.info( "There were no changes to the BioMaterial -> BioAssay map" ); } @@ -353,12 +299,11 @@ private boolean updateBioMaterialMap( HttpServletRequest request, ExpressionExpe /** * Check old vs. new quantitation types, and update any affected data vectors. * - * @param request request * @param expressionExperiment ee * @param updatedQuantitationTypes updated QTs * @return whether any changes were made */ - private boolean updateQuantTypes( HttpServletRequest request, ExpressionExperiment expressionExperiment, + private boolean updateQuantTypes( ExpressionExperiment expressionExperiment, Collection updatedQuantitationTypes ) { Collection oldQuantitationTypes = expressionExperimentService @@ -414,8 +359,8 @@ private boolean updateQuantTypes( HttpServletRequest request, ExpressionExperime revisedType.setType( newType ); revisedType.setScale( newscale ); revisedType.setGeneralType( newgentype ); - revisedType.setDescription( this.scrub( newDescription ) ); - revisedType.setName( this.scrub( newName ) ); + revisedType.setDescription( escapeHtml4( newDescription ) ); + revisedType.setName( escapeHtml4( newName ) ); revisedType.setIsNormalized( newisNormalized ); revisedType.setIsBatchCorrected( newIsBatchCorrected ); revisedType.setIsRecomputedFromRawData( newIsRecomputedFromRawData ); @@ -429,8 +374,8 @@ private boolean updateQuantTypes( HttpServletRequest request, ExpressionExperime qType.setType( newType ); qType.setScale( newscale ); qType.setGeneralType( newgentype ); - qType.setDescription( this.scrub( newDescription ) ); - qType.setName( this.scrub( newName ) ); + qType.setDescription( escapeHtml4( newDescription ) ); + qType.setName( escapeHtml4( newName ) ); qType.setIsNormalized( newisNormalized ); qType.setIsBatchCorrected( newIsBatchCorrected ); qType.setIsRecomputedFromRawData( newIsRecomputedFromRawData ); diff --git a/gemma-web/src/main/webapp/WEB-INF/gemma-servlet.xml b/gemma-web/src/main/webapp/WEB-INF/gemma-servlet.xml index 5118a83a66..296d660c70 100644 --- a/gemma-web/src/main/webapp/WEB-INF/gemma-servlet.xml +++ b/gemma-web/src/main/webapp/WEB-INF/gemma-servlet.xml @@ -51,13 +51,12 @@ dwrController fileUploadController bibliographicReferenceController - arrayDesignFormController + arrayDesignEditController bioMaterialController compositeSequenceController experimentalDesignController - expressionExperimentFormController - + expressionExperimentEditController expressionExperimentDataFetchController expressionExperimentDataFetchController taskCompletionController @@ -159,38 +158,10 @@ - - - - - - - - - - - - - - - - - - - - - - - - - @@ -355,7 +326,7 @@ - + @@ -768,7 +739,7 @@ - + diff --git a/gemma-web/src/test/java/ubic/gemma/web/controller/expression/arrayDesign/ArrayDesignEditControllerTest.java b/gemma-web/src/test/java/ubic/gemma/web/controller/expression/arrayDesign/ArrayDesignEditControllerTest.java new file mode 100644 index 0000000000..b97ebd9fd7 --- /dev/null +++ b/gemma-web/src/test/java/ubic/gemma/web/controller/expression/arrayDesign/ArrayDesignEditControllerTest.java @@ -0,0 +1,55 @@ +package ubic.gemma.web.controller.expression.arrayDesign; + +import org.junit.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.test.context.ContextConfiguration; +import ubic.gemma.core.context.TestComponent; +import ubic.gemma.model.expression.arrayDesign.ArrayDesignValueObject; +import ubic.gemma.persistence.service.expression.arrayDesign.ArrayDesignService; +import ubic.gemma.web.util.BaseWebTest; +import ubic.gemma.web.util.MessageUtil; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; + +@ContextConfiguration +public class ArrayDesignEditControllerTest extends BaseWebTest { + + @Configuration + @TestComponent + static class ArrayDesignEditControllerTestContextConfiguration extends BaseWebTestContextConfiguration { + + @Bean + public ArrayDesignEditController arrayDesignFormController() { + return new ArrayDesignEditController(); + } + + @Bean + public ArrayDesignService arrayDesignService() { + return mock(); + } + + @Bean + public MessageUtil messageUtil() { + return mock(); + } + } + + @Autowired + private ArrayDesignService arrayDesignService; + + @Test + public void test() throws Exception { + ArrayDesignValueObject ad = new ArrayDesignValueObject(); + when( arrayDesignService.loadValueObjectById( 2L ) ).thenReturn( ad ); + perform( get( "/arrayDesign/editArrayDesign.html?id={id}", 2L ) ) + .andExpect( status().isOk() ) + .andExpect( view().name( "arrayDesign.edit" ) ) + .andExpect( model().attribute( "arrayDesign", ad ) ) + .andExpect( model().attributeExists( "technologyTypes" ) ); + } +} \ No newline at end of file diff --git a/gemma-web/src/test/java/ubic/gemma/web/controller/expression/experiment/ExpressionExperimentEditControllerTest.java b/gemma-web/src/test/java/ubic/gemma/web/controller/expression/experiment/ExpressionExperimentEditControllerTest.java new file mode 100644 index 0000000000..d845dcb36f --- /dev/null +++ b/gemma-web/src/test/java/ubic/gemma/web/controller/expression/experiment/ExpressionExperimentEditControllerTest.java @@ -0,0 +1,107 @@ +package ubic.gemma.web.controller.expression.experiment; + +import org.junit.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.test.context.support.WithMockUser; +import org.springframework.security.test.context.support.WithSecurityContextTestExecutionListener; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.TestExecutionListeners; +import ubic.gemma.core.analysis.preprocess.PreprocessorService; +import ubic.gemma.core.context.TestComponent; +import ubic.gemma.model.expression.experiment.ExpressionExperiment; +import ubic.gemma.model.expression.experiment.ExpressionExperimentValueObject; +import ubic.gemma.persistence.persister.Persister; +import ubic.gemma.persistence.service.common.auditAndSecurity.AuditTrailService; +import ubic.gemma.persistence.service.common.description.ExternalDatabaseService; +import ubic.gemma.persistence.service.common.quantitationtype.QuantitationTypeService; +import ubic.gemma.persistence.service.expression.bioAssay.BioAssayService; +import ubic.gemma.persistence.service.expression.biomaterial.BioMaterialService; +import ubic.gemma.persistence.service.expression.experiment.ExpressionExperimentService; +import ubic.gemma.web.util.BaseWebTest; +import ubic.gemma.web.util.MessageUtil; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.view; + +@ContextConfiguration +@TestExecutionListeners(WithSecurityContextTestExecutionListener.class) +public class ExpressionExperimentEditControllerTest extends BaseWebTest { + + + @Configuration + @TestComponent + static class ExpressionExperimentEditControllerTestContextConfiguration { + + @Bean + public ExpressionExperimentEditController expressionExperimentFormController() { + return new ExpressionExperimentEditController(); + } + + @Bean + public AuditTrailService auditTrailService() { + return mock(); + } + + @Bean + public BioAssayService bioAssayService() { + return mock(); + } + + @Bean + public BioMaterialService bioMaterialService() { + return mock(); + } + + @Bean + public ExpressionExperimentService expressionExperimentService() { + return mock(); + } + + @Bean + public ExternalDatabaseService externalDatabaseService() { + return mock(); + } + + @Bean + public Persister persisterHelper() { + return mock(); + } + + @Bean + public PreprocessorService preprocessorService() { + return mock(); + } + + @Bean + public QuantitationTypeService quantitationTypeService() { + return mock(); + } + + @Bean + public MessageUtil messageUtil() { + return mock(); + } + } + + @Autowired + private ExpressionExperimentService expressionExperimentService; + + @Test + @WithMockUser + public void test() throws Exception { + ExpressionExperiment ee = new ExpressionExperiment(); + ExpressionExperimentValueObject eeVo = new ExpressionExperimentValueObject(); + when( expressionExperimentService.loadAndThawLiteOrFail( eq( 2L ), any(), any() ) ).thenReturn( ee ); + when( expressionExperimentService.loadValueObject( ee ) ).thenReturn( eeVo ); + perform( get( "/expressionExperiment/editExpressionExperiment.html?id={id}", 2L ) ) + .andExpect( status().isOk() ) + .andExpect( view().name( "expressionExperiment.edit" ) ); + } +} \ No newline at end of file