gt: 		rowIds = DocumentIdsSelection.fromNullable(singleRowId);
context: package de.metas.ui.web.window.datatypes;
  
  import java.util.LinkedHashSet;
  import java.util.List;
  import java.util.Objects;
  import java.util.Set;
  
  import javax.annotation.Nullable;
  import javax.annotation.concurrent.Immutable;
  
  import org.adempiere.ad.element.api.AdWindowId;
  
  import com.google.common.base.Preconditions;
  import com.google.common.collect.ImmutableList;
  
  import de.metas.ui.web.window.descriptor.DetailId;
  import de.metas.ui.web.window.exceptions.InvalidDocumentPathException;
  import de.metas.util.Check;
  import de.metas.util.GuavaCollectors;
  import de.metas.util.lang.RepoIdAware;
  import lombok.NonNull;
  
  /*
   * #%L
   * metasfresh-webui-api
   * %%
   * Copyright (C) 2016 metas GmbH
   * %%
   * This program is free software: you can redistribute it and/or modify
   * it under the terms of the GNU General Public License as
   * published by the Free Software Foundation, either version 2 of the
   * License, or (at your option) any later version.
   *
   * This program is distributed in the hope that it will be useful,
   * but WITHOUT ANY WARRANTY; without even the implied warranty of
   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
   * GNU General Public License for more details.
   *
   * You should have received a copy of the GNU General Public
   * License along with this program. If not, see
   * <http://www.gnu.org/licenses/gpl-2.0.html>.
   * #L%
   */
  @Immutable
  public final class DocumentPath
  {
  	public static Builder builder()
  	{
  		return new Builder();
  	}
  
  	public static DocumentPath rootDocumentPath(@NonNull final AdWindowId adWindowId, final int documentIdInt)
  	{
  		return rootDocumentPath(WindowId.of(adWindowId), documentIdInt);
  	}
  
  	public static DocumentPath rootDocumentPath(@NonNull final WindowId windowId, final int documentIdInt)
  	{
  		final DocumentId documentId = DocumentId.of(documentIdInt);
  		if (documentId.isNew())
  		{
  			throw new IllegalArgumentException("new or null documentId is not accepted: " + documentId);
  		}
  		return new DocumentPath(DocumentType.Window, windowId.toDocumentId(), documentId);
  	}
  
  	public static DocumentPath rootDocumentPath(@NonNull final WindowId windowId, @NonNull final RepoIdAware documentRepoId)
  	{
  		return rootDocumentPath(windowId, documentRepoId.getRepoId());
  	}
  
  	public static DocumentPath rootDocumentPath(@NonNull final WindowId windowId, final String documentIdStr)
  	{
  		final DocumentId documentId = DocumentId.of(documentIdStr);
  		if (documentId.isNew())
  		{
  			throw new IllegalArgumentException("new or null documentId is not accepted: " + documentId);
  		}
  		return new DocumentPath(DocumentType.Window, windowId.toDocumentId(), documentId);
  	}
  
  	public static DocumentPath rootDocumentPath(@NonNull final WindowId windowId, @NonNull final DocumentId documentId)
  	{
  		if (documentId.isNew())
  		{
  			throw new IllegalArgumentException("new or null documentId is not accepted: " + documentId);
  		}
  		return new DocumentPath(DocumentType.Window, windowId.toDocumentId(), documentId);
  	}
  
  	public static DocumentPath rootDocumentPath(final DocumentType documentType, final DocumentId documentTypeId, final DocumentId documentId)
  	{
  		if (documentId == null || documentId.isNew())
  		{
  			throw new IllegalArgumentException("new or null documentId is not accepted: " + documentId);
  		}
  
  		return new DocumentPath(documentType, documentTypeId, documentId);
  	}
  
  	public static List<DocumentPath> rootDocumentPathsList(final WindowId windowId, final String documentIdsListStr)
  	{
  		if (documentIdsListStr == null || documentIdsListStr.isEmpty())
  		{
  			return ImmutableList.of();
  		}
  
  		return DocumentIdsSelection.ofCommaSeparatedString(documentIdsListStr)
  				.stream()
  				.map(documentId -> rootDocumentPath(windowId, documentId))
  				.collect(GuavaCollectors.toImmutableList());
  	}
  
  	public static DocumentPath includedDocumentPath(@NonNull final WindowId windowId, final String idStr, final String detailId, final String rowIdStr)
  	{
  		if (Check.isEmpty(detailId, true))
  		{
  			throw new IllegalArgumentException("No detailId provided");
  		}
  		if (Check.isEmpty(rowIdStr, true))
  		{
  			throw new IllegalArgumentException("No rowId provided");
  		}
  
  		return builder()
  				.setDocumentType(windowId)
  				.setDocumentId(idStr)
  				.setDetailId(detailId)
  				.setRowId(rowIdStr)
  				.build();
  	}
  
  	public static DocumentPath includedDocumentPath(@NonNull final WindowId windowId, @NonNull final DocumentId documentId, @NonNull final DetailId detailId, @NonNull final DocumentId rowId)
  	{
  		return builder()
  				.setDocumentType(windowId)
  				.setDocumentId(documentId)
  				.setDetailId(detailId)
  				.setRowId(rowId)
  				.build();
  	}
  	
  	public static DocumentPath includedDocumentPath(@NonNull final WindowId windowId, @NonNull final DocumentId documentId, @NonNull final DetailId detailId, @NonNull final DocumentIdsSelection rowIds)
  	{
  		return builder()
  				.setDocumentType(windowId)
  				.setDocumentId(documentId)
  				.setDetailId(detailId)
  				.setRowIds(rowIds)
  				.build();
  	}
  
  
  	public static DocumentPath includedDocumentPath(@NonNull final WindowId windowId, @NonNull final DocumentId documentId, @NonNull final DetailId detailId)
  	{
  		return builder()
  				.setDocumentType(windowId)
  				.setDocumentId(documentId)
  				.setDetailId(detailId)
  				.allowNullRowId()
  				.build();
  	}
  
  	/**
  	 * Creates the path of a single document (root document or included document).
  	 */
  	public static DocumentPath singleWindowDocumentPath(@NonNull final WindowId windowId, final DocumentId id, final DetailId detailId, final DocumentId rowId)
  	{
  		return builder()
  				.setDocumentType(windowId)
  				.setDocumentId(id)
  				.setDetailId(detailId)
  				.setRowId(rowId)
  				.build();
  	}
  
  	private final DocumentType documentType;
  	private final DocumentId documentTypeId;
  	private final DocumentId documentId;
  	private final DetailId detailId;
  	private final DocumentIdsSelection rowIds;
  
  	private transient Integer _hashcode; // lazy
  	private transient String _toString; // lazy
  
  	/** Root document constructor */
  	private DocumentPath(final DocumentType documentType, final DocumentId documentTypeId, final DocumentId documentId)
  	{
  		super();
  
  		Preconditions.checkNotNull(documentType, "documentType shall not be null");
  		Preconditions.checkNotNull(documentTypeId, "documentTypeId shall not be null");
  
  		this.documentType = documentType;
  		this.documentTypeId = documentTypeId;
  		this.documentId = documentId;
  
  		detailId = null;
  		rowIds = DocumentIdsSelection.EMPTY;
  	}
  
  	/** Multiple rowIds constructor */
  	private DocumentPath(@NonNull final DocumentType documentType, @NonNull final DocumentId documentTypeId, final DocumentId documentId, final DetailId detailId, @NonNull final DocumentIdsSelection rowIds)
  	{
  		this.documentType = documentType;
  		this.documentTypeId = documentTypeId;
  		this.documentId = documentId;
  
  		this.detailId = detailId;
  
  		this.rowIds = rowIds;
  	}
  
  	/** Single rowId constructor */
  	private DocumentPath(final DocumentType documentType, final DocumentId documentTypeId, final DocumentId documentId, final DetailId detailId, final DocumentId singleRowId)
  	{
  		super();
  
  		Preconditions.checkNotNull(documentType, "documentType shall not be null");
  		Preconditions.checkNotNull(documentTypeId, "documentTypeId shall not be null");
  
  		this.documentType = documentType;
  		this.documentTypeId = documentTypeId;
  		this.documentId = documentId;
  
  		this.detailId = detailId;
  