-
-
Notifications
You must be signed in to change notification settings - Fork 9k
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
Changes from 13 commits
0763ffd
5b1ace0
7c43986
056b3b0
e0a80dc
417e0d8
f3d13af
8cb1070
19e8be4
559a5af
beaabb0
5106675
fd2009a
d1c40f4
3538931
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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; | ||
|
||
|
@@ -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); | ||
|
@@ -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) | ||
|
@@ -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 | ||
* 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)) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. AFAICT There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Done There was a problem hiding this comment. Choose a reason for hiding this commentThe 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}); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
allView.name = DEFAULT_VIEW_NAME; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should this not There was a problem hiding this comment. Choose a reason for hiding this commentThe 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; | ||
} | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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; | ||
|
@@ -238,6 +239,7 @@ public final TopLevelItem getJob(String name) { | |
* @see #rename(String) | ||
*/ | ||
@Exported(visibility=2,name="name") | ||
@Nonnull | ||
public String getViewName() { | ||
return name; | ||
} | ||
|
@@ -415,7 +417,7 @@ public List<Widget> getWidgets() { | |
* If this view uses <t:projectView> for rendering, this method returns columns to be displayed. | ||
*/ | ||
public Iterable<? extends ListViewColumn> getColumns() { | ||
return ListViewColumn.createDefaultInitialColumnList(); | ||
return ListViewColumn.createDefaultInitialColumnList(this); | ||
} | ||
|
||
/** | ||
|
@@ -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} | ||
*/ | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Perhaps just check There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Need to also check whether the |
||
@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; | ||
} | ||
|
||
|
@@ -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; | ||
|
@@ -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); | ||
|
@@ -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 | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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}. | ||
|
@@ -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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes but it needs to use the There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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()); | ||
} | ||
|
||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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; | ||
|
@@ -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)) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
||
// 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))) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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? There was a problem hiding this comment. Choose a reason for hiding this commentThe 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; | ||
} | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.