/******************************************************************************* * Copyright (c) 2014, 2015 IBH SYSTEMS GmbH. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html * * Contributors: * IBH SYSTEMS GmbH - initial API and implementation *******************************************************************************/ package org.eclipse.packagedrone.repo.channel.web.channel; import static com.google.common.net.UrlEscapers.urlPathSegmentEscaper; import static java.util.Optional.empty; import static java.util.Optional.of; import static java.util.Optional.ofNullable; import static javax.servlet.annotation.ServletSecurity.EmptyRoleSemantic.PERMIT; import java.io.IOException; import java.io.UnsupportedEncodingException; import java.net.URISyntaxException; import java.net.URLEncoder; import java.time.Instant; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.Comparator; import java.util.HashMap; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.Set; import java.util.TreeMap; import java.util.function.BiConsumer; import javax.servlet.ServletException; import javax.servlet.annotation.HttpConstraint; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.Part; import javax.validation.Valid; import javax.xml.ws.Holder; import org.apache.http.client.utils.URIBuilder; import org.eclipse.packagedrone.repo.ChannelAspectInformation; import org.eclipse.packagedrone.repo.MetaKey; import org.eclipse.packagedrone.repo.aspect.ChannelAspectProcessor; import org.eclipse.packagedrone.repo.aspect.group.GroupInformation; import org.eclipse.packagedrone.repo.aspect.recipe.RecipeInformation; import org.eclipse.packagedrone.repo.aspect.recipe.RecipeNotFoundException; import org.eclipse.packagedrone.repo.channel.ArtifactInformation; import org.eclipse.packagedrone.repo.channel.AspectableChannel; import org.eclipse.packagedrone.repo.channel.ChannelArtifactInformation; import org.eclipse.packagedrone.repo.channel.ChannelDetails; import org.eclipse.packagedrone.repo.channel.ChannelId; import org.eclipse.packagedrone.repo.channel.ChannelInformation; import org.eclipse.packagedrone.repo.channel.ChannelNotFoundException; import org.eclipse.packagedrone.repo.channel.ChannelService; import org.eclipse.packagedrone.repo.channel.ChannelService.By; import org.eclipse.packagedrone.repo.channel.ChannelService.ChannelOperation; import org.eclipse.packagedrone.repo.channel.DeployKeysChannelAdapter; import org.eclipse.packagedrone.repo.channel.DescriptorAdapter; import org.eclipse.packagedrone.repo.channel.ModifiableChannel; import org.eclipse.packagedrone.repo.channel.ReadableChannel; import org.eclipse.packagedrone.repo.channel.deploy.DeployAuthService; import org.eclipse.packagedrone.repo.channel.deploy.DeployGroup; import org.eclipse.packagedrone.repo.channel.deploy.DeployKey; import org.eclipse.packagedrone.repo.channel.util.DownloadHelper; import org.eclipse.packagedrone.repo.channel.web.Tags; import org.eclipse.packagedrone.repo.channel.web.breadcrumbs.Breadcrumbs; import org.eclipse.packagedrone.repo.channel.web.breadcrumbs.Breadcrumbs.Entry; import org.eclipse.packagedrone.repo.channel.web.internal.Activator; import org.eclipse.packagedrone.repo.generator.GeneratorProcessor; import org.eclipse.packagedrone.repo.manage.system.SitePrefixService; import org.eclipse.packagedrone.repo.web.sitemap.ChangeFrequency; import org.eclipse.packagedrone.repo.web.sitemap.SitemapExtender; import org.eclipse.packagedrone.repo.web.sitemap.UrlSetContext; import org.eclipse.packagedrone.repo.web.utils.Channels; import org.eclipse.packagedrone.sec.web.controller.HttpContraintControllerInterceptor; import org.eclipse.packagedrone.sec.web.controller.Secured; import org.eclipse.packagedrone.sec.web.controller.SecuredControllerInterceptor; import org.eclipse.packagedrone.web.Controller; import org.eclipse.packagedrone.web.LinkTarget; import org.eclipse.packagedrone.web.ModelAndView; import org.eclipse.packagedrone.web.RequestMapping; import org.eclipse.packagedrone.web.RequestMethod; import org.eclipse.packagedrone.web.ViewResolver; import org.eclipse.packagedrone.web.common.CommonController; import org.eclipse.packagedrone.web.common.InterfaceExtender; import org.eclipse.packagedrone.web.common.Modifier; import org.eclipse.packagedrone.web.common.menu.MenuEntry; import org.eclipse.packagedrone.web.common.page.Pagination; import org.eclipse.packagedrone.web.controller.ControllerInterceptor; import org.eclipse.packagedrone.web.controller.ProfilerControllerInterceptor; import org.eclipse.packagedrone.web.controller.binding.BindingResult; import org.eclipse.packagedrone.web.controller.binding.PathVariable; import org.eclipse.packagedrone.web.controller.binding.RequestParameter; import org.eclipse.packagedrone.web.controller.form.FormData; import org.eclipse.packagedrone.web.controller.validator.ControllerValidator; import org.eclipse.packagedrone.web.controller.validator.ValidationContext; import org.eclipse.scada.utils.ExceptionHelper; import org.osgi.framework.FrameworkUtil; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.google.common.io.ByteStreams; import com.google.common.net.UrlEscapers; import com.google.gson.GsonBuilder; @Secured @Controller @ViewResolver ( "/WEB-INF/views/%s.jsp" ) @ControllerInterceptor ( SecuredControllerInterceptor.class ) @HttpConstraint ( rolesAllowed = "MANAGER" ) @ControllerInterceptor ( HttpContraintControllerInterceptor.class ) @ControllerInterceptor ( ProfilerControllerInterceptor.class ) public class ChannelController implements InterfaceExtender, SitemapExtender { private static final int DEFAULT_MAX_WEB_SIZE = 10_000; public static final String DRONE_WEB_MAX_LIST_SIZE = "drone.web.maxListSize"; private static final String DEFAULT_EXAMPLE_KEY = "xxxxx"; private final static Logger logger = LoggerFactory.getLogger ( ChannelController.class ); private final static List<MenuEntry> menuEntries = Collections.singletonList ( new MenuEntry ( "Channels", 100, new LinkTarget ( "/channel" ), Modifier.DEFAULT, null ) ); private DeployAuthService deployAuthService; private SitePrefixService sitePrefix; private ChannelService channelService; private final GeneratorProcessor generators = new GeneratorProcessor ( FrameworkUtil.getBundle ( ChannelController.class ).getBundleContext () ); public void setChannelService ( final ChannelService channelService ) { this.channelService = channelService; } public void setDeployAuthService ( final DeployAuthService deployAuthService ) { this.deployAuthService = deployAuthService; } public void setSitePrefixService ( final SitePrefixService sitePrefix ) { this.sitePrefix = sitePrefix; } public void start () { this.generators.open (); } public void stop () { this.generators.close (); } @Override public List<MenuEntry> getMainMenuEntries ( final HttpServletRequest request ) { return menuEntries; } @Secured ( false ) @RequestMapping ( value = "/channel", method = RequestMethod.GET ) @HttpConstraint ( PERMIT ) public ModelAndView list ( @RequestParameter ( value = "start", required = false ) final Integer startPage) { final ModelAndView result = new ModelAndView ( "channel/list" ); final List<ChannelInformation> channels = new ArrayList<> ( this.channelService.list () ); channels.sort ( ChannelId.NAME_COMPARATOR ); result.put ( "channels", Pagination.paginate ( startPage, 10, channels ) ); return result; } @RequestMapping ( value = "/channel/create", method = RequestMethod.GET ) public ModelAndView create () { // FIXME: with provider id this.channelService.create ( null, null ); return new ModelAndView ( "redirect:/channel" ); } @RequestMapping ( value = "/channel/createDetailed", method = RequestMethod.GET ) public ModelAndView createDetailed () { final Map<String, Object> model = new HashMap<> ( 1 ); model.put ( "command", new CreateChannel () ); return new ModelAndView ( "channel/create", model ); } @RequestMapping ( value = "/channel/createDetailed", method = RequestMethod.POST ) public ModelAndView createDetailedPost ( @Valid @FormData ( "command" ) final CreateChannel data, final BindingResult result) { if ( !result.hasErrors () ) { final ChannelDetails desc = new ChannelDetails (); desc.setDescription ( data.getDescription () ); // FIXME: with provider id final ChannelId channel = this.channelService.create ( null, desc ); setChannelName ( channel, data.getName () ); return new ModelAndView ( String.format ( "redirect:/channel/%s/view", urlPathSegmentEscaper ().escape ( channel.getId () ) ) ); } return new ModelAndView ( "channel/create" ); } @RequestMapping ( value = "/channel/createWithRecipe", method = RequestMethod.GET ) public ModelAndView createWithRecipe () { final Map<String, Object> model = new HashMap<> ( 2 ); model.put ( "command", new CreateChannel () ); model.put ( "recipes", Activator.getRecipes ().getSortedRecipes ( RecipeInformation::getLabel ) ); return new ModelAndView ( "channel/createWithRecipe", model ); } @RequestMapping ( value = "/channel/createWithRecipe", method = RequestMethod.POST ) public ModelAndView createWithRecipePost ( @Valid @FormData ( "command" ) final CreateChannel data, @RequestParameter ( required = false, value = "recipe" ) final String recipeId, final BindingResult result) throws UnsupportedEncodingException, RecipeNotFoundException { if ( !result.hasErrors () ) { final Holder<ChannelId> holder = new Holder<> (); final Holder<String> targetHolder = new Holder<> (); if ( recipeId == null || recipeId.isEmpty () ) { // without recipe final ChannelDetails desc = new ChannelDetails (); desc.setDescription ( data.getDescription () ); //FIXME: add provider id holder.value = this.channelService.create ( null, desc ); setChannelName ( holder.value, data.getName () ); } else { // with recipe Activator.getRecipes ().process ( recipeId, recipe -> { final ChannelDetails desc = new ChannelDetails (); desc.setDescription ( data.getDescription () ); //FIXME: add provider id final ChannelId channel = this.channelService.create ( null, desc ); setChannelName ( channel, data.getName () ); this.channelService.accessRun ( By.id ( channel.getId () ), AspectableChannel.class, aspChannel -> { final LinkTarget target = recipe.setup ( channel.getId (), aspChannel ); if ( target != null ) { final Map<String, String> model = new HashMap<> ( 1 ); model.put ( "channelId", channel.getId () ); targetHolder.value = target.expand ( model ).getUrl (); } } ); holder.value = channel; } ); if ( targetHolder.value != null ) { return new ModelAndView ( "redirect:" + targetHolder.value ); } } return new ModelAndView ( String.format ( "redirect:/channel/%s/view", URLEncoder.encode ( holder.value.getId (), "UTF-8" ) ) ); } final Map<String, Object> model = new HashMap<> ( 1 ); model.put ( "recipes", Activator.getRecipes ().getSortedRecipes ( RecipeInformation::getLabel ) ); return new ModelAndView ( "channel/createWithRecipe", model ); } protected void setChannelName ( final ChannelId id, final String name ) { this.channelService.accessRun ( By.id ( id.getId () ), DescriptorAdapter.class, channel -> { channel.setName ( name ); } ); } @Secured ( false ) @RequestMapping ( value = "/channel/{channelId}/view", method = RequestMethod.GET ) @HttpConstraint ( PERMIT ) public ModelAndView view ( @PathVariable ( "channelId" ) final String channelId, final HttpServletRequest request, final HttpServletResponse response) throws ServletException, IOException { final Optional<ChannelInformation> channel = this.channelService.getState ( By.name ( channelId ) ); if ( channel.isPresent () ) { return new ModelAndView ( String.format ( "redirect:/channel/%s/view", channel.get ().getId () ) ); } else { request.getRequestDispatcher ( "tree" ).forward ( request, response ); return null; } } @Secured ( false ) @RequestMapping ( value = "/channel/{channelId}/viewPlain", method = RequestMethod.GET ) @HttpConstraint ( PERMIT ) public ModelAndView viewPlain ( @PathVariable ( "channelId" ) final String channelId) { try { return this.channelService.accessCall ( By.id ( channelId ), ReadableChannel.class, ( channel ) -> { final Map<String, Object> model = new HashMap<> (); model.put ( "channel", channel.getInformation () ); final Collection<ArtifactInformation> artifacts = channel.getContext ().getArtifacts ().values (); if ( artifacts.size () > maxWebListSize () ) { return viewTooMany ( channel ); } // sort artifacts final List<ArtifactInformation> sortedArtifacts = new ArrayList<> ( artifacts ); sortedArtifacts.sort ( Comparator.comparing ( ArtifactInformation::getName ) ); model.put ( "sortedArtifacts", sortedArtifacts ); return new ModelAndView ( "channel/view", model ); } ); } catch ( final ChannelNotFoundException e ) { return CommonController.createNotFound ( "channel", channelId ); } } private ModelAndView viewTooMany ( final ReadableChannel channel ) { final Map<String, Object> model = new HashMap<> (); model.put ( "channel", channel.getInformation () ); model.put ( "numberOfArtifacts", channel.getArtifacts ().size () ); model.put ( "maxNumberOfArtifacts", maxWebListSize () ); model.put ( "propertyName", DRONE_WEB_MAX_LIST_SIZE ); return new ModelAndView ( "channel/viewTooMany", model ); } @Secured ( false ) @RequestMapping ( value = "/channel/{channelId}/tree", method = RequestMethod.GET ) @HttpConstraint ( PERMIT ) public ModelAndView tree ( @PathVariable ( "channelId" ) final String channelId) { try { return this.channelService.accessCall ( By.id ( channelId ), ReadableChannel.class, ( channel ) -> { if ( channel.getContext ().getArtifacts ().size () > maxWebListSize () ) { return viewTooMany ( channel ); } final ModelAndView result = new ModelAndView ( "channel/tree" ); final Map<String, List<ArtifactInformation>> tree = new HashMap<> (); for ( final ArtifactInformation entry : channel.getContext ().getArtifacts ().values () ) { List<ArtifactInformation> list = tree.get ( entry.getParentId () ); if ( list == null ) { list = new LinkedList<> (); tree.put ( entry.getParentId (), list ); } list.add ( entry ); } result.put ( "channel", channel.getInformation () ); result.put ( "treeArtifacts", tree ); result.put ( "treeSeverityTester", new TreeTesterImpl ( tree ) ); return result; } ); } catch ( final ChannelNotFoundException e ) { return CommonController.createNotFound ( "channel", channelId ); } } private Integer maxWebListSize () { return Integer.getInteger ( DRONE_WEB_MAX_LIST_SIZE, DEFAULT_MAX_WEB_SIZE ); } @Secured ( false ) @RequestMapping ( value = "/channel/{channelId}/validation", method = RequestMethod.GET ) @HttpConstraint ( PERMIT ) public ModelAndView viewValidation ( @PathVariable ( "channelId" ) final String channelId) { try { return this.channelService.accessCall ( By.id ( channelId ), ReadableChannel.class, channel -> { final ModelAndView result = new ModelAndView ( "channel/validation" ); result.put ( "channel", channel.getInformation () ); result.put ( "messages", channel.getInformation ().getState ().getValidationMessages () ); result.put ( "aspects", Activator.getAspects ().getAspectInformations () ); return result; } ); } catch ( final ChannelNotFoundException e ) { return CommonController.createNotFound ( "channel", channelId ); } } @Secured ( false ) @RequestMapping ( value = "/channel/{channelId}/details", method = RequestMethod.GET ) @HttpConstraint ( PERMIT ) public ModelAndView details ( @PathVariable ( "channelId" ) final String channelId) { final ModelAndView result = new ModelAndView ( "channel/details" ); try { this.channelService.accessRun ( By.id ( channelId ), ReadableChannel.class, ( channel ) -> { result.put ( "channel", channel.getInformation () ); } ); } catch ( final ChannelNotFoundException e ) { return CommonController.createNotFound ( "channel", channelId ); } return result; } @RequestMapping ( value = "/channel/{channelId}/delete", method = RequestMethod.GET ) public ModelAndView delete ( @PathVariable ( "channelId" ) final String channelId) { final ModelAndView result = new ModelAndView ( "redirect:/channel" ); if ( this.channelService.delete ( By.id ( channelId ) ) ) { result.put ( "success", String.format ( "Deleted channel %s", channelId ) ); } else { result.put ( "warning", String.format ( "Unable to delete channel %s. Was not found.", channelId ) ); } return result; } @RequestMapping ( value = "/channel/{channelId}/artifacts/{artifactId}/delete", method = RequestMethod.GET ) public ModelAndView deleteArtifact ( @PathVariable ( "channelId" ) final String channelId, @PathVariable ( "artifactId" ) final String artifactId) { return withChannel ( channelId, ModifiableChannel.class, channel -> { channel.getContext ().deleteArtifact ( artifactId ); return redirectDefaultView ( channelId, true ); } ); } @RequestMapping ( value = "/channel/{channelId}/artifacts/{artifactId}/get", method = RequestMethod.GET ) public void getArtifact ( @PathVariable ( "channelId" ) final String channelId, @PathVariable ( "artifactId" ) final String artifactId, final HttpServletResponse response) throws IOException { DownloadHelper.streamArtifact ( response, this.channelService, channelId, artifactId, null, true ); } @RequestMapping ( value = "/channel/{channelId}/artifacts/{artifactId}/dump", method = RequestMethod.GET ) public void dumpArtifact ( @PathVariable ( "channelId" ) final String channelId, @PathVariable ( "artifactId" ) final String artifactId, final HttpServletResponse response) throws IOException { DownloadHelper.streamArtifact ( response, this.channelService, channelId, artifactId, null, false ); } @RequestMapping ( value = "/channel/{channelId}/artifacts/{artifactId}/view", method = RequestMethod.GET ) public ModelAndView viewArtifact ( @PathVariable ( "channelId" ) final String channelId, @PathVariable ( "artifactId" ) final String artifactId) { return withChannel ( channelId, ReadableChannel.class, channel -> { final Optional<ChannelArtifactInformation> artifact = channel.getArtifact ( artifactId ); if ( !artifact.isPresent () ) { return CommonController.createNotFound ( "aspect", artifactId ); } final Map<String, Object> model = new HashMap<String, Object> ( 1 ); model.put ( "artifact", artifact.get () ); model.put ( "sortedMetaData", new TreeMap<> ( artifact.get ().getMetaData () ) ); model.put ( "aspects", Activator.getAspects ().getAspectInformations () ); return new ModelAndView ( "artifact/view", model ); } ); } @RequestMapping ( value = "/channel/{channelId}/add", method = RequestMethod.GET ) public ModelAndView add ( @PathVariable ( "channelId" ) final String channelId) { final ModelAndView mav = new ModelAndView ( "/channel/add" ); mav.put ( "generators", this.generators.getInformations ().values () ); mav.put ( "channelId", channelId ); return mav; } @RequestMapping ( value = "/channel/{channelId}/add", method = RequestMethod.POST ) public ModelAndView addPost ( @PathVariable ( "channelId" ) final String channelId, @RequestParameter ( required = false, value = "name" ) String name, final @RequestParameter ( "file" ) Part file) { try { if ( name == null || name.isEmpty () ) { name = file.getSubmittedFileName (); } final String finalName = name; this.channelService.accessRun ( By.id ( channelId ), ModifiableChannel.class, channel -> { channel.getContext ().createArtifact ( file.getInputStream (), finalName, null ); } ); return redirectDefaultView ( channelId, true ); } catch ( final Exception e ) { return CommonController.createError ( "Upload", "Upload failed", e ); } } @RequestMapping ( value = "/channel/{channelId}/drop", method = RequestMethod.POST ) public void drop ( @PathVariable ( "channelId" ) final String channelId, @RequestParameter ( required = false, value = "name" ) String name, final @RequestParameter ( "file" ) Part file, final HttpServletResponse response) throws IOException { response.setContentType ( "text/plain" ); try { if ( name == null || name.isEmpty () ) { name = file.getSubmittedFileName (); } final String finalName = name; this.channelService.accessRun ( By.id ( channelId ), ModifiableChannel.class, channel -> { channel.getContext ().createArtifact ( file.getInputStream (), finalName, null ); } ); } catch ( final Throwable e ) { logger.debug ( "Failed to drop file", e ); response.setStatus ( HttpServletResponse.SC_INTERNAL_SERVER_ERROR ); response.getWriter ().write ( "Internal error: " + ExceptionHelper.getMessage ( e ) ); return; } response.setStatus ( HttpServletResponse.SC_OK ); response.getWriter ().write ( "OK" ); } @RequestMapping ( value = "/channel/{channelId}/clear", method = RequestMethod.GET ) public ModelAndView clear ( @PathVariable ( "channelId" ) final String channelId) { return withChannel ( channelId, ModifiableChannel.class, channel -> { channel.getContext ().clear (); return redirectDefaultView ( channelId, true ); } ); } protected ModelAndView redirectDefaultView ( final String channelId, final boolean force ) { return new ModelAndView ( ( force ? "redirect" : "referer" ) + ":/channel/" + channelId + "/view" ); } @RequestMapping ( value = "/channel/{channelId}/deployKeys" ) public ModelAndView deployKeys ( @PathVariable ( "channelId" ) final String channelId) { return withChannel ( channelId, DeployKeysChannelAdapter.class, deployChannel -> { return withChannel ( channelId, ReadableChannel.class, channel -> { final Map<String, Object> model = new HashMap<> (); final List<DeployGroup> channelDeployGroups = new ArrayList<> ( deployChannel.getDeployGroups () ); Collections.sort ( channelDeployGroups, DeployGroup.NAME_COMPARATOR ); model.put ( "channel", channel.getInformation () ); model.put ( "channelDeployGroups", channelDeployGroups ); model.put ( "deployGroups", getGroupsForChannel ( channelDeployGroups ) ); model.put ( "sitePrefix", this.sitePrefix.getSitePrefix () ); return new ModelAndView ( "channel/deployKeys", model ); } ); } ); } protected List<DeployGroup> getGroupsForChannel ( final Collection<DeployGroup> channelDeployGroups ) { final List<DeployGroup> groups = new ArrayList<> ( this.deployAuthService.listGroups ( 0, -1 ) ); groups.removeAll ( channelDeployGroups ); Collections.sort ( groups, DeployGroup.NAME_COMPARATOR ); return groups; } protected <T> ModelAndView withChannel ( final String channelId, final Class<T> clazz, final ChannelOperation<ModelAndView, T> operation ) { return Channels.withChannel ( this.channelService, channelId, clazz, operation ); } @RequestMapping ( "/channel/{channelId}/help/p2" ) @Secured ( false ) @HttpConstraint ( PERMIT ) public ModelAndView helpP2 ( @PathVariable ( "channelId" ) final String channelId) { return withChannel ( channelId, ReadableChannel.class, channel -> { final Map<String, Object> model = new HashMap<> (); model.put ( "channel", channel.getInformation () ); model.put ( "sitePrefix", this.sitePrefix.getSitePrefix () ); model.put ( "p2Active", channel.hasAspect ( "p2.repo" ) ); return new ModelAndView ( "channel/help/p2", model ); } ); } @RequestMapping ( "/channel/{channelId}/help/api" ) @Secured ( false ) @HttpConstraint ( PERMIT ) public ModelAndView helpApi ( @PathVariable ( "channelId" ) final String channelId, final HttpServletRequest request) { return withChannel ( channelId, ReadableChannel.class, channel -> { final Map<String, Object> model = new HashMap<> (); model.put ( "channel", channel.getInformation () ); model.put ( "sitePrefix", this.sitePrefix.getSitePrefix () ); final String exampleKey; if ( request.isUserInRole ( "MANAGER" ) ) { exampleKey = this.channelService.getChannelDeployKeys ( By.id ( channel.getId ().getId () ) ).orElse ( Collections.emptyList () ).stream ().map ( DeployKey::getKey ).findFirst ().orElse ( DEFAULT_EXAMPLE_KEY ); } else { exampleKey = DEFAULT_EXAMPLE_KEY; } model.put ( "exampleKey", exampleKey ); model.put ( "exampleSitePrefix", makeCredentialsPrefix ( this.sitePrefix.getSitePrefix (), "deploy", exampleKey ) ); return new ModelAndView ( "channel/help/api", model ); } ); } private String makeCredentialsPrefix ( final String sitePrefix, final String name, final String password ) { try { final URIBuilder builder = new URIBuilder ( sitePrefix ); builder.setUserInfo ( name, password ); return builder.build ().toString (); } catch ( final URISyntaxException e ) { return sitePrefix; } } @RequestMapping ( value = "/channel/{channelId}/addDeployGroup", method = RequestMethod.POST ) public ModelAndView addDeployGroup ( @PathVariable ( "channelId" ) final String channelId, @RequestParameter ( "groupId" ) final String groupId) { return modifyDeployGroup ( channelId, groupId, DeployKeysChannelAdapter::assignDeployGroup ); } @RequestMapping ( value = "/channel/{channelId}/removeDeployGroup", method = RequestMethod.POST ) public ModelAndView removeDeployGroup ( @PathVariable ( "channelId" ) final String channelId, @RequestParameter ( "groupId" ) final String groupId) { return modifyDeployGroup ( channelId, groupId, DeployKeysChannelAdapter::unassignDeployGroup ); } protected ModelAndView modifyDeployGroup ( final String channelId, final String groupId, final BiConsumer<DeployKeysChannelAdapter, String> cons ) { return withChannel ( channelId, DeployKeysChannelAdapter.class, channel -> { cons.accept ( channel, groupId ); return new ModelAndView ( "redirect:/channel/" + channelId + "/deployKeys" ); } ); } @Secured ( false ) @RequestMapping ( value = "/channel/{channelId}/aspects", method = RequestMethod.GET ) @HttpConstraint ( PERMIT ) public ModelAndView aspects ( @PathVariable ( "channelId" ) final String channelId) { return withChannel ( channelId, ReadableChannel.class, channel -> { final ModelAndView model = new ModelAndView ( "channel/aspects" ); final ChannelAspectProcessor aspects = Activator.getAspects (); final Collection<GroupInformation> groups = aspects.getGroups (); model.put ( "channel", channel.getInformation () ); final Set<String> assigned = channel.getInformation ().getAspectStates ().keySet (); final List<AspectInformation> allAspects = AspectInformation.resolve ( groups, aspects.getAspectInformations ().values () ); final List<AspectInformation> assignedAspects = AspectInformation.filterIds ( allAspects, ( id ) -> assigned.contains ( id ) ); model.put ( "assignedAspects", assignedAspects ); model.put ( "groupedAssignedAspects", AspectInformation.group ( assignedAspects ) ); model.put ( "addAspects", AspectInformation.group ( AspectInformation.filterIds ( allAspects, ( id ) -> !assigned.contains ( id ) ) ) ); final Map<String, String> nameMap = new HashMap<> (); for ( final AspectInformation ai : allAspects ) { nameMap.put ( ai.getFactoryId (), ai.getName () ); } model.put ( "nameMapJson", new GsonBuilder ().create ().toJson ( nameMap ) ); model.put ( "breadcrumbs", new Breadcrumbs ( new Entry ( "Home", "/" ), Breadcrumbs.create ( "Channel", ChannelController.class, "view", "channelId", channelId ), new Entry ( "Aspects" ) ) ); return model; } ); } @RequestMapping ( value = "/channel/{channelId}/viewAspectVersions", method = RequestMethod.GET ) public ModelAndView viewAspectVersions ( @PathVariable ( "channelId" ) final String channelId) { return withChannel ( channelId, ReadableChannel.class, channel -> { final Map<String, String> states = channel.getInformation ().getAspectStates (); final List<ChannelAspectInformation> aspects = Activator.getAspects ().resolve ( states.keySet () ); Collections.sort ( aspects, ChannelAspectInformation.NAME_COMPARATOR ); final Map<String, Object> model = new HashMap<> ( 3 ); model.put ( "channel", channel.getInformation () ); model.put ( "states", states ); model.put ( "aspects", aspects ); return new ModelAndView ( "channel/viewAspectVersions", model ); } ); } @RequestMapping ( value = "/channel/{channelId}/lock", method = RequestMethod.GET ) public ModelAndView lock ( @PathVariable ( "channelId" ) final String channelId) { try { this.channelService.accessRun ( By.id ( channelId ), ModifiableChannel.class, channel -> { channel.lock (); } ); } catch ( final ChannelNotFoundException e ) { return CommonController.createNotFound ( "channel", channelId ); } return redirectDefaultView ( channelId, false ); } @RequestMapping ( value = "/channel/{channelId}/unlock", method = RequestMethod.GET ) public ModelAndView unlock ( @PathVariable ( "channelId" ) final String channelId) { try { this.channelService.accessRun ( By.id ( channelId ), ModifiableChannel.class, channel -> { channel.unlock (); } ); } catch ( final ChannelNotFoundException e ) { return CommonController.createNotFound ( "channel", channelId ); } return redirectDefaultView ( channelId, false ); } @RequestMapping ( value = "/channel/{channelId}/addAspect", method = RequestMethod.POST ) public ModelAndView addAspect ( @PathVariable ( "channelId" ) final String channelId, @RequestParameter ( "aspect" ) final String aspectFactoryId) { return withChannel ( channelId, AspectableChannel.class, channel -> { channel.addAspects ( false, aspectFactoryId ); return new ModelAndView ( String.format ( "redirect:aspects", channelId ) ); } ); } @RequestMapping ( value = "/channel/{channelId}/addAspectWithDependencies", method = RequestMethod.POST ) public ModelAndView addAspectWithDependencies ( @PathVariable ( "channelId" ) final String channelId, @RequestParameter ( "aspect" ) final String aspectFactoryId) { return withChannel ( channelId, AspectableChannel.class, channel -> { channel.addAspects ( true, aspectFactoryId ); return new ModelAndView ( String.format ( "redirect:aspects", channelId ) ); } ); } @RequestMapping ( value = "/channel/{channelId}/removeAspect", method = RequestMethod.POST ) public ModelAndView removeAspect ( @PathVariable ( "channelId" ) final String channelId, @RequestParameter ( "aspect" ) final String aspectFactoryId) { return withChannel ( channelId, AspectableChannel.class, channel -> { channel.removeAspects ( aspectFactoryId ); return new ModelAndView ( String.format ( "redirect:aspects", channelId ) ); } ); } @RequestMapping ( value = "/channel/{channelId}/refreshAspect", method = RequestMethod.POST ) public ModelAndView refreshAspect ( @PathVariable ( "channelId" ) final String channelId, @RequestParameter ( "aspect" ) final String aspectFactoryId) { return withChannel ( channelId, AspectableChannel.class, channel -> { channel.refreshAspects ( aspectFactoryId ); return new ModelAndView ( String.format ( "redirect:aspects", channelId ) ); } ); } @RequestMapping ( value = "/channel/{channelId}/refreshAllAspects", method = RequestMethod.GET ) public ModelAndView refreshAllAspects ( @PathVariable ( "channelId" ) final String channelId, final HttpServletRequest request) { return withChannel ( channelId, AspectableChannel.class, channel -> { channel.refreshAspects (); return redirectDefaultView ( channelId, false ); } ); } @RequestMapping ( value = "/channel/{channelId}/edit", method = RequestMethod.GET ) public ModelAndView edit ( @PathVariable ( "channelId" ) final String channelId) { final Map<String, Object> model = new HashMap<> (); final Optional<ChannelInformation> info = this.channelService.getState ( By.id ( channelId ) ); if ( !info.isPresent () ) { return CommonController.createNotFound ( "channel", channelId ); } final EditChannel edit = new EditChannel (); final ChannelInformation channel = info.get (); edit.setId ( channel.getId () ); edit.setName ( channel.getName () ); edit.setDescription ( channel.getState ().getDescription () ); model.put ( "command", edit ); model.put ( "breadcrumbs", new Breadcrumbs ( new Entry ( "Home", "/" ), Breadcrumbs.create ( "Channel", ChannelController.class, "view", "channelId", channelId ), new Entry ( "Edit" ) ) ); return new ModelAndView ( "channel/edit", model ); } @RequestMapping ( value = "/channel/{channelId}/edit", method = RequestMethod.POST ) public ModelAndView editPost ( @PathVariable ( "channelId" ) final String channelId, @Valid @FormData ( "command" ) final EditChannel data, final BindingResult result) { if ( !result.hasErrors () ) { this.channelService.accessRun ( By.id ( channelId ), ModifiableChannel.class, channel -> { final ChannelDetails newDesc = new ChannelDetails (); newDesc.setDescription ( data.getDescription () ); channel.setDescription ( newDesc ); } ); this.channelService.accessRun ( By.id ( channelId ), DescriptorAdapter.class, channel -> { channel.setName ( data.getName () ); } ); return redirectDefaultView ( channelId, true ); } else { final Map<String, Object> model = new HashMap<> (); model.put ( "command", data ); model.put ( "breadcrumbs", new Breadcrumbs ( new Entry ( "Home", "/" ), Breadcrumbs.create ( "Channel", ChannelController.class, "view", "channelId", channelId ), new Entry ( "Edit" ) ) ); return new ModelAndView ( "channel/edit", model ); } } @RequestMapping ( value = "/channel/{channelId}/viewCache", method = RequestMethod.GET ) @HttpConstraint ( rolesAllowed = { "MANAGER", "ADMIN" } ) public ModelAndView viewCache ( @PathVariable ( "channelId" ) final String channelId) { return withChannel ( channelId, ReadableChannel.class, channel -> { final Map<String, Object> model = new HashMap<> (); model.put ( "channel", channel.getInformation () ); model.put ( "cacheEntries", channel.getCacheEntries ().values () ); return new ModelAndView ( "channel/viewCache", model ); } ); } @RequestMapping ( value = "/channel/{channelId}/viewCacheEntry", method = RequestMethod.GET ) @HttpConstraint ( rolesAllowed = { "MANAGER", "ADMIN" } ) public ModelAndView viewCacheEntry ( @PathVariable ( "channelId" ) final String channelId, @RequestParameter ( "namespace" ) final String namespace, @RequestParameter ( "key" ) final String key, final HttpServletResponse response) { return withChannel ( channelId, ReadableChannel.class, channel -> { if ( !channel.streamCacheEntry ( new MetaKey ( namespace, key ), entry -> { logger.trace ( "Length: {}, Mime: {}", entry.getSize (), entry.getMimeType () ); response.setContentLengthLong ( entry.getSize () ); response.setContentType ( entry.getMimeType () ); response.setHeader ( "Content-Disposition", String.format ( "inline; filename=%s", URLEncoder.encode ( entry.getName (), "UTF-8" ) ) ); // response.setHeader ( "Content-Disposition", String.format ( "attachment; filename=%s", entry.getName () ) ); ByteStreams.copy ( entry.getStream (), response.getOutputStream () ); } ) ) { return CommonController.createNotFound ( "channel cache entry", String.format ( "%s:%s", namespace, key ) ); } return null; } ); } @Override public List<MenuEntry> getActions ( final HttpServletRequest request, final Object object ) { if ( object instanceof ChannelInformation ) { final ChannelInformation channel = (ChannelInformation)object; final Map<String, Object> model = new HashMap<> ( 1 ); model.put ( "channelId", channel.getId () ); final List<MenuEntry> result = new LinkedList<> (); if ( request.isUserInRole ( "MANAGER" ) ) { if ( !channel.getState ().isLocked () ) { result.add ( new MenuEntry ( "Add Artifact", 100, LinkTarget.createFromController ( ChannelController.class, "add" ).expand ( model ), Modifier.PRIMARY, null ) ); result.add ( new MenuEntry ( "Delete Channel", 400, LinkTarget.createFromController ( ChannelController.class, "delete" ).expand ( model ), Modifier.DANGER, "trash" ).makeModalMessage ( "Delete channel", "Are you sure you want to delete the whole channel?" ) ); result.add ( new MenuEntry ( "Clear Channel", 500, LinkTarget.createFromController ( ChannelController.class, "clear" ).expand ( model ), Modifier.WARNING, null ).makeModalMessage ( "Clear channel", "Are you sure you want to delete all artifacts from this channel?" ) ); result.add ( new MenuEntry ( "Lock Channel", 600, LinkTarget.createFromController ( ChannelController.class, "lock" ).expand ( model ), Modifier.DEFAULT, null ) ); } else { result.add ( new MenuEntry ( "Unlock Channel", 600, LinkTarget.createFromController ( ChannelController.class, "unlock" ).expand ( model ), Modifier.DEFAULT, null ) ); } result.add ( new MenuEntry ( "Edit", 150, "Edit Channel", 200, LinkTarget.createFromController ( ChannelController.class, "edit" ).expand ( model ), Modifier.DEFAULT, null ) ); result.add ( new MenuEntry ( "Maintenance", 160, "Refresh aspects", 100, LinkTarget.createFromController ( ChannelController.class, "refreshAllAspects" ).expand ( model ), Modifier.SUCCESS, "refresh" ) ); } if ( request.getRemoteUser () != null ) { result.add ( new MenuEntry ( "Edit", 150, "Configure Aspects", 300, LinkTarget.createFromController ( ChannelController.class, "aspects" ).expand ( model ), Modifier.DEFAULT, null ) ); } return result; } else if ( Tags.ACTION_TAG_CHANNELS.equals ( object ) ) { final List<MenuEntry> result = new LinkedList<> (); if ( request.isUserInRole ( "MANAGER" ) ) { // result.add ( new MenuEntry ( "Create Channel", 100, LinkTarget.createFromController ( ChannelController.class, "createDetailed" ), Modifier.PRIMARY, null ) ); result.add ( new MenuEntry ( "Create Channel", 120, LinkTarget.createFromController ( ChannelController.class, "createWithRecipe" ), Modifier.PRIMARY, null ) ); } return result; } else if ( object instanceof org.eclipse.packagedrone.repo.channel.ChannelArtifactInformation ) { final ChannelArtifactInformation ai = (ChannelArtifactInformation)object; final List<MenuEntry> result = new LinkedList<> (); final Map<String, Object> model = new HashMap<> ( 2 ); model.put ( "channelId", ai.getChannelId ().getId () ); model.put ( "artifactId", ai.getId () ); if ( request.isUserInRole ( "MANAGER" ) ) { if ( ai.is ( "stored" ) ) { result.add ( new MenuEntry ( "Attach Artifact", 200, LinkTarget.createFromController ( ChannelController.class, "attachArtifact" ).expand ( model ), Modifier.PRIMARY, null ) ); result.add ( new MenuEntry ( "Delete", 1000, LinkTarget.createFromController ( ChannelController.class, "deleteArtifact" ).expand ( model ), Modifier.DANGER, "trash" ) ); } } return result; } return null; } @Override public List<MenuEntry> getViews ( final HttpServletRequest request, final Object object ) { if ( object instanceof ChannelInformation ) { final ChannelInformation channel = (ChannelInformation)object; final Map<String, Object> model = new HashMap<> ( 1 ); model.put ( "channelId", channel.getId () ); final List<MenuEntry> result = new LinkedList<> (); result.add ( new MenuEntry ( "Content", 100, LinkTarget.createFromController ( ChannelController.class, "view" ).expand ( model ), Modifier.DEFAULT, null ) ); result.add ( new MenuEntry ( "List", 120, LinkTarget.createFromController ( ChannelController.class, "viewPlain" ).expand ( model ), Modifier.DEFAULT, null ) ); result.add ( new MenuEntry ( "Details", 200, LinkTarget.createFromController ( ChannelController.class, "details" ).expand ( model ), Modifier.DEFAULT, null ) ); result.add ( new MenuEntry ( null, -1, "Validation", 210, LinkTarget.createFromController ( ChannelController.class, "viewValidation" ).expand ( model ), Modifier.DEFAULT, null ).setBadge ( channel.getState ().getValidationErrorCount () ) ); if ( request.isUserInRole ( "MANAGER" ) ) { result.add ( new MenuEntry ( "Deploy Keys", 1000, LinkTarget.createFromController ( ChannelController.class, "deployKeys" ).expand ( model ), Modifier.DEFAULT, null ) ); } if ( request.isUserInRole ( "MANAGER" ) || request.isUserInRole ( "ADMIN" ) ) { result.add ( new MenuEntry ( "Internal", 400, "View Cache", 100, LinkTarget.createFromController ( ChannelController.class, "viewCache" ).expand ( model ), Modifier.DEFAULT, null ) ); result.add ( new MenuEntry ( "Internal", 400, "Aspect Versions", 100, LinkTarget.createFromController ( ChannelController.class, "viewAspectVersions" ).expand ( model ), Modifier.DEFAULT, null ) ); } if ( channel.hasAspect ( "p2.repo" ) ) { result.add ( new MenuEntry ( "Help", Integer.MAX_VALUE, "P2 Repository", 2_000, LinkTarget.createFromController ( ChannelController.class, "helpP2" ).expand ( model ), Modifier.DEFAULT, "info-sign" ) ); } result.add ( new MenuEntry ( "Help", Integer.MAX_VALUE, "API Upload", 1_100, LinkTarget.createFromController ( ChannelController.class, "helpApi" ).expand ( model ), Modifier.DEFAULT, "upload" ) ); return result; } return null; } @ControllerValidator ( formDataClass = CreateChannel.class ) public void validateCreate ( final CreateChannel data, final ValidationContext ctx ) { validateChannelNameUnique ( null, data.getName (), ctx ); } @ControllerValidator ( formDataClass = EditChannel.class ) public void validateEdit ( final EditChannel data, final ValidationContext ctx ) { validateChannelNameUnique ( data.getId (), data.getName (), ctx ); } private void validateChannelNameUnique ( final String id, final String name, final ValidationContext ctx ) { if ( name == null || name.isEmpty () ) { return; } final ChannelInformation other = this.channelService.getState ( By.name ( name ) ).orElse ( null ); if ( id != null && other != null && !other.getId ().equals ( id ) ) { ctx.error ( "name", String.format ( "The channel name '%s' is already in use by channel '%s'", name, other.getId () ) ); } } @RequestMapping ( value = "/channel/{channelId}/artifact/{artifactId}/attach", method = RequestMethod.GET ) public ModelAndView attachArtifact ( @PathVariable ( "channelId" ) final String channelId, @PathVariable ( "artifactId" ) final String artifactId) { return withChannel ( channelId, ReadableChannel.class, channel -> { final Optional<ChannelArtifactInformation> artifact = channel.getArtifact ( artifactId ); if ( !artifact.isPresent () ) { return CommonController.createNotFound ( "artifact", artifactId ); } return new ModelAndView ( "/artifact/attach", "artifact", artifact.get () ); } ); } @RequestMapping ( value = "/channel/{channelId}/artifact/{artifactId}/attach", method = RequestMethod.POST ) public ModelAndView attachArtifactPost ( @PathVariable ( "channelId" ) final String channelId, @PathVariable ( "artifactId" ) final String artifactId, @RequestParameter ( required = false, value = "name" ) final String name, final @RequestParameter ( "file" ) Part file) { return withChannel ( channelId, ModifiableChannel.class, channel -> { String targetName = name; final Optional<ChannelArtifactInformation> parentArtifact = channel.getArtifact ( artifactId ); if ( !parentArtifact.isPresent () ) { return CommonController.createNotFound ( "artifact", artifactId ); } try { if ( targetName == null || targetName.isEmpty () ) { targetName = file.getSubmittedFileName (); } channel.getContext ().createArtifact ( artifactId, file.getInputStream (), targetName, null ); } catch ( final IOException e ) { return new ModelAndView ( "/error/upload" ); } return new ModelAndView ( "redirect:/channel/" + UrlEscapers.urlPathSegmentEscaper ().escape ( channelId ) + "/view" ); } ); } @Override public void extend ( final UrlSetContext context ) { // add location of channels page context.addLocation ( "/channel", ofNullable ( calcLastMod () ), of ( ChangeFrequency.DAILY ), empty () ); } private Instant calcLastMod () { Instant globalLastMod = null; for ( final ChannelInformation ci : this.channelService.list () ) { final Optional<Instant> lastMod = ofNullable ( ci.getState ().getModificationTimestamp () ); if ( globalLastMod == null || lastMod.get ().isAfter ( globalLastMod ) ) { globalLastMod = lastMod.get (); } } return globalLastMod; } }