All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.almworks.jira.structure.api.view.ViewSpecification Maven / Gradle / Ivy

There is a newer version: 17.25.3
Show newest version
package com.almworks.jira.structure.api.view;

import com.almworks.jira.structure.api.util.JsonMapUtil;
import com.almworks.jira.structure.api.util.StructureUtil;
import com.atlassian.annotations.PublicApi;
import org.codehaus.jackson.annotate.JsonIgnore;
import org.codehaus.jackson.map.annotate.JsonDeserialize;
import org.codehaus.jackson.map.annotate.JsonSerialize;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import javax.xml.bind.annotation.*;
import java.util.*;

/**
 * 

{@code ViewSpecification} represents the visual configuration of a structure grid. * It is usually part of the {@link StructureView}, * which also has a name and description for a view specification, but it can also exist by itself.

* *

View specification contains the following properties:

*
    *
  • List of columns that are displayed by the Structure widget -- see {@link ViewSpecification.Column}
  • *
  • Column display mode that is used by default for the view -- see {@link ColumnDisplayMode}
  • *
* *

This class is an immutable representation of the view. You can also use {@link ViewSpecification.Builder} * class to build or modify a view specification, or to convert it into JSON format for transfer or storage.

* *

This class is thread-safe by the merit of immutability.

* * @see ViewSpecification.Column * @see ViewSpecification.Builder * @see StructureView * @author Igor Sereda */ @PublicApi public class ViewSpecification { /** * Empty specification that does not contain any columns. Used as fallback instance where not null view specification * is required. */ public static final ViewSpecification EMPTY = new ViewSpecification(null, true, ColumnDisplayMode.AUTO_FIT, RowDisplayMode.ONE_LINE, null); private final List myColumns; private final int myColumnDisplayMode; private final int myRowDisplayMode; private final List myPins; private ViewSpecification(List columns, boolean reuseList, int columnDisplayMode, int rowDisplayMode, List pins) { myColumns = columns == null ? Collections.emptyList() : Collections.unmodifiableList(reuseList ? columns : new ArrayList<>(columns)); myColumnDisplayMode = columnDisplayMode; myRowDisplayMode = rowDisplayMode; myPins = pins == null ? Collections.emptyList() : Collections.unmodifiableList(reuseList ? pins : new ArrayList<>(pins)); } /** * @return a list of columns, not null */ @NotNull public List getColumns() { return myColumns; } /** * @return the current column display mode - see {@link ColumnDisplayMode} */ public int getColumnDisplayMode() { return myColumnDisplayMode; } /** * @return the current row display mode - see {@link RowDisplayMode} */ public int getRowDisplayMode() { return myRowDisplayMode; } /** * @return a list of pinned columns csid, not null */ @NotNull public List getPins() { return myPins; } @Override public String toString() { return "ViewSpecification{" + "columns=" + myColumns + ", columnDisplayMode='" + myColumnDisplayMode + '\'' + ", rowDisplayMode='" + myRowDisplayMode + '\'' + ", pins='" + myPins + '\'' + '}'; } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; ViewSpecification that = (ViewSpecification) o; return myColumns.equals(that.myColumns) && myColumnDisplayMode == that.myColumnDisplayMode && myRowDisplayMode == that.myRowDisplayMode && myPins.equals(that.myPins); } @Override public int hashCode() { int result = myColumns.hashCode(); result = 31 * result + myColumnDisplayMode; result = 31 * result + myPins.hashCode(); return result; } /** *

A builder for {@link ViewSpecification}, also used to serialize view specification into JSON.

* *

This class is not thread-safe.

*/ @XmlRootElement(name = "view-specification") @XmlType(name = "view-specification") public static class Builder implements Cloneable { private List myColumnBuilders = new ArrayList<>(); private int myColumnDisplayMode = ColumnDisplayMode.AUTO_FIT; private int myRowDisplayMode = RowDisplayMode.ONE_LINE; private int myCsidSequence = -1; private List myPins = new ArrayList<>(); /** * Constructs empty builder. */ public Builder() { } /** * Constructs a builder that copies the specification passed as a parameter. * * @param specification a specification to copy */ public Builder(@Nullable ViewSpecification specification) { if (specification != null) { for (Column column : specification.getColumns()) { myColumnBuilders.add(new Column.Builder(column)); } myColumnDisplayMode = specification.myColumnDisplayMode; myRowDisplayMode = specification.myRowDisplayMode; myPins = new ArrayList<>(specification.myPins); } } @Override @SuppressWarnings("CloneDoesntDeclareCloneNotSupportedException") public Builder clone() { try { Builder r = (Builder) super.clone(); r.myColumnBuilders = new ArrayList<>(r.myColumnBuilders); r.myPins = new ArrayList<>(r.myPins); List cols = r.myColumnBuilders; for (int i = 0; i < cols.size(); i++) { Column.Builder col = cols.get(i); cols.set(i, col == null ? null : col.clone()); } return r; } catch (CloneNotSupportedException e) { throw new AssertionError(e); } } /** * Adds passed column builders as columns for the future view specification. * * @param columns columns * @return this builder */ public Builder addColumns(Column.Builder... columns) { if (columns == null) return this; Collections.addAll(myColumnBuilders, columns); invalidateCsidSequence(); return this; } /** * Removes a column identified by {@code csid}. * * @param csid the ID of the column to be removed * @return this builder */ public Builder removeColumn(String csid) { for (Iterator ii = myColumnBuilders.iterator(); ii.hasNext(); ) { Column.Builder column = ii.next(); String columnCsid = column.getCsid(); if (csid == null && columnCsid == null || csid != null && csid.equals(columnCsid)) { ii.remove(); invalidateCsidSequence(); } } return this; } /** *

Adds a column identified by the column {@code key} and {@code csid}.

* *

Although key and csid arguments could be null, the resulting builder will not have a valid state - each * column in the specification must have csid and key.

* * @param key column key * @param csid column csid * @return the builder for the column * @see ViewSpecification.Column */ public Column.Builder addColumn(String key, String csid) { Column.Builder builder = new Column.Builder(); builder.setCsid(csid); builder.setKey(key); myColumnBuilders.add(builder); invalidateCsidSequence(); return builder; } /** *

Adds a column identified by the column {@code key} and automatically generated {@code csid}.

* * @param key column key * @return the builder for the column * @see ViewSpecification.Column */ public Column.Builder addColumn(String key) { return addColumn(key, getNextCsid()); } /** *

Adds "main" column to the view, which displays issue summary, indented to reflect the depth.

* * @return this builder */ public Builder addMainColumn() { addColumn("main", "main"); return this; } /** *

Adds a field column, identified by JIRA field ID.

* *

Field id is either one of the system fields (see {@code com.atlassian.jira.issue.IssueFieldConstants}) * or a custom field id in form of {@code customfield_NNNNN}.

* * @param field JIRA field id * @return this builder */ public Builder addFieldColumn(String field) { addColumn("field").setParameter("field", field); return this; } /** *

Adds "Total Time" column, based on one of the three JIRA time fields.

* *

Parameter {@code field} must be one of the following:

*
    *
  • {@code "timeoriginalestimate"}
  • *
  • {@code "timeestimate"}
  • *
  • {@code "timespent"}
  • *
* * @param field JIRA time field id * @return this builder */ public Builder addTimeAggregateColumn(String field) { addColumn("field").setParameter("field", field).setParameter("aggregate", "sum"); return this; } /** *

Adds an aggregate column that sums up the given JIRA field.

* *

Parameter {@code field} must be one of the following:

*
    *
  • {@code "timeoriginalestimate"}
  • *
  • {@code "timeestimate"}
  • *
  • {@code "timespent"}
  • *
  • {@code "votes"}
  • *
  • any numeric custom field id
  • *
* * @param field JIRA field id * @return this builder */ public Builder addFieldSumColumn(String field) { addColumn("field").setParameter("field", field).setParameter("aggregate", "sum"); return this; } /** *

Adds the "Progress" column provided by Structure plugin.

* * @return this builder */ public Builder addProgressColumn() { addColumn("progress"); return this; } /** *

Adds the "TP" column provided by Structure plugin.

* * @return this builder */ public Builder addTPColumn() { Column.Builder builder = addColumn("icons"); builder.setName(StructureUtil.getText(null, null, "s.w.column.tp.label")); builder.setStringListParameter("fields", "issuetype", "priority"); return this; } /** *

Adds an "Icons" column showing the icon representations of the given fields, in that order.

* *

Each {@code field} must be one of the following:

*
    *
  • {@code "project"}
  • *
  • {@code "issuetype"}
  • *
  • {@code "priority"}
  • *
  • {@code "status"}
  • *
  • {@code "reporter"}
  • *
  • {@code "assignee"}
  • *
* * @param fields JIRA field ids * @return this builder */ public Builder addIconsColumn(String... fields) { Column.Builder builder = addColumn("icons"); builder.setStringListParameter("fields", fields); return this; } /** * Builds an instance of {@link ViewSpecification}. If any column builder has invalid state, it is skipped * and not put into the final view. * * @return the created {@code ViewSpecification} */ @NotNull public ViewSpecification build() { ArrayList columns = new ArrayList<>(myColumnBuilders.size()); for (Column.Builder columnBuilder : myColumnBuilders) { if (columnBuilder.isValid()) { columns.add(columnBuilder.build()); } } return new ViewSpecification(columns, true, myColumnDisplayMode, myRowDisplayMode, myPins); } private void invalidateCsidSequence() { myCsidSequence = -1; } private String getNextCsid() { if (myCsidSequence < 0) { int max = 0; for (Column.Builder builder : myColumnBuilders) { String csid = builder.getCsid(); if (csid == null) continue; try { max = Math.max(max, Integer.parseInt(csid)); } catch (NumberFormatException e) { // ignore } } myCsidSequence = max; } return String.valueOf(++myCsidSequence); } /** * @return the current column builders */ @XmlElementRef() @XmlElementWrapper(name = "columns") @JsonDeserialize(contentAs = Column.Builder.class) @NotNull public List getColumns() { return myColumnBuilders; } /** * Changes the current column builders to the passed list. * * @param columns column builders */ public void setColumns(List columns) { myColumnBuilders = columns == null ? new ArrayList<>() : new ArrayList<>(columns); } /** * Set column display mode * * @param columnDisplayMode new column display mode * @return this builder * @see ColumnDisplayMode */ public Builder setColumnDisplayMode(int columnDisplayMode) { myColumnDisplayMode = ColumnDisplayMode.isValid(columnDisplayMode) ? columnDisplayMode : ColumnDisplayMode.AUTO_FIT; return this; } /** * Set row display mode * * @param rowDisplayMode new row display mode * @return this builder * @see RowDisplayMode */ public Builder setRowDisplayMode(int rowDisplayMode) { myRowDisplayMode = RowDisplayMode.isValid(rowDisplayMode) ? rowDisplayMode : RowDisplayMode.ONE_LINE; return this; } /** * @return the current column display mode * @see ColumnDisplayMode */ @XmlElement public int getColumnDisplayMode() { return myColumnDisplayMode; } /** * @return the current row display mode * @see RowDisplayMode */ @XmlElement public int getRowDisplayMode() { return myRowDisplayMode; } public Builder setPins(List pins) { myPins = pins == null ? new ArrayList<>() : new ArrayList<>(pins); return this; } /** * @return list of csid of pinned columns * @see ColumnDisplayMode */ @XmlElement public List getPins() { return myPins; } @Override public String toString() { return "ViewSpecification.Builder{" + "columns=" + myColumnBuilders + ", columnDisplayMode='" + myColumnDisplayMode + '\'' + ", pins='" + myPins + '\'' + '}'; } } /** *

Represents a single column configuration in the Structure widget.

* *

Structure columns have the following properties:

* *
    *
  • {@code csid} - mandatory property that should be an unique column ID within the view specification. Typically, * special columns have special {@code csid} while all other columns have numeric incrementing {@code csid}.
  • *
  • {@code key} - mandatory property that defines the class of the column, its behavior. As of Structure 2.0, there's a predefined * set of supported column keys. In the future, we plan to make it expandable.
  • *
  • {@code name} - optional property that defines the header of the column in the grid. If not set, default * header is used as decided by the column class.
  • *
  • {@code parameters} - an unbounded map of any parameters that make sense to the specific class of the column. *
  • *
* *

Supported parameter value types:

*
    *
  • {@code String}
  • *
  • {@code Integer}
  • *
  • {@code Long}
  • *
  • {@code Double}
  • *
  • {@code Boolean}
  • *
  • {@code List} with all elements in the list being of supported parameter type
  • *
  • {@code Map} with all keys in the map being {@code String}s and all values of supported parameter type
  • *
* *

Class {@code ViewSpecification.Column} is immutable and thread-safe. To create or change a column, * use {@link ViewSpecification.Column.Builder}.

*/ public static class Column { private final String myCsid; private final String myKey; private final String myName; private final Map myParameters; private Column(String csid, String key, String name, Map parameters) { myCsid = csid == null ? "" : csid; myKey = key == null ? "" : key; myName = name; myParameters = JsonMapUtil.copyParameters(parameters, false, true, false); } @NotNull public String getCsid() { return myCsid; } @NotNull public String getKey() { return myKey; } @Nullable public String getName() { return myName; } /** * @return immutable map of column parameters */ @NotNull public Map getParameters() { return myParameters; } @Override public String toString() { return "Column{" + "csid='" + myCsid + '\'' + ", key='" + myKey + '\'' + ", name='" + myName + '\'' + ", parameters=" + myParameters + '}'; } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; Column column = (Column) o; if (!myCsid.equals(column.myCsid)) return false; if (!myKey.equals(column.myKey)) return false; if (myName != null ? !myName.equals(column.myName) : column.myName != null) return false; if (myParameters != null ? !myParameters.equals(column.myParameters) : column.myParameters != null) return false; return true; } @Override public int hashCode() { int result = myCsid.hashCode(); result = 31 * result + myKey.hashCode(); result = 31 * result + (myName != null ? myName.hashCode() : 0); result = 31 * result + (myParameters != null ? myParameters.hashCode() : 0); return result; } /** *

{@code ViewSpecification.Column.Builder} is used to create instances of {@link ViewSpecification.Column}, * and also to convert them to JSON format.

* *

The builder may have invalid state when {@code csid} or {@code key} is {@code null}. Only the builder * in valid state can produce an instance of {@code Column}, so make sure to set those two properties.

*/ @XmlRootElement(name = "column") @XmlType(name = "column", propOrder = {"csid", "key", "name", "parameters"}) @JsonSerialize(include = JsonSerialize.Inclusion.NON_NULL) public static class Builder implements Cloneable { private String myCsid; private String myKey; private String myName; private Map myParameters; /** * Creates an empty builder. */ public Builder() { } /** * Creates a builder with a copy of a column properties. If there's a parameter map, it is copied for modification. * * @param column column to copy */ public Builder(@Nullable Column column) { if (column != null) { myCsid = column.getCsid(); myKey = column.getKey(); myName = column.getName(); myParameters = JsonMapUtil.copyParameters(column.getParameters(), true, false, false); } } @Override @SuppressWarnings("CloneDoesntDeclareCloneNotSupportedException") public Builder clone() { try { Builder r = (Builder) super.clone(); r.myParameters = JsonMapUtil.copyParameters(r.myParameters, true, false, false); return r; } catch (CloneNotSupportedException e) { throw new AssertionError(e); } } @Nullable @XmlElement public String getCsid() { return myCsid; } public void setCsid(String csid) { myCsid = csid; } @Nullable @XmlElement public String getKey() { return myKey; } public void setKey(String key) { myKey = key; } @Nullable @XmlElement public String getName() { return myName; } public void setName(String name) { myName = name; } /** * @return parameter map, or null if no parameters are defined. This method gives access to the internal * parameter map for the sake of serializing speed. Although you can update it, it is preferrable to use * {@link #setParameter} method. */ @Nullable @XmlElement public Map getParameters() { return myParameters; } /** *

Updates the parameter map for the column. The map is copied by this method, so the passed object can * be reused by the calling method.

* *

Passing {@code null} will clear the parameter map.

* * @param parameters new parameter map */ public void setParameters(@Nullable Map parameters) { myParameters = JsonMapUtil.copyParameters(parameters, true, false, false); } /** * Removes a parameter from parameter map. * * @param name name of the parameter * @return this builder */ public Builder removeParameter(String name) { return setParameter(name, null); } /** *

Sets a parameter for this column. The parameter and the value are added to the parameter map.

* *

For the list of supported parameter types, see {@link Column}.

* * @param name the name of the parameter * @param value the value of the parameter * @return this builder * @throws IllegalArgumentException if the parameter is of unsupported type */ public Builder setParameter(String name, @Nullable Object value) { if (name == null) { throw new NullPointerException(); } if (value == null) { if (myParameters != null) { myParameters.remove(name); } } else { JsonMapUtil.checkValidParameter(value); if (myParameters == null) { myParameters = new LinkedHashMap<>(); } myParameters.put(name, JsonMapUtil.copyParameter(value, false)); } return this; } /** * Utility method to set a parameter of type {@code List} with {@code String} elements. * * @param name parameter name * @param values a list of values for the parameter * @return this builder */ public Builder setStringListParameter(String name, String... values) { List list = Arrays.asList(values); setParameter(name, list); return this; } /** * @return true if this builder has a valid state and can produce {@link Column} - that is, it has non-empty {@code key} and * non-empty {@code csid} */ @JsonIgnore public boolean isValid() { return myCsid != null && myCsid.length() > 0 && myKey != null && myKey.length() > 0; } /** * Creates an instance of {@link Column} using the current state of the builder. After the column is created, * the state can be reused to create another instance. * * @return the new immutable column * @throws IllegalStateException if the state is invalid - see {@link #isValid} */ @NotNull public Column build() throws IllegalStateException { if (!isValid()) { throw new IllegalStateException("column builder is not in valid state: " + this); } return new Column(myCsid, myKey, myName, myParameters); } @Override public String toString() { return "ViewSpecification.Column.Builder{" + "csid='" + myCsid + '\'' + ", key='" + myKey + '\'' + ", name='" + myName + '\'' + ", parameters=" + myParameters + '}'; } } } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy