Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[JENKINS-39300] View.getDisplayName() is ignored when rendering tabs #2603

Merged
merged 15 commits into from
Dec 12, 2016
Merged
Show file tree
Hide file tree
Changes from 13 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
95 changes: 91 additions & 4 deletions core/src/main/java/hudson/model/AllView.java
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,16 @@
*/
package hudson.model;

import java.util.List;
import java.util.Locale;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.annotation.CheckForNull;
import javax.annotation.Nonnull;
import jenkins.util.SystemProperties;
import org.apache.commons.lang.StringUtils;
import org.jenkinsci.Symbol;
import org.kohsuke.stapler.DataBoundConstructor;
import org.kohsuke.stapler.Stapler;
import org.kohsuke.stapler.StaplerRequest;
import org.kohsuke.stapler.StaplerResponse;

Expand All @@ -44,6 +51,21 @@
* @since 1.269
*/
public class AllView extends View {

/**
* The name of the default {@link AllView}. An {@link AllView} with this name will get a localized display name.
* Other {@link AllView} instances will be assumed to have been created by the user and thus will use the
* name the user created them with.
*
* @since FIXME
*/
public static final String DEFAULT_VIEW_NAME = "all";

/**
* Our logger.
*/
private static final Logger LOGGER = Logger.getLogger(AllView.class.getName());

@DataBoundConstructor
public AllView(String name) {
super(name);
Expand All @@ -64,6 +86,11 @@ public boolean contains(TopLevelItem item) {
return true;
}

@Override
public String getDisplayName() {
return DEFAULT_VIEW_NAME.equals(name) ? Messages.Hudson_ViewName() : name;
}

@RequirePOST
@Override
public Item doCreateItem(StaplerRequest req, StaplerResponse rsp)
Expand All @@ -89,13 +116,73 @@ protected void submit(StaplerRequest req) throws IOException, ServletException,
// noop
}

/**
* Corrects the name of the {@link AllView} if and only if the {@link AllView} is the primary view and
* its name is one of the localized forms of {@link Messages#_Hudson_ViewName()} and the user has not opted out of
* fixing the view name by setting the system property {@code hudson.mode.AllView.JENKINS-38606} to {@code false}.
* Use this method to round-trip the primary view name, e.g.
* {@code primaryView = applyJenkins38606Fixup(views, primaryView)}
* <p>
* NOTE: we can only fix the localized name of an {@link AllView} if it is the primary view as otherwise urls
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

<p>

* would change, whereas the primary view is special and does not normally get accessed by the
* {@code /view/_name_} url. (Also note that there are some cases where the primary view will get accessed by
* its {@code /view/_name_} url which will then fall back to the primary view)
*
* @param views the list of views.
* @param primaryView the current primary view name.
* @return the primary view name - this will be the same as the provided primary view name unless a JENKINS-38606
* matching name is detected, in which case this will be the new name of the primary view.
* @since FIXME
*/
@Nonnull
public static String migrateLegacyPrimaryAllViewLocalizedName(@Nonnull List<View> views,
@Nonnull String primaryView) {
if (DEFAULT_VIEW_NAME.equals(primaryView)) {
// modern name, we are safe
return primaryView;
}
if (SystemProperties.getBoolean(AllView.class.getName()+".JENKINS-38606", true)) {
AllView allView = null;
for (View v : views) {
if (DEFAULT_VIEW_NAME.equals(v.getViewName())) {
// name conflict, we cannot rename the all view anyway
return primaryView;
}
if (StringUtils.equals(v.getViewName(), primaryView)) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AFAICT getViewName() should be marked @Nonnull.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

meaning you could use

if (v.getViewName().equals(primaryView)) {

if (v instanceof AllView) {
allView = (AllView) v;
} else {
// none of our business fixing as we can only safely fix the primary view
return primaryView;
}
}
}
if (allView != null) {
// the primary view is an AllView but using a non-default name
for (Locale l : Locale.getAvailableLocales()) {
if (primaryView.equals(Messages._Hudson_ViewName().toString(l))) {
// bingo JENKINS-38606 detected
LOGGER.log(Level.INFO,
"JENKINS-38606 detected for AllView in {0}; renaming view from {1} to {2}",
new Object[]{allView.owner.getUrl(), DEFAULT_VIEW_NAME});
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

allView.name = DEFAULT_VIEW_NAME;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this not save the owner?

Copy link
Member Author

@stephenc stephenc Nov 3, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nope, responsibility of the caller. As in, the normal way we apply data migrations is to read in and migrate and only write out the new data when the objects are actually saved again.

return DEFAULT_VIEW_NAME;
}
}
}
}
return primaryView;
}

@Extension @Symbol("all")
public static final class DescriptorImpl extends ViewDescriptor {
@Override
public boolean isInstantiable() {
for (View v : Stapler.getCurrentRequest().findAncestorObject(ViewGroup.class).getViews())
if(v instanceof AllView)
public boolean isApplicableIn(ViewGroup owner) {
for (View v : owner.getViews()) {
if (v instanceof AllView) {
return false;
}
}
return true;
}

Expand Down
6 changes: 4 additions & 2 deletions core/src/main/java/hudson/model/ListView.java
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,9 @@ private Object readResolve() {

protected void initColumns() {
if (columns == null)
columns = new DescribableList<ListViewColumn, Descriptor<ListViewColumn>>(this,ListViewColumn.createDefaultInitialColumnList());
columns = new DescribableList<ListViewColumn, Descriptor<ListViewColumn>>(this,
ListViewColumn.createDefaultInitialColumnList(getClass())
);
}

protected void initJobFilters() {
Expand Down Expand Up @@ -459,7 +461,7 @@ public FormValidation doCheckIncludeRegex( @QueryParameter String value ) throws
*/
@Deprecated
public static List<ListViewColumn> getDefaultColumns() {
return ListViewColumn.createDefaultInitialColumnList();
return ListViewColumn.createDefaultInitialColumnList(ListView.class);
}

@Restricted(NoExternalUse.class)
Expand Down
6 changes: 4 additions & 2 deletions core/src/main/java/hudson/model/MyViewsProperty.java
Original file line number Diff line number Diff line change
Expand Up @@ -83,9 +83,11 @@ public Object readResolve() {
// this shouldn't happen, but an error in 1.319 meant the last view could be deleted
views = new CopyOnWriteArrayList<View>();

if (views.isEmpty())
if (views.isEmpty()) {
// preserve the non-empty invariant
views.add(new AllView(Messages.Hudson_ViewName(), this));
views.add(new AllView(AllView.DEFAULT_VIEW_NAME, this));
}
primaryViewName = AllView.migrateLegacyPrimaryAllViewLocalizedName(views, primaryViewName);

viewGroupMixIn = new ViewGroupMixIn(this) {
protected List<View> views() { return views; }
Expand Down
31 changes: 27 additions & 4 deletions core/src/main/java/hudson/model/View.java
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@
import hudson.util.XStream2;
import hudson.views.ListViewColumn;
import hudson.widgets.Widget;
import javax.annotation.Nonnull;
import jenkins.model.Jenkins;
import jenkins.model.ModelObjectWithChildren;
import jenkins.model.item_category.Categories;
Expand Down Expand Up @@ -238,6 +239,7 @@ public final TopLevelItem getJob(String name) {
* @see #rename(String)
*/
@Exported(visibility=2,name="name")
@Nonnull
public String getViewName() {
return name;
}
Expand Down Expand Up @@ -415,7 +417,7 @@ public List<Widget> getWidgets() {
* If this view uses &lt;t:projectView> for rendering, this method returns columns to be displayed.
*/
public Iterable<? extends ListViewColumn> getColumns() {
return ListViewColumn.createDefaultInitialColumnList();
return ListViewColumn.createDefaultInitialColumnList(this);
}

/**
Expand Down Expand Up @@ -1219,11 +1221,30 @@ public static DescriptorExtensionList<View,ViewDescriptor> all() {
return Jenkins.getInstance().<View,ViewDescriptor>getDescriptorList(View.class);
}

/**
* Returns the {@link ViewDescriptor} instances that can be instantiated for the {@link ViewGroup} in the current
* {@link StaplerRequest}.
* <p>
* <strong>NOTE: Historically this method is only ever called from a {@link StaplerRequest}</strong>
* @return the list of instantiable {@link ViewDescriptor} instances for the current {@link StaplerRequest}
*/
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note: this is the javadoc that applied to the method before my changes as the method would have blown up with a NPE if invoked outside of the newView.jelly screen of a ViewGroup as soon as AllView.isInstantiable() was called

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps just check owner == null and return all()?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Need to also check whether the StaplerRequest is null.

@Nonnull
public static List<ViewDescriptor> allInstantiable() {
List<ViewDescriptor> r = new ArrayList<ViewDescriptor>();
for (ViewDescriptor d : all())
if(d.isInstantiable())
StaplerRequest request = Stapler.getCurrentRequest();
if (request == null) {
throw new IllegalStateException("This method can only be invoked from a stapler request");
}
ViewGroup owner = request.findAncestorObject(ViewGroup.class);
if (owner == null) {
throw new IllegalStateException("This method can only be invoked from a request with a ViewGroup ancestor");
}
for (ViewDescriptor d : DescriptorVisibilityFilter.apply(owner, all())) {
if (d.isApplicableIn(owner) && d.isInstantiable()
&& owner.getACL().hasCreatePermission(Jenkins.getAuthentication(), owner, d)) {
r.add(d);
}
}
return r;
}

Expand Down Expand Up @@ -1268,6 +1289,7 @@ public static View create(StaplerRequest req, StaplerResponse rsp, ViewGroup own
if (mode==null || mode.length()==0) {
if(isXmlSubmission) {
View v = createViewFromXML(name, req.getInputStream());
owner.getACL().checkCreatePermission(owner, v.getDescriptor());
v.owner = owner;
rsp.setStatus(HttpServletResponse.SC_OK);
return v;
Expand All @@ -1276,7 +1298,7 @@ public static View create(StaplerRequest req, StaplerResponse rsp, ViewGroup own
}

View v;
if (mode!=null && mode.equals("copy")) {
if ("copy".equals(mode)) {
v = copy(req, owner, name);
} else {
ViewDescriptor descriptor = all().findByName(mode);
Expand All @@ -1287,6 +1309,7 @@ public static View create(StaplerRequest req, StaplerResponse rsp, ViewGroup own
// create a view
v = descriptor.newInstance(req,req.getSubmittedForm());
}
owner.getACL().checkCreatePermission(owner, v.getDescriptor());
v.owner = owner;

// redirect to the config screen
Expand Down
71 changes: 71 additions & 0 deletions core/src/main/java/hudson/model/ViewDescriptor.java
Original file line number Diff line number Diff line change
Expand Up @@ -23,17 +23,26 @@
*/
package hudson.model;

import hudson.util.FormValidation;
import hudson.views.ListViewColumn;
import hudson.views.ListViewColumnDescriptor;
import hudson.views.ViewJobFilter;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.Iterator;
import java.util.List;
import javax.annotation.CheckForNull;
import javax.annotation.Nonnull;
import jenkins.model.DirectlyModifiableTopLevelItemGroup;
import jenkins.model.Jenkins;
import org.apache.commons.lang.StringUtils;
import org.jvnet.tiger_types.Types;
import org.kohsuke.accmod.Restricted;
import org.kohsuke.accmod.restrictions.DoNotUse;
import org.kohsuke.stapler.AncestorInPath;
import org.kohsuke.stapler.QueryParameter;
import org.kohsuke.stapler.Stapler;
import org.kohsuke.stapler.StaplerRequest;

/**
* {@link Descriptor} for {@link View}.
Expand Down Expand Up @@ -101,13 +110,75 @@ public AutoCompletionCandidates doAutoCompleteCopyNewItemFrom(@QueryParameter fi
* Possible {@link ListViewColumnDescriptor}s that can be used with this view.
*/
public List<Descriptor<ListViewColumn>> getColumnsDescriptors() {
StaplerRequest request = Stapler.getCurrentRequest();
if (request != null) {
View view = request.findAncestorObject(clazz);
return view == null ? DescriptorVisibilityFilter.applyType(clazz, ListViewColumn.all())
: DescriptorVisibilityFilter.apply(view, ListViewColumn.all());
}
return ListViewColumn.all();
}

/**
* Possible {@link ViewJobFilter} types that can be used with this view.
*/
public List<Descriptor<ViewJobFilter>> getJobFiltersDescriptors() {
StaplerRequest request = Stapler.getCurrentRequest();
if (request != null) {
View view = request.findAncestorObject(clazz);
return view == null ? DescriptorVisibilityFilter.applyType(clazz, ViewJobFilter.all())
: DescriptorVisibilityFilter.apply(view, ViewJobFilter.all());
}
return ViewJobFilter.all();
}

/**
* Validation of the display name field.
*
* @param view the view to check the new display name of.
* @param value the proposed new display name.
* @return the validation result.
* @since FIXME
*/
@SuppressWarnings("unused") // expose utility check method to subclasses
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a particular subclass you had in mind? Not in this PR; some other linked one?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes but it needs to use the Referer header hack to resolve the View so probably waiting on dedicated support in stapler for that

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So where is that subclass?

protected FormValidation checkDisplayName(@Nonnull View view, @CheckForNull String value) {
if (StringUtils.isBlank(value)) {
// no custom name, no need to check
return FormValidation.ok();
}
for (View v: view.owner.getViews()) {
if (v.getViewName().equals(view.getViewName())) {
continue;
}
if (StringUtils.equals(v.getDisplayName(), value)) {
return FormValidation.warning(Messages.View_DisplayNameNotUniqueWarning(value));
}
}
return FormValidation.ok();
}

/**
* Returns true if this {@link View} type is applicable to the given {@link ViewGroup} type.
* <p>
* Default implementation returns {@code true} always.
*
* @return true to indicate applicable, in which case the view will be instantiable within the type of owner.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Specify default implementation.

* @since FIXME
*/
public boolean isApplicable(Class<? extends ViewGroup> ownerType) {
return true;
}

/**
* Returns true if this {@link View} type is applicable in the specific {@link ViewGroup}.
* <p>
* Default implementation returns {@link #isApplicable(Class)} for the {@link ViewGroup#getClass()}.
*
* @return true to indicate applicable, in which case the view will be instantiable within the given owner.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Specify default implementation.

* @since FIXME
*/
public boolean isApplicableIn(ViewGroup owner) {
return isApplicable(owner.getClass());
}

}
11 changes: 11 additions & 0 deletions core/src/main/java/hudson/model/ViewGroupMixIn.java
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@
import hudson.model.ItemGroupMixIn;
import hudson.model.View;
import hudson.model.ViewGroup;
import java.util.Locale;
import java.util.logging.Level;
import org.kohsuke.stapler.export.Exported;

import java.io.IOException;
Expand Down Expand Up @@ -99,6 +101,15 @@ public View getView(String name) {
View pv = getPrimaryView();
if (pv instanceof ViewGroup)
return ((ViewGroup)pv).getView(name);
if (pv instanceof AllView && AllView.DEFAULT_VIEW_NAME.equals(pv.name)) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds right but hard for me at least to follow how this will work in progress; recommend having some code coverage using @LocalData to prove it works.

// JENKINS-38606: primary view is the default AllView, is somebody using an old link to localized form?
for (Locale l : Locale.getAvailableLocales()) {
if (name.equals(Messages._Hudson_ViewName().toString(l))) {
Copy link
Member

@daniel-beck daniel-beck Dec 10, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So changing the translations will break this compatibility measure for that language? Is this so rare to be irrelevant?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes

// why yes they are, let's keep that link working
return pv;
}
}
}
}
return null;
}
Expand Down
Loading