extensions/net.sf.basedb.reggie/trunk/src/net/sf/basedb/reggie/dao/ReggieItem.java

Code
Comments
Other
Rev Date Author Line
1326 29 Mar 11 nicklas 1 package net.sf.basedb.reggie.dao;
1326 29 Mar 11 nicklas 2
4883 04 Jul 18 nicklas 3 import java.util.ArrayList;
7269 26 Jun 23 nicklas 4 import java.util.Collections;
3764 22 Feb 16 nicklas 5 import java.util.Comparator;
5423 13 May 19 nicklas 6 import java.util.Date;
3503 22 Sep 15 nicklas 7 import java.util.HashMap;
4881 03 Jul 18 nicklas 8 import java.util.List;
3503 22 Sep 15 nicklas 9 import java.util.Map;
7269 26 Jun 23 nicklas 10 import java.util.Set;
5777 06 Dec 19 nicklas 11 import java.util.regex.Matcher;
5777 06 Dec 19 nicklas 12 import java.util.regex.Pattern;
3503 22 Sep 15 nicklas 13
1326 29 Mar 11 nicklas 14 import org.json.simple.JSONObject;
1326 29 Mar 11 nicklas 15
1326 29 Mar 11 nicklas 16 import net.sf.basedb.core.Annotatable;
1326 29 Mar 11 nicklas 17 import net.sf.basedb.core.AnnotationType;
5694 31 Oct 19 nicklas 18 import net.sf.basedb.core.AnyToAny;
4897 10 Jul 18 nicklas 19 import net.sf.basedb.core.BasicItem;
3503 22 Sep 15 nicklas 20 import net.sf.basedb.core.BioMaterial;
5423 13 May 19 nicklas 21 import net.sf.basedb.core.ChangeHistory;
1326 29 Mar 11 nicklas 22 import net.sf.basedb.core.DbControl;
3503 22 Sep 15 nicklas 23 import net.sf.basedb.core.DerivedBioAssay;
5694 31 Oct 19 nicklas 24 import net.sf.basedb.core.File;
4897 10 Jul 18 nicklas 25 import net.sf.basedb.core.Include;
4881 03 Jul 18 nicklas 26 import net.sf.basedb.core.InvalidDataException;
4897 10 Jul 18 nicklas 27 import net.sf.basedb.core.Item;
1821 06 Feb 13 nicklas 28 import net.sf.basedb.core.ItemNotFoundException;
4881 03 Jul 18 nicklas 29 import net.sf.basedb.core.ItemQuery;
3503 22 Sep 15 nicklas 30 import net.sf.basedb.core.ItemSubtype;
3503 22 Sep 15 nicklas 31 import net.sf.basedb.core.MeasuredBioMaterial;
1326 29 Mar 11 nicklas 32 import net.sf.basedb.core.Nameable;
4897 10 Jul 18 nicklas 33 import net.sf.basedb.core.Ownable;
1821 06 Feb 13 nicklas 34 import net.sf.basedb.core.PermissionDeniedException;
3503 22 Sep 15 nicklas 35 import net.sf.basedb.core.RawBioAssay;
5423 13 May 19 nicklas 36 import net.sf.basedb.core.Registered;
4897 10 Jul 18 nicklas 37 import net.sf.basedb.core.User;
7269 26 Jun 23 nicklas 38 import net.sf.basedb.core.SyncFilter.SourceItemTransform;
5423 13 May 19 nicklas 39 import net.sf.basedb.core.log.ChangeType;
4883 04 Jul 18 nicklas 40 import net.sf.basedb.core.query.Expressions;
4883 04 Jul 18 nicklas 41 import net.sf.basedb.core.query.Hql;
7269 26 Jun 23 nicklas 42 import net.sf.basedb.core.query.IdListRestriction;
4883 04 Jul 18 nicklas 43 import net.sf.basedb.core.query.Orders;
4883 04 Jul 18 nicklas 44 import net.sf.basedb.core.query.Restrictions;
3166 05 Mar 15 nicklas 45 import net.sf.basedb.core.snapshot.SnapshotManager;
1326 29 Mar 11 nicklas 46 import net.sf.basedb.reggie.JsonUtil;
5423 13 May 19 nicklas 47 import net.sf.basedb.reggie.Reggie;
4881 03 Jul 18 nicklas 48 import net.sf.basedb.reggie.ReservedItems;
1333 05 Apr 11 nicklas 49 import net.sf.basedb.reggie.converter.IdentityConverter;
1333 05 Apr 11 nicklas 50 import net.sf.basedb.reggie.converter.ValueConverter;
4883 04 Jul 18 nicklas 51 import net.sf.basedb.util.MD5;
4881 03 Jul 18 nicklas 52 import net.sf.basedb.util.Values;
7269 26 Jun 23 nicklas 53 import net.sf.basedb.util.listable.ListableUtil;
7269 26 Jun 23 nicklas 54 import net.sf.basedb.util.listable.RestrictionTransformer;
7269 26 Jun 23 nicklas 55 import net.sf.basedb.util.listable.SourceItemTransformer;
7269 26 Jun 23 nicklas 56 import net.sf.basedb.util.listable.SourceItemTransformerFactory;
7269 26 Jun 23 nicklas 57 import net.sf.basedb.util.listable.TransformContext;
1326 29 Mar 11 nicklas 58
1326 29 Mar 11 nicklas 59
1326 29 Mar 11 nicklas 60 /**
1326 29 Mar 11 nicklas 61   An abstract base class for reggie-specific items that have
1326 29 Mar 11 nicklas 62   been mapped to BASE items using some special rules. This class
1326 29 Mar 11 nicklas 63   can be used on items that are {@link Annotatable} and
1326 29 Mar 11 nicklas 64   {@link Nameable}.
1326 29 Mar 11 nicklas 65   
1326 29 Mar 11 nicklas 66   @author nicklas
1326 29 Mar 11 nicklas 67   @since 1.2
1326 29 Mar 11 nicklas 68 */
1326 29 Mar 11 nicklas 69 public abstract class ReggieItem<T extends Annotatable & Nameable>
1326 29 Mar 11 nicklas 70 {
1326 29 Mar 11 nicklas 71
4881 03 Jul 18 nicklas 72   // Reserved child names -- default timeout is 5 minutes
4883 04 Jul 18 nicklas 73   private static final ReservedItems<String> RESERVED_NAMES = new ReservedItems<String>(300);
4881 03 Jul 18 nicklas 74
1326 29 Mar 11 nicklas 75   private final T item;
1326 29 Mar 11 nicklas 76   private JSONObject json;
3764 22 Feb 16 nicklas 77   private int indexOrder;
1326 29 Mar 11 nicklas 78   
1326 29 Mar 11 nicklas 79   protected ReggieItem(T item)
1326 29 Mar 11 nicklas 80   {
1326 29 Mar 11 nicklas 81     this.item = item;
1326 29 Mar 11 nicklas 82   }
1326 29 Mar 11 nicklas 83
1326 29 Mar 11 nicklas 84   /**
1326 29 Mar 11 nicklas 85     Get the real BASE item that this reggie item represents.
1326 29 Mar 11 nicklas 86   */
1326 29 Mar 11 nicklas 87   public T getItem()
1326 29 Mar 11 nicklas 88   {
1326 29 Mar 11 nicklas 89     return item;
1326 29 Mar 11 nicklas 90   }
1326 29 Mar 11 nicklas 91   
1326 29 Mar 11 nicklas 92   /**
4623 17 Nov 17 nicklas 93     Get the id of the item.
4623 17 Nov 17 nicklas 94     @since 4.13
4623 17 Nov 17 nicklas 95   */
4623 17 Nov 17 nicklas 96   public int getId()
4623 17 Nov 17 nicklas 97   {
4623 17 Nov 17 nicklas 98     return item.getId();
4623 17 Nov 17 nicklas 99   }
4623 17 Nov 17 nicklas 100   
4623 17 Nov 17 nicklas 101   /**
1326 29 Mar 11 nicklas 102     Get the name of the item. 
1326 29 Mar 11 nicklas 103   */
1326 29 Mar 11 nicklas 104   public String getName()
1326 29 Mar 11 nicklas 105   {
1326 29 Mar 11 nicklas 106     return item.getName();
1326 29 Mar 11 nicklas 107   }
1326 29 Mar 11 nicklas 108   
3764 22 Feb 16 nicklas 109   /**
3764 22 Feb 16 nicklas 110     Set an index order value for this item. This can be used to
3764 22 Feb 16 nicklas 111     sort a list of items with the help of {@link IndexOrderComparator}.
3764 22 Feb 16 nicklas 112     @since 4.2
3764 22 Feb 16 nicklas 113   */
3764 22 Feb 16 nicklas 114   public void setIndexOrder(int indexOrder)
3764 22 Feb 16 nicklas 115   {
3764 22 Feb 16 nicklas 116     this.indexOrder = indexOrder;
3764 22 Feb 16 nicklas 117   }
3764 22 Feb 16 nicklas 118   
3764 22 Feb 16 nicklas 119   public int getIndexOrder()
3764 22 Feb 16 nicklas 120   {
3764 22 Feb 16 nicklas 121     return indexOrder;
3764 22 Feb 16 nicklas 122   }
3764 22 Feb 16 nicklas 123   
1521 24 Jan 12 nicklas 124   @Override
1521 24 Jan 12 nicklas 125   public boolean equals(Object o) 
1521 24 Jan 12 nicklas 126   {
1521 24 Jan 12 nicklas 127     if (this == o) return true;
1521 24 Jan 12 nicklas 128     if (o == null || this.getClass() != o.getClass()) return false;
3572 02 Nov 15 nicklas 129     return item.equals(((ReggieItem<?>)o).item);
1521 24 Jan 12 nicklas 130   }
1521 24 Jan 12 nicklas 131
1521 24 Jan 12 nicklas 132   @Override
1521 24 Jan 12 nicklas 133   public int hashCode() 
1521 24 Jan 12 nicklas 134   {
1521 24 Jan 12 nicklas 135     return item.hashCode();
1521 24 Jan 12 nicklas 136   }
1521 24 Jan 12 nicklas 137
1326 29 Mar 11 nicklas 138   /**
1326 29 Mar 11 nicklas 139     Initialize a JSON object with information. Subclasses
1326 29 Mar 11 nicklas 140     should override this method if the need to put in more
3571 30 Oct 15 nicklas 141     than the 'id', 'name' or loaded annotations 
3571 30 Oct 15 nicklas 142     (see {@link #loadAnnotations(DbControl, String, Annotationtype, ValueConverter)})
1326 29 Mar 11 nicklas 143   */
1326 29 Mar 11 nicklas 144   protected void initJSON(JSONObject json)
1326 29 Mar 11 nicklas 145   {}
1326 29 Mar 11 nicklas 146   
1326 29 Mar 11 nicklas 147   /**
1326 29 Mar 11 nicklas 148     Get the information as a JSON object ready to be sent as an AJAX response.
1326 29 Mar 11 nicklas 149   */
1326 29 Mar 11 nicklas 150   public JSONObject asJSONObject()
1326 29 Mar 11 nicklas 151   {
1326 29 Mar 11 nicklas 152     if (json == null)
1326 29 Mar 11 nicklas 153     {
1326 29 Mar 11 nicklas 154       json = new JSONObject();
1326 29 Mar 11 nicklas 155       json.put("id", item.getId());
1326 29 Mar 11 nicklas 156       json.put("name", item.getName());
1326 29 Mar 11 nicklas 157       initJSON(json);
1326 29 Mar 11 nicklas 158     }
1326 29 Mar 11 nicklas 159     return json;
1326 29 Mar 11 nicklas 160   }
1326 29 Mar 11 nicklas 161   
1326 29 Mar 11 nicklas 162   /**
6218 20 Apr 21 nicklas 163     Get the first non-null value of 'key' and 'keys' 
6218 20 Apr 21 nicklas 164     and put it into 'key'. Return the value.
6218 20 Apr 21 nicklas 165     @since 4.32
6218 20 Apr 21 nicklas 166   */
6218 20 Apr 21 nicklas 167   public Object coalesceInto(String key, String... keys)
6218 20 Apr 21 nicklas 168   {
6218 20 Apr 21 nicklas 169     JSONObject json = asJSONObject();
6218 20 Apr 21 nicklas 170     Object val = json.get(key);
6218 20 Apr 21 nicklas 171     int index = 0;
6218 20 Apr 21 nicklas 172     while (val == null && index < keys.length)
6218 20 Apr 21 nicklas 173     {
6218 20 Apr 21 nicklas 174       val = json.get(keys[index]);
6218 20 Apr 21 nicklas 175       index++;
6218 20 Apr 21 nicklas 176     }
6218 20 Apr 21 nicklas 177     if (index > 0) json.put(key, val);
6218 20 Apr 21 nicklas 178     return val;
6218 20 Apr 21 nicklas 179   }
6218 20 Apr 21 nicklas 180   
6218 20 Apr 21 nicklas 181   /**
1326 29 Mar 11 nicklas 182     Load annotations for the given annotation type and store the values in
1326 29 Mar 11 nicklas 183     the given JSON key. Single-valued annotation types are stored as a simple
1326 29 Mar 11 nicklas 184     key-value pair. Multi-valued annotation types are stored as a
1326 29 Mar 11 nicklas 185     key-array pair (even if there is only one value in the array).
1326 29 Mar 11 nicklas 186   */
3572 02 Nov 15 nicklas 187   @SuppressWarnings({ "unchecked", "rawtypes" })
1610 23 Apr 12 nicklas 188   public void loadAnnotations(DbControl dc, String jsonKey, Annotationtype annotationType, ValueConverter converter)
1326 29 Mar 11 nicklas 189   {
1333 05 Apr 11 nicklas 190     if (converter == null) converter = IdentityConverter.INSTANCE;
1821 06 Feb 13 nicklas 191     try
1326 29 Mar 11 nicklas 192     {
4009 23 Jun 16 nicklas 193       JSONObject json = asJSONObject();
1821 06 Feb 13 nicklas 194       AnnotationType at = annotationType.load(dc);
1821 06 Feb 13 nicklas 195       if (at.getMultiplicity() == 1)
1821 06 Feb 13 nicklas 196       {
1821 06 Feb 13 nicklas 197         json.put(jsonKey, converter.convert(annotationType.getAnnotationValue(dc, item)));
1821 06 Feb 13 nicklas 198       }
1821 06 Feb 13 nicklas 199       else
1821 06 Feb 13 nicklas 200       {
1821 06 Feb 13 nicklas 201         json.put(jsonKey, JsonUtil.toArray(annotationType.getAnnotationValues(dc, item), converter));
1821 06 Feb 13 nicklas 202       }
1326 29 Mar 11 nicklas 203     }
1821 06 Feb 13 nicklas 204     catch (PermissionDeniedException ex)
1326 29 Mar 11 nicklas 205     {
1821 06 Feb 13 nicklas 206       json.put(jsonKey, "No permission");
1326 29 Mar 11 nicklas 207     }
1821 06 Feb 13 nicklas 208     catch (ItemNotFoundException ex)
1821 06 Feb 13 nicklas 209     {
1821 06 Feb 13 nicklas 210       json.put(jsonKey, "Not found (may indicate lack of permission)");
1821 06 Feb 13 nicklas 211     }
1821 06 Feb 13 nicklas 212     catch (RuntimeException ex)
1821 06 Feb 13 nicklas 213     {
1821 06 Feb 13 nicklas 214       json.put(jsonKey, ex.getMessage());
1821 06 Feb 13 nicklas 215     }
1326 29 Mar 11 nicklas 216   }
1326 29 Mar 11 nicklas 217   
1326 29 Mar 11 nicklas 218   /**
3166 05 Mar 15 nicklas 219     Load annotations for the given annotation type and store the values in
3166 05 Mar 15 nicklas 220     the given JSON key. Single-valued annotation types are stored as a simple
3166 05 Mar 15 nicklas 221     key-value pair. Multi-valued annotation types are stored as a
3166 05 Mar 15 nicklas 222     key-array pair (even if there is only one value in the array).
3166 05 Mar 15 nicklas 223     @since 3.2
3166 05 Mar 15 nicklas 224   */
3572 02 Nov 15 nicklas 225   @SuppressWarnings({ "unchecked", "rawtypes" })
3166 05 Mar 15 nicklas 226   public void loadAnnotations(DbControl dc, SnapshotManager manager, String jsonKey, Annotationtype annotationType, ValueConverter converter)
3166 05 Mar 15 nicklas 227   {
3166 05 Mar 15 nicklas 228     if (converter == null) converter = IdentityConverter.INSTANCE;
3166 05 Mar 15 nicklas 229     try
3166 05 Mar 15 nicklas 230     {
4009 23 Jun 16 nicklas 231       JSONObject json = asJSONObject();
3166 05 Mar 15 nicklas 232       AnnotationType at = annotationType.load(dc);
3166 05 Mar 15 nicklas 233       if (at.getMultiplicity() == 1)
3166 05 Mar 15 nicklas 234       {
3166 05 Mar 15 nicklas 235         json.put(jsonKey, converter.convert(annotationType.getAnnotationValue(dc, manager, item)));
3166 05 Mar 15 nicklas 236       }
3166 05 Mar 15 nicklas 237       else
3166 05 Mar 15 nicklas 238       {
3166 05 Mar 15 nicklas 239         json.put(jsonKey, JsonUtil.toArray(annotationType.getAnnotationValues(dc, manager, item), converter));
3166 05 Mar 15 nicklas 240       }
3166 05 Mar 15 nicklas 241     }
3166 05 Mar 15 nicklas 242     catch (PermissionDeniedException ex)
3166 05 Mar 15 nicklas 243     {
3166 05 Mar 15 nicklas 244       json.put(jsonKey, "No permission");
3166 05 Mar 15 nicklas 245     }
3166 05 Mar 15 nicklas 246     catch (ItemNotFoundException ex)
3166 05 Mar 15 nicklas 247     {
3166 05 Mar 15 nicklas 248       json.put(jsonKey, "Not found (may indicate lack of permission)");
3166 05 Mar 15 nicklas 249     }
3166 05 Mar 15 nicklas 250     catch (RuntimeException ex)
3166 05 Mar 15 nicklas 251     {
3166 05 Mar 15 nicklas 252       json.put(jsonKey, ex.getMessage());
3166 05 Mar 15 nicklas 253     }
3166 05 Mar 15 nicklas 254   }
3166 05 Mar 15 nicklas 255
3166 05 Mar 15 nicklas 256   /**
1326 29 Mar 11 nicklas 257     Manually set an annotation value. Or.. actually simply call
1326 29 Mar 11 nicklas 258     {@link JSONObject#put(Object, Object)} with the given key and value.
1326 29 Mar 11 nicklas 259   */
1326 29 Mar 11 nicklas 260   public void setAnnotation(String jsonKey, Object value)
1326 29 Mar 11 nicklas 261   {
1326 29 Mar 11 nicklas 262     JSONObject json = asJSONObject();
1326 29 Mar 11 nicklas 263     json.put(jsonKey, value);
1326 29 Mar 11 nicklas 264   }
3503 22 Sep 15 nicklas 265   
3503 22 Sep 15 nicklas 266   /**
4983 27 Sep 18 nicklas 267     Helper method for loading the DO_NOT_USE and DO_NOT_USE_COMMENT annotations
4983 27 Sep 18 nicklas 268     on items that support it.
4983 27 Sep 18 nicklas 269     @since 4.20
4983 27 Sep 18 nicklas 270   */
4983 27 Sep 18 nicklas 271   public void loadDoNotUseAnnotations(DbControl dc, SnapshotManager manager)
4983 27 Sep 18 nicklas 272   {
4983 27 Sep 18 nicklas 273     loadAnnotations(dc, manager, "DO_NOT_USE", Annotationtype.DO_NOT_USE, null);
4983 27 Sep 18 nicklas 274     loadAnnotations(dc, manager, "DO_NOT_USE_COMMENT", Annotationtype.DO_NOT_USE_COMMENT, null);
4983 27 Sep 18 nicklas 275   }
4983 27 Sep 18 nicklas 276   
5789 13 Dec 19 nicklas 277   
4983 27 Sep 18 nicklas 278   /**
3503 22 Sep 15 nicklas 279     Utility method for finding parent biomaterial of the specified subtypes
3503 22 Sep 15 nicklas 280     by following the chain of parent items up to biosource.
3503 22 Sep 15 nicklas 281     The starting point can be either a biomaterial, a raw bioassay or a 
3503 22 Sep 15 nicklas 282     derived bioassay with a link to a parent extract (library).
3503 22 Sep 15 nicklas 283     @since 3.7
3503 22 Sep 15 nicklas 284   */
3503 22 Sep 15 nicklas 285   public Map<Subtype, BioMaterial> findParentBioMaterial(DbControl dc, Subtype... subtypes)
3503 22 Sep 15 nicklas 286   {
3503 22 Sep 15 nicklas 287     Map<Subtype, BioMaterial> parents = new HashMap<Subtype, BioMaterial>();
3503 22 Sep 15 nicklas 288     for (Subtype sub : subtypes)
3503 22 Sep 15 nicklas 289     {
3503 22 Sep 15 nicklas 290       sub.load(dc);
3503 22 Sep 15 nicklas 291       parents.put(sub, null);
3503 22 Sep 15 nicklas 292     }
3503 22 Sep 15 nicklas 293     
3503 22 Sep 15 nicklas 294     
3503 22 Sep 15 nicklas 295     BioMaterial bm = null;
3503 22 Sep 15 nicklas 296     if (item instanceof BioMaterial)
3503 22 Sep 15 nicklas 297     {
3503 22 Sep 15 nicklas 298       bm = (BioMaterial)item;
3503 22 Sep 15 nicklas 299     }
3503 22 Sep 15 nicklas 300     else if (item instanceof RawBioAssay)
3503 22 Sep 15 nicklas 301     {
3503 22 Sep 15 nicklas 302       bm = ((RawBioAssay)item).getParentExtract();
3503 22 Sep 15 nicklas 303     }
3503 22 Sep 15 nicklas 304     else if (item instanceof DerivedBioAssay)
3503 22 Sep 15 nicklas 305     {
3503 22 Sep 15 nicklas 306       bm = ((DerivedBioAssay)item).getExtract();
3503 22 Sep 15 nicklas 307     }
3503 22 Sep 15 nicklas 308     
6075 23 Nov 20 nicklas 309     int numItems = 0;
6075 23 Nov 20 nicklas 310     while (bm != null && numItems < subtypes.length)
3503 22 Sep 15 nicklas 311     {
3503 22 Sep 15 nicklas 312       ItemSubtype bmType = bm.getItemSubtype();
3503 22 Sep 15 nicklas 313       
6946 06 Dec 22 nicklas 314       if (bmType != null)
6075 23 Nov 20 nicklas 315       {
6946 06 Dec 22 nicklas 316         Subtype st = Subtype.get(bmType);
6946 06 Dec 22 nicklas 317         if (parents.containsKey(st)) 
6946 06 Dec 22 nicklas 318         {
7007 25 Jan 23 nicklas 319           // Increase the number of items found only if it was a new type
7007 25 Jan 23 nicklas 320           if (parents.put(st, bm) == null) numItems++;
6946 06 Dec 22 nicklas 321         }
6075 23 Nov 20 nicklas 322       }
3503 22 Sep 15 nicklas 323       
3503 22 Sep 15 nicklas 324       if (bm instanceof MeasuredBioMaterial)
3503 22 Sep 15 nicklas 325       {
4426 27 Mar 17 nicklas 326         // Get parent, may be null if starting with item derived from external RNA!
3503 22 Sep 15 nicklas 327         bm = ((MeasuredBioMaterial)bm).getParent();
3503 22 Sep 15 nicklas 328       }
3503 22 Sep 15 nicklas 329       else
3503 22 Sep 15 nicklas 330       {
3503 22 Sep 15 nicklas 331         // We have reached the patient!
4426 27 Mar 17 nicklas 332         bm = null;
3503 22 Sep 15 nicklas 333       }
3503 22 Sep 15 nicklas 334     }
3503 22 Sep 15 nicklas 335     
3503 22 Sep 15 nicklas 336     return parents;
3503 22 Sep 15 nicklas 337
3503 22 Sep 15 nicklas 338   }
3503 22 Sep 15 nicklas 339   
3764 22 Feb 16 nicklas 340   /**
3792 18 Mar 16 nicklas 341     Utility method for finding a single parent item of a given type.
3792 18 Mar 16 nicklas 342     If you need to find multiple parents use the {@link #findParentBioMaterial(DbControl, Subtype...)}
3792 18 Mar 16 nicklas 343     since it is more efficient.
3792 18 Mar 16 nicklas 344     @return The found biomaterial or null if not found
3792 18 Mar 16 nicklas 345     @since 4.3
3792 18 Mar 16 nicklas 346   */
3792 18 Mar 16 nicklas 347   public BioMaterial findSingleParent(DbControl dc, Subtype subtype)
3792 18 Mar 16 nicklas 348   {
3792 18 Mar 16 nicklas 349     return findParentBioMaterial(dc, subtype).get(subtype);
3792 18 Mar 16 nicklas 350   }
3792 18 Mar 16 nicklas 351   
7269 26 Jun 23 nicklas 352   /**
7269 26 Jun 23 nicklas 353     Create a query for finding child items of the given subtype.
7269 26 Jun 23 nicklas 354     @since 4.49
7269 26 Jun 23 nicklas 355   */
7269 26 Jun 23 nicklas 356   public <C extends BasicItem> ItemQuery<C> findChildItems(DbControl dc, Subtype childType)
7269 26 Jun 23 nicklas 357   {
7269 26 Jun 23 nicklas 358     SourceItemTransformerFactory stf = ListableUtil.getTransformerFactory(childType.getMainType());
7269 26 Jun 23 nicklas 359     SourceItemTransformer st = stf.create(getItem().getType(), SourceItemTransform.PARENT_TO_CHILD);
7269 26 Jun 23 nicklas 360     st = new RestrictionTransformer(st, childType.restriction(dc, null));
7269 26 Jun 23 nicklas 361     TransformContext ctx = new TransformContext(dc);
7269 26 Jun 23 nicklas 362     ctx.setInclude(Reggie.INCLUDE_IN_CURRENT_PROJECT);
7269 26 Jun 23 nicklas 363     
7269 26 Jun 23 nicklas 364     Set<Integer> children = st.transform(ctx, Collections.singleton(getId()));
7269 26 Jun 23 nicklas 365     ItemQuery<C> query = childType.getMainType().getQuery();
7269 26 Jun 23 nicklas 366     query.setIncludes(Reggie.INCLUDE_IN_CURRENT_PROJECT);
7269 26 Jun 23 nicklas 367     query.restrictPermanent(new IdListRestriction(children));
7269 26 Jun 23 nicklas 368     return query;
7269 26 Jun 23 nicklas 369   }
4893 09 Jul 18 nicklas 370
5694 31 Oct 19 nicklas 371   /**
5694 31 Oct 19 nicklas 372     Get a file that is linked via an any-to-any link with a known
5694 31 Oct 19 nicklas 373     name.
5694 31 Oct 19 nicklas 374     @since 4.24
5694 31 Oct 19 nicklas 375   */
5694 31 Oct 19 nicklas 376   public File getLinkedFile(DbControl dc, String linkName)
5694 31 Oct 19 nicklas 377   {
5694 31 Oct 19 nicklas 378     File file = null;
5694 31 Oct 19 nicklas 379     try
5694 31 Oct 19 nicklas 380     {
5694 31 Oct 19 nicklas 381       AnyToAny link = AnyToAny.getByName(dc, (BasicItem)getItem(), linkName);
5694 31 Oct 19 nicklas 382       if (link.getToType() == Item.FILE) file = (File)link.getTo();
5694 31 Oct 19 nicklas 383     }
5694 31 Oct 19 nicklas 384     catch (ItemNotFoundException ex)
5694 31 Oct 19 nicklas 385     {}
5694 31 Oct 19 nicklas 386     return file;
5694 31 Oct 19 nicklas 387   }
5694 31 Oct 19 nicklas 388   
7436 15 Nov 23 nicklas 389   /**
7436 15 Nov 23 nicklas 390     Get an item that is linked via an any-to-any link with a known name.
7436 15 Nov 23 nicklas 391     If the link or item doesn't exists null is returned.
7436 15 Nov 23 nicklas 392     @since 4.50
7436 15 Nov 23 nicklas 393   */
7436 15 Nov 23 nicklas 394   @SuppressWarnings("unchecked")
7436 15 Nov 23 nicklas 395   public <L extends BasicItem> L getLinkedItem(DbControl dc, String linkName)
7436 15 Nov 23 nicklas 396   {
7436 15 Nov 23 nicklas 397     L item = null;
7436 15 Nov 23 nicklas 398     try
7436 15 Nov 23 nicklas 399     {
7436 15 Nov 23 nicklas 400       AnyToAny link = AnyToAny.getByName(dc, (BasicItem)getItem(), linkName);
7436 15 Nov 23 nicklas 401       item = (L)link.getTo();
7436 15 Nov 23 nicklas 402     }
7436 15 Nov 23 nicklas 403     catch (ItemNotFoundException ex)
7436 15 Nov 23 nicklas 404     {}
7436 15 Nov 23 nicklas 405     return item;
7436 15 Nov 23 nicklas 406   }
7436 15 Nov 23 nicklas 407
7436 15 Nov 23 nicklas 408   
4893 09 Jul 18 nicklas 409   protected String getNextChildItemName(DbControl dc, ItemQuery<? extends Nameable> childQuery, String suffix, boolean releaseIfTransactionFails)
4893 09 Jul 18 nicklas 410   {
4893 09 Jul 18 nicklas 411     return getNextChildItemName(dc, getName(), childQuery, suffix, releaseIfTransactionFails);
4893 09 Jul 18 nicklas 412   }
4893 09 Jul 18 nicklas 413
3792 18 Mar 16 nicklas 414   /**
4881 03 Jul 18 nicklas 415     Generates a new child item name based on existing child items. The given
4881 03 Jul 18 nicklas 416     query is assumed to return all existing child items. They should all have a
4881 03 Jul 18 nicklas 417     base name starting with the name of this item + the suffix (separated with a .)
4881 03 Jul 18 nicklas 418     
4881 03 Jul 18 nicklas 419     If no child items exists the first child is given the base name. Subsequent child
4898 10 Jul 18 nicklas 420     items are indexed with 2, 3, 4, and so on. Exception, if the suffix is an empty string
4898 10 Jul 18 nicklas 421     the first child item get index = 1.
6193 30 Mar 21 nicklas 422
6193 30 Mar 21 nicklas 423     If the query is null it is assumed that the parent item is a newly created 
6193 30 Mar 21 nicklas 424     item that can't have any existing children.
4881 03 Jul 18 nicklas 425     
4881 03 Jul 18 nicklas 426     Generated names are reserved for 5 minutes, but can optionally be released
4881 03 Jul 18 nicklas 427     immediately if the transaction fails. Typically, this parameter should be TRUE 
4881 03 Jul 18 nicklas 428     if the name is generated and a new item is created and saved to the database 
4881 03 Jul 18 nicklas 429     in a single transaction. This will release the reserved name in case the
4881 03 Jul 18 nicklas 430     transaction fails and should avoid unnecessary "holes" in the child names 
4881 03 Jul 18 nicklas 431     when re-trying the action.
4881 03 Jul 18 nicklas 432     
4881 03 Jul 18 nicklas 433     The parameter should be FALSE if the name is generated in the beginning of a 
4881 03 Jul 18 nicklas 434     multi-step wizard and not saved to the database until the final step in the 
4881 03 Jul 18 nicklas 435     wizard. This may lead to "holes" in the child names if a wizard is not 
4881 03 Jul 18 nicklas 436     completed, but there is no easy solution for that. 
4881 03 Jul 18 nicklas 437     
4881 03 Jul 18 nicklas 438     Calling this method more than once within the same transaction
4881 03 Jul 18 nicklas 439     should create unique names every time.
4883 04 Jul 18 nicklas 440     @since 4.19
4881 03 Jul 18 nicklas 441   */
4893 09 Jul 18 nicklas 442   public static String getNextChildItemName(DbControl dc, String parentName, ItemQuery<? extends Nameable> childQuery, String suffix, boolean releaseIfTransactionFails)
4881 03 Jul 18 nicklas 443   {
4881 03 Jul 18 nicklas 444     
4881 03 Jul 18 nicklas 445     // All child item names should start with this string
4893 09 Jul 18 nicklas 446     String baseName = parentName+"."+suffix;
4881 03 Jul 18 nicklas 447     
4881 03 Jul 18 nicklas 448     int maxChildIndex = 0; // The highest index of a child item we can find
6193 30 Mar 21 nicklas 449     if (childQuery != null)
4881 03 Jul 18 nicklas 450     {
6193 30 Mar 21 nicklas 451       List<? extends Nameable> existingChildren = childQuery.list(dc);
6193 30 Mar 21 nicklas 452       for (Nameable child : existingChildren)
5188 07 Dec 18 nicklas 453       {
6193 30 Mar 21 nicklas 454         String childName = child.getName();
6193 30 Mar 21 nicklas 455         if (!childName.startsWith(baseName))
5188 07 Dec 18 nicklas 456         {
5188 07 Dec 18 nicklas 457           throw new InvalidDataException("Child item '" + childName + "' of '" + parentName + 
6193 30 Mar 21 nicklas 458               "' has unexpected name. It should start with '" + baseName + "'.");
5188 07 Dec 18 nicklas 459         }
6193 30 Mar 21 nicklas 460   
6193 30 Mar 21 nicklas 461         int childIndex = 0;
6193 30 Mar 21 nicklas 462         String digits = childName.substring(baseName.length());
6193 30 Mar 21 nicklas 463         
6193 30 Mar 21 nicklas 464         // The first child has no digits and this string may be empty
6193 30 Mar 21 nicklas 465         if (digits.equals(""))
6193 30 Mar 21 nicklas 466         {
6193 30 Mar 21 nicklas 467           childIndex = 1;
6193 30 Mar 21 nicklas 468         }
6193 30 Mar 21 nicklas 469         else
6193 30 Mar 21 nicklas 470         {
6193 30 Mar 21 nicklas 471           // Oyherwise, the rest of the name should be digits
6193 30 Mar 21 nicklas 472           if (!digits.matches("\\d+"))
6193 30 Mar 21 nicklas 473           {
6193 30 Mar 21 nicklas 474             throw new InvalidDataException("Child item '" + childName + "' of '" + parentName + 
6193 30 Mar 21 nicklas 475               "' has unexpected name. Digits expected but found '" + digits + "'.");
6193 30 Mar 21 nicklas 476           }
6193 30 Mar 21 nicklas 477           childIndex = Values.getInt(digits);
6193 30 Mar 21 nicklas 478         }
6193 30 Mar 21 nicklas 479         if (childIndex > maxChildIndex) maxChildIndex = childIndex;
4881 03 Jul 18 nicklas 480       }
4881 03 Jul 18 nicklas 481     }
4881 03 Jul 18 nicklas 482     
4881 03 Jul 18 nicklas 483     int nextChildIndex = maxChildIndex+1;
4898 10 Jul 18 nicklas 484     String childName = baseName + (nextChildIndex == 1 && suffix.length() > 0 ? "" : nextChildIndex);
4881 03 Jul 18 nicklas 485     
4881 03 Jul 18 nicklas 486     int numIterations = 0;
4889 06 Jul 18 nicklas 487     while (!RESERVED_NAMES.reserve(dc, childName))
4881 03 Jul 18 nicklas 488     {
4881 03 Jul 18 nicklas 489       nextChildIndex++;
4881 03 Jul 18 nicklas 490       childName = baseName+nextChildIndex;
4881 03 Jul 18 nicklas 491       numIterations++;
4881 03 Jul 18 nicklas 492       if (numIterations == 100)
4881 03 Jul 18 nicklas 493       {
4893 09 Jul 18 nicklas 494         throw new RuntimeException("Failed to generate a child name for '" + parentName + 
4881 03 Jul 18 nicklas 495           "' after 100 tries. Last tested child name was '" + childName + "'.");
4881 03 Jul 18 nicklas 496       }
4881 03 Jul 18 nicklas 497     }
4881 03 Jul 18 nicklas 498     
4881 03 Jul 18 nicklas 499     if (releaseIfTransactionFails) 
4881 03 Jul 18 nicklas 500     {
4883 04 Jul 18 nicklas 501       RESERVED_NAMES.releaseIfTransactionFails(dc, childName);
4881 03 Jul 18 nicklas 502     }
4881 03 Jul 18 nicklas 503
4881 03 Jul 18 nicklas 504     return childName;
4881 03 Jul 18 nicklas 505   }
4881 03 Jul 18 nicklas 506   
4883 04 Jul 18 nicklas 507   /**
4883 04 Jul 18 nicklas 508     Creates a single name. 
4883 04 Jul 18 nicklas 509     @see #getNextItemNames(DbControl, int, ItemQuery, String, int, boolean)
4883 04 Jul 18 nicklas 510   */
4893 09 Jul 18 nicklas 511   public static String getNextItemName(DbControl dc, ItemQuery<? extends Nameable> query, String prefix, int numDigitsInName, boolean releaseIfTransactionFails)
4883 04 Jul 18 nicklas 512   {
4883 04 Jul 18 nicklas 513     return getNextItemNames(dc, 1, query, prefix, numDigitsInName, releaseIfTransactionFails).get(0);
4883 04 Jul 18 nicklas 514   }
4881 03 Jul 18 nicklas 515   
4881 03 Jul 18 nicklas 516   /**
4883 04 Jul 18 nicklas 517     Generates one or more new item names based on existing items. The given query is assumed to return 
4883 04 Jul 18 nicklas 518     all existing items that should be considered. This method will add a filter on the 
4883 04 Jul 18 nicklas 519     name column that must start with the given "prefix" and then be followd by only digits.
4883 04 Jul 18 nicklas 520     This method will also re-sort the query in descending name order (so that the first
4883 04 Jul 18 nicklas 521     item is the one with the highest index).
4883 04 Jul 18 nicklas 522     
4883 04 Jul 18 nicklas 523     Generated names are reserved for 5 minutes, but can optionally be released
4883 04 Jul 18 nicklas 524     immediately if the transaction fails. Typically, this parameter should be TRUE 
4883 04 Jul 18 nicklas 525     if the name is generated and a new item is created and saved to the database 
4883 04 Jul 18 nicklas 526     in a single transaction. This will release the reserved name in case the
4883 04 Jul 18 nicklas 527     transaction fails and should avoid unnecessary "holes" among the names when 
4883 04 Jul 18 nicklas 528     re-trying the action.
4883 04 Jul 18 nicklas 529     
4883 04 Jul 18 nicklas 530     The parameter should be FALSE if the name is generated in the beginning of a 
4883 04 Jul 18 nicklas 531     multi-step wizard and not saved to the database until the final step in the 
4883 04 Jul 18 nicklas 532     wizard. This may lead to "holes" in the sequence of names if a wizard is not 
4883 04 Jul 18 nicklas 533     completed, but there is no easy solution for that. 
4902 10 Jul 18 nicklas 534         
4883 04 Jul 18 nicklas 535     Calling this method more than once within the same transaction
4883 04 Jul 18 nicklas 536     should create unique names every time.
4883 04 Jul 18 nicklas 537     
4883 04 Jul 18 nicklas 538     @since 4.19
4883 04 Jul 18 nicklas 539   */
4883 04 Jul 18 nicklas 540   protected static List<String> getNextItemNames(DbControl dc, int numNames, ItemQuery<? extends Nameable> query, String prefix, int numDigitsInName, boolean releaseIfTransactionFails)
4883 04 Jul 18 nicklas 541   {
4883 04 Jul 18 nicklas 542     // Add filter to the query and re-sort on name in descending order
4883 04 Jul 18 nicklas 543     query.restrict(Restrictions.rlike(Hql.property("name"), Expressions.string("^"+prefix + "[0-9]+$")));
4883 04 Jul 18 nicklas 544     query.order(Orders.desc(Hql.property("name")));
4883 04 Jul 18 nicklas 545     query.setMaxResults(1);
4902 10 Jul 18 nicklas 546     query.include(Include.ALL);
4883 04 Jul 18 nicklas 547
4883 04 Jul 18 nicklas 548     List<? extends Nameable> result = query.list(dc);
4883 04 Jul 18 nicklas 549     int nextIndex = 1;
4883 04 Jul 18 nicklas 550     if (result.size() > 0)
4883 04 Jul 18 nicklas 551     {
4883 04 Jul 18 nicklas 552       String lastItemName = result.get(0).getName();
4883 04 Jul 18 nicklas 553       String digits = lastItemName.substring(prefix.length());
4883 04 Jul 18 nicklas 554       if (!digits.matches("\\d+"))
4883 04 Jul 18 nicklas 555       {
4883 04 Jul 18 nicklas 556         throw new InvalidDataException("Item '" + lastItemName + 
4883 04 Jul 18 nicklas 557           "' has unexpected name. Digits expected after '"+prefix + "' but found '" + digits + "'.");
4883 04 Jul 18 nicklas 558       }
4883 04 Jul 18 nicklas 559       nextIndex = Values.getInt(digits)+1;
4883 04 Jul 18 nicklas 560     }
4883 04 Jul 18 nicklas 561     
4883 04 Jul 18 nicklas 562     int numIterations = 0;
4883 04 Jul 18 nicklas 563     // This will hold the generated names
4883 04 Jul 18 nicklas 564     List<String> nextNames = new ArrayList<>(numNames);
4883 04 Jul 18 nicklas 565     while (true)
4883 04 Jul 18 nicklas 566     {
4883 04 Jul 18 nicklas 567       String nextName = prefix + MD5.leftPad(Integer.toString(nextIndex), '0', numDigitsInName);    
4889 06 Jul 18 nicklas 568       if (RESERVED_NAMES.reserve(dc, nextName)) 
4883 04 Jul 18 nicklas 569       {
4883 04 Jul 18 nicklas 570         if (releaseIfTransactionFails) 
4883 04 Jul 18 nicklas 571         {
4883 04 Jul 18 nicklas 572           RESERVED_NAMES.releaseIfTransactionFails(dc, nextName);
4883 04 Jul 18 nicklas 573         }
4883 04 Jul 18 nicklas 574         nextNames.add(nextName);
4883 04 Jul 18 nicklas 575         if (nextNames.size() == numNames) break; // We have enough names
4883 04 Jul 18 nicklas 576         numIterations = 0;
4883 04 Jul 18 nicklas 577       }
4883 04 Jul 18 nicklas 578       nextIndex++;
4883 04 Jul 18 nicklas 579       numIterations++;
4883 04 Jul 18 nicklas 580       if (numIterations == 100)
4883 04 Jul 18 nicklas 581       {
4883 04 Jul 18 nicklas 582         throw new RuntimeException("Failed to generate a name for '" + prefix + 
4883 04 Jul 18 nicklas 583           "' after 100 tries. Last tested name was '" + nextName + "'.");
4883 04 Jul 18 nicklas 584       }
4883 04 Jul 18 nicklas 585     }
4889 06 Jul 18 nicklas 586     
4883 04 Jul 18 nicklas 587     return nextNames;
4883 04 Jul 18 nicklas 588   }
4883 04 Jul 18 nicklas 589   
4883 04 Jul 18 nicklas 590   /**
4902 10 Jul 18 nicklas 591     Generate the next auto-generated external ID. This method will search all items
4902 10 Jul 18 nicklas 592     starting with the given prefix and find the one with the highest numeric suffix. 
4902 10 Jul 18 nicklas 593     The returned string is the found index + 1. This method uses the
4902 10 Jul 18 nicklas 594     {@link ReservedItems} to make sure that the same id is not generated
4902 10 Jul 18 nicklas 595     twice.
4897 10 Jul 18 nicklas 596     @since 4.19
4902 10 Jul 18 nicklas 597   */
4902 10 Jul 18 nicklas 598   public static String getNextExternalId(DbControl dc, ItemQuery<? extends BioMaterial> query, String prefix)
4902 10 Jul 18 nicklas 599   {
4902 10 Jul 18 nicklas 600     query.restrict(Restrictions.rlike(Hql.property("externalId"), Expressions.string("^" + prefix + "[0-9]+$")));
4902 10 Jul 18 nicklas 601     query.order(Orders.desc(Hql.property("externalId")));
4902 10 Jul 18 nicklas 602     query.setMaxResults(1);
4902 10 Jul 18 nicklas 603     query.include(Include.ALL);
4902 10 Jul 18 nicklas 604     
4902 10 Jul 18 nicklas 605     
4902 10 Jul 18 nicklas 606     List<? extends BioMaterial> result = query.list(dc);
4902 10 Jul 18 nicklas 607     int nextIndex = 1;
4902 10 Jul 18 nicklas 608     if (result.size() > 0)
4902 10 Jul 18 nicklas 609     {
4902 10 Jul 18 nicklas 610       String lastItemId = result.get(0).getExternalId();
4902 10 Jul 18 nicklas 611       String digits = lastItemId.substring(prefix.length());
4902 10 Jul 18 nicklas 612       if (!digits.matches("\\d+"))
4902 10 Jul 18 nicklas 613       {
4902 10 Jul 18 nicklas 614         throw new InvalidDataException("Item '" + lastItemId + 
4902 10 Jul 18 nicklas 615           "' has unexpected name. Digits expected after '"+prefix + "' but found '" + digits + "'.");
4902 10 Jul 18 nicklas 616       }
4902 10 Jul 18 nicklas 617       nextIndex = Values.getInt(digits)+1;
4902 10 Jul 18 nicklas 618     }
4902 10 Jul 18 nicklas 619     
4902 10 Jul 18 nicklas 620     int numDigitsInName = 6;
4902 10 Jul 18 nicklas 621     int numIterations = 0;
4902 10 Jul 18 nicklas 622     // This will hold the generated names
4902 10 Jul 18 nicklas 623     String nextExternalId;
4902 10 Jul 18 nicklas 624     while (true)
4902 10 Jul 18 nicklas 625     {
4902 10 Jul 18 nicklas 626       nextExternalId = prefix + MD5.leftPad(Integer.toString(nextIndex), '0', numDigitsInName);    
4902 10 Jul 18 nicklas 627       if (RESERVED_NAMES.reserve(dc, nextExternalId)) 
4902 10 Jul 18 nicklas 628       {
4902 10 Jul 18 nicklas 629         RESERVED_NAMES.releaseIfTransactionFails(dc, nextExternalId);
4902 10 Jul 18 nicklas 630         break;
4902 10 Jul 18 nicklas 631       }
4902 10 Jul 18 nicklas 632       nextIndex++;
4902 10 Jul 18 nicklas 633       numIterations++;
4902 10 Jul 18 nicklas 634       if (numIterations == 100)
4902 10 Jul 18 nicklas 635       {
4902 10 Jul 18 nicklas 636         throw new RuntimeException("Failed to generate an external ID for '" + prefix + 
4902 10 Jul 18 nicklas 637           "' after 100 tries. Last tested value was '" + nextExternalId + "'.");
4902 10 Jul 18 nicklas 638       }
4902 10 Jul 18 nicklas 639     }
4902 10 Jul 18 nicklas 640     
4902 10 Jul 18 nicklas 641     return nextExternalId;
4902 10 Jul 18 nicklas 642   }
4902 10 Jul 18 nicklas 643
4902 10 Jul 18 nicklas 644   
4902 10 Jul 18 nicklas 645   /**
5777 06 Dec 19 nicklas 646     Generate the next auto-generated external ID based on one that was previously
5777 06 Dec 19 nicklas 647     generated and reserved. The previous ID is one that is generated by either this
5777 06 Dec 19 nicklas 648     method or {@link ##getNextExternalId(DbControl, ItemQuery, String)}.
5777 06 Dec 19 nicklas 649     @since 4.25
5777 06 Dec 19 nicklas 650   */
5777 06 Dec 19 nicklas 651   public static String getNextExternalId(DbControl dc, String previousId)
5777 06 Dec 19 nicklas 652   {
5777 06 Dec 19 nicklas 653     Pattern p = Pattern.compile("(\\D+)(\\d+)");
5777 06 Dec 19 nicklas 654     
5777 06 Dec 19 nicklas 655     Matcher m = p.matcher(previousId);
5777 06 Dec 19 nicklas 656     if (!m.matches()) throw new InvalidDataException("Previous ID doens't match pattern \\w+\\d+: " + previousId);
5777 06 Dec 19 nicklas 657
5777 06 Dec 19 nicklas 658     String prefix = m.group(1);
5777 06 Dec 19 nicklas 659     String baseNumber = m.group(2);
5777 06 Dec 19 nicklas 660     
5777 06 Dec 19 nicklas 661     int nextIndex = Values.getInt(baseNumber) + 1;
5777 06 Dec 19 nicklas 662     
5777 06 Dec 19 nicklas 663     int numDigitsInName = baseNumber.length();
5777 06 Dec 19 nicklas 664     int numIterations = 0;
5777 06 Dec 19 nicklas 665     // This will hold the generated names
5777 06 Dec 19 nicklas 666     String nextExternalId;
5777 06 Dec 19 nicklas 667     while (true)
5777 06 Dec 19 nicklas 668     {
5777 06 Dec 19 nicklas 669       nextExternalId = prefix + MD5.leftPad(Integer.toString(nextIndex), '0', numDigitsInName);    
5777 06 Dec 19 nicklas 670       if (RESERVED_NAMES.reserve(dc, nextExternalId)) 
5777 06 Dec 19 nicklas 671       {
5777 06 Dec 19 nicklas 672         RESERVED_NAMES.releaseIfTransactionFails(dc, nextExternalId);
5777 06 Dec 19 nicklas 673         break;
5777 06 Dec 19 nicklas 674       }
5777 06 Dec 19 nicklas 675       nextIndex++;
5777 06 Dec 19 nicklas 676       numIterations++;
5777 06 Dec 19 nicklas 677       if (numIterations == 100)
5777 06 Dec 19 nicklas 678       {
5777 06 Dec 19 nicklas 679         throw new RuntimeException("Failed to generate an external ID for '" + prefix + 
5777 06 Dec 19 nicklas 680           "' after 100 tries. Last tested value was '" + nextExternalId + "'.");
5777 06 Dec 19 nicklas 681       }
5777 06 Dec 19 nicklas 682     }
5777 06 Dec 19 nicklas 683     
5777 06 Dec 19 nicklas 684     return nextExternalId;
5777 06 Dec 19 nicklas 685   }
5777 06 Dec 19 nicklas 686
5777 06 Dec 19 nicklas 687   
5777 06 Dec 19 nicklas 688   /**
4902 10 Jul 18 nicklas 689     @since 4.19
4897 10 Jul 18 nicklas 690     @see #ensureNonExistingItem(DbControl, Item, String, String)
4897 10 Jul 18 nicklas 691    */
4897 10 Jul 18 nicklas 692   public static String ensureNonExistingItem(DbControl dc, Subtype subtype, String name)
4897 10 Jul 18 nicklas 693   {
4897 10 Jul 18 nicklas 694     return ensureNonExistingItem(dc, subtype.getMainType(), subtype.getName(), name);
4897 10 Jul 18 nicklas 695   }
4897 10 Jul 18 nicklas 696   
4897 10 Jul 18 nicklas 697   /**
4897 10 Jul 18 nicklas 698     Check if an item with the given name already exists or not in the database. 
4897 10 Jul 18 nicklas 699     If the item exists, an exception is thrown. This method will simply check if
4897 10 Jul 18 nicklas 700     an item of the given 'itemType' and 'name' exists. No other filter is used.
4897 10 Jul 18 nicklas 701     The 'subtype' parameter is only used to give an error message if an item
4897 10 Jul 18 nicklas 702     is found.
4897 10 Jul 18 nicklas 703     @return The 'name'
4897 10 Jul 18 nicklas 704     @since 4.19
4897 10 Jul 18 nicklas 705   */
4897 10 Jul 18 nicklas 706   public static String ensureNonExistingItem(DbControl dc, Item itemType, String subtype, String name)
4897 10 Jul 18 nicklas 707   {
4897 10 Jul 18 nicklas 708     ItemQuery<?> query = (ItemQuery<?>)itemType.getQuery();
4897 10 Jul 18 nicklas 709     query.include(Include.ALL);
4897 10 Jul 18 nicklas 710     query.restrict(Restrictions.eq(Hql.property("name"), Expressions.string(name)));
4897 10 Jul 18 nicklas 711     query.setMaxResults(1);
4897 10 Jul 18 nicklas 712     
4897 10 Jul 18 nicklas 713     List<? extends BasicItem> items = query.list(dc);
4897 10 Jul 18 nicklas 714     if (items.size() > 0)
4897 10 Jul 18 nicklas 715     {
4897 10 Jul 18 nicklas 716       // The item already exists in the database
4897 10 Jul 18 nicklas 717       BasicItem item = items.get(0);
4897 10 Jul 18 nicklas 718       String msg = subtype + " '" + name + "' has already been registered";
4897 10 Jul 18 nicklas 719       if (item instanceof Ownable)
4897 10 Jul 18 nicklas 720       {
4897 10 Jul 18 nicklas 721         User owner = ((Ownable)item).getOwner();
4897 10 Jul 18 nicklas 722         msg += " by " + owner.getName();
4897 10 Jul 18 nicklas 723       }
5423 13 May 19 nicklas 724       if (item instanceof Registered)
5423 13 May 19 nicklas 725       {
5423 13 May 19 nicklas 726         Date registered = getCreationDateFromChangeHistory(dc, item);
5423 13 May 19 nicklas 727         if (registered != null)
5423 13 May 19 nicklas 728         {
5423 13 May 19 nicklas 729           long sinceMinutes = (System.currentTimeMillis() - registered.getTime()) / 3600;
5423 13 May 19 nicklas 730           if (sinceMinutes <= 1) 
5423 13 May 19 nicklas 731           {
5423 13 May 19 nicklas 732             msg += " 1 minute ago.";
5423 13 May 19 nicklas 733           }
5423 13 May 19 nicklas 734           else if (sinceMinutes <= 10)
5423 13 May 19 nicklas 735           {
5423 13 May 19 nicklas 736             msg += " " + sinceMinutes + " minutes ago.";
5423 13 May 19 nicklas 737           }
5423 13 May 19 nicklas 738           else if (sinceMinutes <= 600)
5423 13 May 19 nicklas 739           {
5423 13 May 19 nicklas 740             msg += " at " + Reggie.CONVERTER_TIME_TO_STRING_WITH_SEPARATOR.convert(registered) + " today.";
5423 13 May 19 nicklas 741           }
5423 13 May 19 nicklas 742           else
5423 13 May 19 nicklas 743           {
5423 13 May 19 nicklas 744             msg += " " + Reggie.CONVERTER_DATE_TO_STRING_WITH_SEPARATOR.convert(registered) + ".";
5423 13 May 19 nicklas 745           }
5423 13 May 19 nicklas 746         }
5423 13 May 19 nicklas 747       }
4897 10 Jul 18 nicklas 748       throw new RuntimeException(msg);
4897 10 Jul 18 nicklas 749     }
4897 10 Jul 18 nicklas 750
4897 10 Jul 18 nicklas 751     // Check reserved item
4897 10 Jul 18 nicklas 752     if (!RESERVED_NAMES.reserve(dc, name))
4897 10 Jul 18 nicklas 753     {
4897 10 Jul 18 nicklas 754       throw new RuntimeException(subtype + " '" + name + "' is already reserved by another process");
4897 10 Jul 18 nicklas 755     }
4897 10 Jul 18 nicklas 756     RESERVED_NAMES.releaseAtEndOfTransaction(dc, name);
4897 10 Jul 18 nicklas 757     return name;
4897 10 Jul 18 nicklas 758   }
4897 10 Jul 18 nicklas 759
5423 13 May 19 nicklas 760   public static Date getCreationDateFromChangeHistory(DbControl dc, BasicItem item)
5423 13 May 19 nicklas 761   {
5423 13 May 19 nicklas 762     ItemQuery<ChangeHistory> query = ChangeHistory.getHistoryOf(item);
5423 13 May 19 nicklas 763     query.restrict(Restrictions.eq(Hql.property("changeType"), Expressions.integer(ChangeType.CREATE.getValue())));
5423 13 May 19 nicklas 764     query.order(Orders.asc(Hql.property("hst", "time")));
5423 13 May 19 nicklas 765     query.setMaxResults(1);
5423 13 May 19 nicklas 766     List<ChangeHistory> list = query.list(dc);
5423 13 May 19 nicklas 767     return list.size() == 0 ? null : list.get(0).getTime();
5423 13 May 19 nicklas 768   }
4897 10 Jul 18 nicklas 769   
4897 10 Jul 18 nicklas 770   /**
3764 22 Feb 16 nicklas 771     Sorts a list of items by their index order value. Items with
3764 22 Feb 16 nicklas 772     a lower value are sorted before items with a higher value.
3764 22 Feb 16 nicklas 773     
3764 22 Feb 16 nicklas 774     @see ReggieItem#setIndexOrder(int)
3764 22 Feb 16 nicklas 775     @since 4.2
3764 22 Feb 16 nicklas 776   */
3764 22 Feb 16 nicklas 777   public static class IndexOrderComparator
3764 22 Feb 16 nicklas 778     implements Comparator<ReggieItem<?>>
3764 22 Feb 16 nicklas 779   {
3764 22 Feb 16 nicklas 780     
3764 22 Feb 16 nicklas 781     public IndexOrderComparator() 
3764 22 Feb 16 nicklas 782     {}
3764 22 Feb 16 nicklas 783
3764 22 Feb 16 nicklas 784     @Override
3764 22 Feb 16 nicklas 785     public int compare(ReggieItem<?> item1, ReggieItem<?> item2) 
3764 22 Feb 16 nicklas 786     {
3764 22 Feb 16 nicklas 787       return item1.indexOrder - item2.indexOrder;
3764 22 Feb 16 nicklas 788     }
3764 22 Feb 16 nicklas 789   }
4985 28 Sep 18 nicklas 790   
4985 28 Sep 18 nicklas 791   /**
4985 28 Sep 18 nicklas 792     Sorts a list of items by name.
4985 28 Sep 18 nicklas 793     @since 4.20
4985 28 Sep 18 nicklas 794   */
4985 28 Sep 18 nicklas 795   public static class NameComparator
4985 28 Sep 18 nicklas 796     implements Comparator<ReggieItem<?>>
4985 28 Sep 18 nicklas 797   {
4985 28 Sep 18 nicklas 798     public NameComparator()
4985 28 Sep 18 nicklas 799     {}
4985 28 Sep 18 nicklas 800     
4985 28 Sep 18 nicklas 801     @Override
4985 28 Sep 18 nicklas 802     public int compare(ReggieItem<?> item1, ReggieItem<?> item2) 
4985 28 Sep 18 nicklas 803     {
4985 28 Sep 18 nicklas 804       return item1.getName().compareTo(item2.getName());
4985 28 Sep 18 nicklas 805     }
4985 28 Sep 18 nicklas 806     
4985 28 Sep 18 nicklas 807   }
4985 28 Sep 18 nicklas 808   
1326 29 Mar 11 nicklas 809 }