package org.edx.mobile.model.course; import android.text.TextUtils; import org.edx.mobile.R; import org.edx.mobile.base.MainApplication; import org.edx.mobile.logger.Logger; import org.edx.mobile.model.Filter; import org.edx.mobile.model.api.IPathNode; import org.edx.mobile.module.storage.IStorage; import java.util.ArrayList; import java.util.EnumSet; import java.util.Iterator; import java.util.List; import java.util.Locale; /** * Default implementation of IBlock */ public class CourseComponent implements IBlock, IPathNode { protected final static Logger logger = new Logger(CourseComponent.class.getName()); private String id; private BlockType type; private String name; private boolean graded; private boolean multiDevice; private String blockUrl; private String webUrl; private BlockCount blockCount; protected CourseComponent parent; protected CourseComponent root; protected List<CourseComponent> children = new ArrayList<>(); private String courseId; private String format; public CourseComponent(){} /** * * @param blockModel * @param parent is null if and only if this is the root */ public CourseComponent(BlockModel blockModel, CourseComponent parent){ this.id = blockModel.id; this.type = blockModel.type; this.name = blockModel.displayName; this.graded = blockModel.graded; this.blockUrl = blockModel.studentViewUrl; this.webUrl = blockModel.lmsWebUrl; this.multiDevice = blockModel.studentViewMultiDevice; this.format = blockModel.format; this.blockCount = blockModel.blockCounts == null ? new BlockCount() : blockModel.blockCounts; this.parent = parent; if ( parent == null){ this.root = this; } else { parent.getChildren().add(this); //we cache the root to improve the performance this.root = (CourseComponent)parent.getRoot(); } } @Override public String getId() { return id; } @Override public void setId(String id) { this.id = id; } @Override public BlockType getType() { return type; } @Override public void setType(BlockType type) { this.type = type; } @Override public String getDisplayName() { if (TextUtils.isEmpty(name)) { return MainApplication.instance().getString(R.string.untitled_block); } return name; } @Override public void setDisplayName(String name) { this.name = name; } @Override public boolean isGraded() { return graded; } @Override public void setGraded(boolean graded) { this.graded = graded; } @Override public String getBlockUrl() { return blockUrl; } @Override public void setBlockUrl(String url) { this.blockUrl = url; } @Override public String getWebUrl() { return webUrl; } @Override public void setWebUrl(String webUrl) { this.webUrl = webUrl; } @Override public BlockCount getBlockCount() { return blockCount; } @Override public void setBlockCount(BlockCount count) { this.blockCount = blockCount; } @Override public CourseComponent getParent() { return parent; } @Override public List<IBlock> getChildren() { return (List) children; } @Override public CourseComponent getRoot(){ return root; } public boolean isMultiDevice() { return multiDevice; } public void setMultiDevice(boolean multiDevice) { this.multiDevice = multiDevice; } public boolean isContainer(){ return type != null ? type.isContainer() : (children != null && children.size() > 0); } /** * get direct children who have child. it is not based on the block type, but on * the real tree structure. * @return */ public List<CourseComponent> getChildContainers(){ List<CourseComponent> childContainers = new ArrayList<>(); if ( children != null ){ for(CourseComponent c : children){ if ( c.isContainer() ) childContainers.add(c); } } return childContainers; } /** * get direct children who is leaf. it is not based on the block type, but on * the real tree structure. * @return */ public List<CourseComponent> getChildLeafs(){ List<CourseComponent> childLeafs = new ArrayList<>(); if ( children != null ){ for(CourseComponent c : children){ if ( !c.isContainer() ) childLeafs.add(c); } } return childLeafs; } /** * recursively find the first node by matcher. return null if get nothing. */ public CourseComponent find(Filter<CourseComponent> matcher){ if ( matcher.apply(this ) ) return this; if ( !isContainer() ) return null; CourseComponent found = null; for(CourseComponent c : children){ found = c.find(matcher); if ( found != null ) return found; } return null; } /** * return all videos blocks under this node */ public List<VideoBlockModel> getVideos(){ List<CourseComponent> videos = new ArrayList<>(); fetchAllLeafComponents(videos, EnumSet.of(BlockType.VIDEO)); // Confirm that these are actually VideoBlockModel instances. // This is necessary because if for some reason the data is null, // then the block is represented as an HtmlBlockModel, even if // the type is video. This should not actually happen in practice // though; this is just a safeguard to handle that unlikely case. for (Iterator<CourseComponent> videosIterator = videos.iterator(); videosIterator.hasNext();) { CourseComponent videoComponent = videosIterator.next(); if (!(videoComponent instanceof VideoBlockModel)) { videosIterator.remove(); } } return (List<VideoBlockModel>)(List)videos; } /** * @return count of videos that have encoded files available * and {@link VideoData#onlyOnWeb} set to <code>false</code> */ public int getDownloadableVideosCount() { int downloadableCount = 0; List<VideoBlockModel> videos = getVideos(); for (VideoBlockModel video : videos) { if (video.getData().encodedVideos.getPreferredVideoInfo() != null && !video.getData().onlyOnWeb) { downloadableCount++; } } return downloadableCount; } /** * used for navigation. * @return <code>true</code> if it is the last child of direct parent. or it does not has direct parent * <code>false</code> if it is not */ public boolean isLastChild(){ if ( parent == null ) return true; List<IBlock> sibling = parent.getChildren(); if ( sibling == null ) { return false; //it wont happen. TODO - should we log here? } return sibling.indexOf(this) == sibling.size() -1; } /** * we get all the leaves below this node. if this node itself is leaf, * just add it to list * @param leaves */ public void fetchAllLeafComponents(List<CourseComponent> leaves, EnumSet<BlockType> types){ if ( !isContainer() && types.contains(type)){ leaves.add(this); } else { for( CourseComponent comp : children ){ comp.fetchAllLeafComponents(leaves, types); } } } /** * get the ancestor based on level, level = 0, means itself. * if level is out of the boundary, just return the toppest one * @param level * @return it will never return null. */ public CourseComponent getAncestor(int level){ if ( parent == null || level == 0 ) return this; IBlock root = parent; while ( level != 0 && root.getParent() != null ){ root = root.getParent(); level--; } return (CourseComponent)root; } /** * get ancestor with give blockType, starting from itself */ public CourseComponent getAncestor(EnumSet<BlockType> types){ if( types.contains(type) ) return this; IBlock ancestor = parent; if ( ancestor == null ) return null; do{ if ( types.contains( ancestor.getType() ) ) return (CourseComponent) ancestor; }while ((ancestor = ancestor.getParent()) != null ); return null; } @Override public boolean equals(Object obj){ if ( obj == null || !(obj instanceof CourseComponent) ) return false; CourseComponent other = (CourseComponent)obj; return this.id.equals(other.id); } @Override public int hashCode(){ return this.id.hashCode(); } //// implement IPathNode interface, for backward compatibility only @Override public boolean isChapter() { return getType() == BlockType.CHAPTER; } @Override public boolean isSequential() { return getType() == BlockType.SEQUENTIAL; } @Override public boolean isVertical() { return getType() == BlockType.VERTICAL; } @Override public String getCategory() { return getType().name().toLowerCase(Locale.ENGLISH); } /** * Not meant to be user facing. See {@link #getDisplayName()} */ public String getInternalName() { return name; } @Override public String getFormat() { return format; } @Override public void setFormat(String format) { this.format = format; } @Override public String getCourseId(){ if( courseId == null || courseId.length() == 0 ){ //root should always has a course id, add the check to avoid loop if ( root == this ){ logger.debug( "root does not has a course id set!!! for " + id); return ""; } return root.getCourseId(); } else { return courseId; } } @Override public void setCourseId(String courseId){ this.courseId = courseId; } /** * calculate and construct a Path object */ public BlockPath getPath(){ BlockPath path = new BlockPath(); path.addPathNodeToPathFront(this); CourseComponent nodeAbove = parent; while ( nodeAbove != null ){ path.addPathNodeToPathFront(nodeAbove); nodeAbove = nodeAbove.getParent(); } return path; } public static CourseComponent getCommonAncestor(CourseComponent node1, CourseComponent node2){ List<CourseComponent> path1 = node1.getPath().getPath(); List<CourseComponent> path2 = node2.getPath().getPath(); if ( path1.isEmpty() || path2.isEmpty() ) return null; for(int i = path1.size() -1; i >=0; i --){ CourseComponent comp1 = path1.get(i); for(int j = path2.size() -1; j >=0; j --){ CourseComponent comp2 = path2.get(j); if (comp1.equals(comp2)) return comp1; } } return null; } }