com.almworks.jira.structure.api.view.ViewSpecification Maven / Gradle / Ivy
Show all versions of structure-api Show documentation
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 com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
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"})
@JsonInclude(value = JsonInclude.Include.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 +
'}';
}
}
}
}