/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.isis.core.metamodel.services.appfeat;
import java.io.Serializable;
import java.nio.charset.Charset;
import java.util.Collections;
import java.util.Comparator;
import java.util.Iterator;
import java.util.List;
import javax.annotation.Nullable;
import com.google.common.base.Function;
import com.google.common.base.Joiner;
import com.google.common.base.Objects;
import com.google.common.base.Predicate;
import com.google.common.base.Splitter;
import com.google.common.collect.Lists;
import com.google.common.collect.Ordering;
import com.google.common.io.BaseEncoding;
import org.apache.isis.applib.annotation.Programmatic;
import org.apache.isis.applib.annotation.Value;
import org.apache.isis.applib.services.appfeat.ApplicationMemberType;
import org.apache.isis.applib.util.TitleBuffer;
/**
* Value type representing a package, class or member.
*
* <p>
* This value is {@link Comparable}, the implementation of which considers {@link #getType() (feature) type},
* {@link #getPackageName() package name}, {@link #getClassName() class name} and {@link #getMemberName() member name}.
* </p>
*/
@Value
public class ApplicationFeatureId implements Comparable<ApplicationFeatureId>, Serializable {
// //////////////////////////////////////
//region > factory methods
public static ApplicationFeatureId newFeature(final ApplicationFeatureType featureType, final String fullyQualifiedName) {
switch (featureType) {
case PACKAGE:
return newPackage(fullyQualifiedName);
case CLASS:
return newClass(fullyQualifiedName);
case MEMBER:
return newMember(fullyQualifiedName);
}
throw new IllegalArgumentException("Unknown feature type " + featureType);
}
public static ApplicationFeatureId newFeature(
final String packageFqn, final String className, final String memberName) {
if(className == null) {
return newPackage(packageFqn);
}
final String classFqn = packageFqn + "." + className;
if(memberName == null) {
return newClass(classFqn);
}
return newMember(classFqn, memberName);
}
public static ApplicationFeatureId newPackage(final String packageFqn) {
final ApplicationFeatureId featureId = new ApplicationFeatureId(ApplicationFeatureType.PACKAGE);
featureId.setPackageName(packageFqn);
return featureId;
}
public static ApplicationFeatureId newClass(final String classFqn) {
final ApplicationFeatureId featureId = new ApplicationFeatureId(ApplicationFeatureType.CLASS);
featureId.type.init(featureId, classFqn);
return featureId;
}
public static ApplicationFeatureId newMember(final String classFqn, final String memberName) {
final ApplicationFeatureId featureId = new ApplicationFeatureId(ApplicationFeatureType.MEMBER);
ApplicationFeatureType.CLASS.init(featureId, classFqn);
featureId.type = ApplicationFeatureType.MEMBER;
featureId.setMemberName(memberName);
return featureId;
}
public static ApplicationFeatureId newMember(final String fullyQualifiedName) {
final ApplicationFeatureId featureId = new ApplicationFeatureId(ApplicationFeatureType.MEMBER);
featureId.type.init(featureId, fullyQualifiedName);
return featureId;
}
/**
* Round-trip with {@link #asString()}
*/
public static ApplicationFeatureId parse(final String asString) {
return new ApplicationFeatureId(asString);
}
/**
* Round-trip with {@link #asEncodedString()}
*/
public static ApplicationFeatureId parseEncoded(final String encodedString) {
return new ApplicationFeatureId(base64UrlDecode(encodedString));
}
//endregion
// //////////////////////////////////////
//region > constructor
private ApplicationFeatureId(final String asString) {
final Iterator<String> iterator = Splitter.on(":").split(asString).iterator();
final ApplicationFeatureType type = ApplicationFeatureType.valueOf(iterator.next());
type.init(this, iterator.next());
}
/**
* Must be called by {@link ApplicationFeatureType#init(ApplicationFeatureId, String)} immediately afterwards
* to fully initialize.
*/
ApplicationFeatureId(final ApplicationFeatureType type) {
this.type = type;
}
public ApplicationFeatureId(final ApplicationFeatureType type, final String fullyQualifiedName) {
type.init(this, fullyQualifiedName);
}
//endregion
// //////////////////////////////////////
//region > identification
/**
* having a title() method (rather than using @Title annotation) is necessary as a workaround to be able to use
* wrapperFactory#unwrap(...) method, which is otherwise broken in Isis 1.6.0
*/
public String title() {
final TitleBuffer buf = new TitleBuffer();
buf.append(getFullyQualifiedName());
return buf.toString();
}
//endregion
// //////////////////////////////////////
//region > fullyQualifiedName (property)
@Programmatic
public String getFullyQualifiedName() {
final StringBuilder buf = new StringBuilder();
buf.append(getPackageName());
if(getClassName() != null) {
buf.append(".").append(getClassName());
}
if(getMemberName() != null) {
buf.append("#").append(getMemberName());
}
return buf.toString();
}
//endregion
// //////////////////////////////////////
//region > type (property)
ApplicationFeatureType type;
public ApplicationFeatureType getType() {
return type;
}
//endregion
// //////////////////////////////////////
//region > packageName (property)
private String packageName;
@Programmatic
public String getPackageName() {
return packageName;
}
void setPackageName(final String packageName) {
this.packageName = packageName;
}
//endregion
// //////////////////////////////////////
//region > className (property, optional)
private String className;
@Programmatic
public String getClassName() {
return className;
}
void setClassName(final String className) {
this.className = className;
}
//endregion
// //////////////////////////////////////
//region > memberName (property, optional)
private String memberName;
@Programmatic
public String getMemberName() {
return memberName;
}
void setMemberName(final String memberName) {
this.memberName = memberName;
}
//endregion
// //////////////////////////////////////
//region > Package or Class: getParentPackageId
/**
* The {@link ApplicationFeatureId id} of the parent package of this
* class or package.
*/
@Programmatic
public ApplicationFeatureId getParentPackageId() {
ApplicationFeatureType.ensurePackageOrClass(this);
if(type == ApplicationFeatureType.CLASS) {
return ApplicationFeatureId.newPackage(getPackageName());
} else {
final String packageName = getPackageName(); // eg aaa.bbb.ccc
if(!packageName.contains(".")) {
return null; // parent is root
}
final Iterable<String> split = Splitter.on(".").split(packageName);
final List<String> parts = Lists.newArrayList(split); // eg [aaa,bbb,ccc]
parts.remove(parts.size()-1); // remove last, eg [aaa,bbb]
final String parentPackageName = Joiner.on(".").join(parts); // eg aaa.bbb
return newPackage(parentPackageName);
}
}
//endregion
// //////////////////////////////////////
//region > Member: getParentClassId
/**
* The {@link ApplicationFeatureId id} of the member's class.
*/
public ApplicationFeatureId getParentClassId() {
ApplicationFeatureType.ensureMember(this);
final String classFqn = this.getPackageName() + "." + getClassName();
return newClass(classFqn);
}
//endregion
// //////////////////////////////////////
//region > asString, asEncodedString
@Programmatic
public String asString() {
return Joiner.on(":").join(type, getFullyQualifiedName());
}
@Programmatic
public String asEncodedString() {
return base64UrlEncode(asString());
}
private static String base64UrlDecode(final String str) {
final byte[] bytes = BaseEncoding.base64Url().decode(str);
return new String(bytes, Charset.forName("UTF-8"));
}
private static String base64UrlEncode(final String str) {
final byte[] bytes = str.getBytes(Charset.forName("UTF-8"));
return BaseEncoding.base64Url().encode(bytes);
}
//endregion
// //////////////////////////////////////
//region > Functions
public static class Functions {
private Functions(){}
public static final Function<ApplicationFeatureId, String> GET_CLASS_NAME = new Function<ApplicationFeatureId, String>() {
@Override
public String apply(final ApplicationFeatureId input) {
return input.getClassName();
}
};
public static final Function<ApplicationFeatureId, String> GET_MEMBER_NAME = new Function<ApplicationFeatureId, String>() {
@Override
public String apply(final ApplicationFeatureId input) {
return input.getMemberName();
}
};
}
//endregion
// //////////////////////////////////////
//region > Predicates
public static class Predicates {
private Predicates(){}
public static Predicate<ApplicationFeatureId> isClassContaining(
final ApplicationMemberType memberType, final ApplicationFeatureRepositoryDefault applicationFeatures) {
return new Predicate<ApplicationFeatureId>() {
@Override
public boolean apply(final ApplicationFeatureId input) {
if(input.getType() != ApplicationFeatureType.CLASS) {
return false;
}
final ApplicationFeature feature = applicationFeatures.findFeature(input);
if(feature == null) {
return false;
}
return memberType == null || !feature.membersOf(memberType).isEmpty();
}
};
}
public static Predicate<ApplicationFeatureId> isClassRecursivelyWithin(final ApplicationFeatureId packageId) {
return new Predicate<ApplicationFeatureId>() {
@Override
public boolean apply(final ApplicationFeatureId input) {
return input.getParentIds().contains(packageId);
}
};
}
}
//endregion
// //////////////////////////////////////
//region > Comparators
public static final class Comparators {
private Comparators(){}
public static Comparator<ApplicationFeatureId> natural() {
return new ApplicationFeatureIdComparator();
}
static class ApplicationFeatureIdComparator implements Comparator<ApplicationFeatureId>, Serializable {
@Override
public int compare(final ApplicationFeatureId o1, final ApplicationFeatureId o2) {
return o1.compareTo(o2);
}
}
}
//endregion
// //////////////////////////////////////
//region > pathIds, parentIds
@Programmatic
public List<ApplicationFeatureId> getPathIds() {
return pathIds(this);
}
@Programmatic
public List<ApplicationFeatureId> getParentIds() {
return pathIds(getParentId());
}
private ApplicationFeatureId getParentId() {
return type == ApplicationFeatureType.MEMBER? getParentClassId(): getParentPackageId();
}
private static List<ApplicationFeatureId> pathIds(final ApplicationFeatureId id) {
final List<ApplicationFeatureId> featureIds = Lists.newArrayList();
return Collections.unmodifiableList(appendParents(id, featureIds));
}
private static List<ApplicationFeatureId> appendParents(final ApplicationFeatureId featureId, final List<ApplicationFeatureId> parentIds) {
if(featureId != null) {
parentIds.add(featureId);
appendParents(featureId.getParentId(), parentIds);
}
return parentIds;
}
//endregion
// //////////////////////////////////////
//region > equals, hashCode, compareTo, toString
private final static Ordering<ApplicationFeatureId> byType = Ordering.natural()
.nullsFirst().onResultOf(new Function<ApplicationFeatureId, ApplicationFeatureType>() {
@Nullable @Override public ApplicationFeatureType apply(@Nullable final ApplicationFeatureId input) {
return input != null ? input.getType() : null;
}
});
private final static Ordering<ApplicationFeatureId> byPackageName = Ordering.natural()
.nullsFirst().onResultOf(new Function<ApplicationFeatureId, String>() {
@Nullable @Override public String apply(@Nullable final ApplicationFeatureId input) {
return input != null ? input.getPackageName() : null;
}
});
private final static Ordering<ApplicationFeatureId> byClassName = Ordering.natural()
.nullsFirst().onResultOf(new Function<ApplicationFeatureId, String>() {
@Nullable @Override public String apply(@Nullable final ApplicationFeatureId input) {
return input != null ? input.getClassName(): null;
}
});
private final static Ordering<ApplicationFeatureId> byMemberName = Ordering.natural()
.nullsFirst().onResultOf(new Function<ApplicationFeatureId, String>() {
@Nullable @Override public String apply(@Nullable final ApplicationFeatureId input) {
return input != null ? input.getMemberName() : null;
}
});
private final static Ordering<ApplicationFeatureId> applicationFeatureIdOrdering =
byType
.compound(byPackageName)
.compound(byClassName)
.compound(byMemberName)
.nullsFirst();
@Override
public int compareTo(final ApplicationFeatureId other) {
// https://issues.apache.org/jira/browse/ISIS-1590
// not using our ObjectContracts helper for efficiency.
return applicationFeatureIdOrdering.compare(this, other);
}
@Override
public boolean equals(final Object o) {
// https://issues.apache.org/jira/browse/ISIS-1590
// not using our ObjectContracts helper for efficiency.
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
final ApplicationFeatureId that = (ApplicationFeatureId) o;
if (className != null ? !className.equals(that.className) : that.className != null) return false;
if (memberName != null ? !memberName.equals(that.memberName) : that.memberName != null) return false;
if (packageName != null ? !packageName.equals(that.packageName) : that.packageName != null) return false;
return type == that.type;
}
@Override
public int hashCode() {
// https://issues.apache.org/jira/browse/ISIS-1590
// not using our ObjectContracts helper for efficiency.
int result = type != null ? type.hashCode() : 0;
result = 31 * result + (packageName != null ? packageName.hashCode() : 0);
result = 31 * result + (className != null ? className.hashCode() : 0);
result = 31 * result + (memberName != null ? memberName.hashCode() : 0);
return result;
}
@Override
public String toString() {
// https://issues.apache.org/jira/browse/ISIS-1590
// not using our ObjectContracts helper for efficiency.
final Objects.ToStringHelper stringHelper = Objects.toStringHelper(this);
switch (type) {
case PACKAGE:
stringHelper.add("type", getType());
stringHelper.add("packageName", getPackageName());
return stringHelper.toString();
case CLASS:
stringHelper.add("type", getType());
stringHelper.add("packageName", getPackageName());
stringHelper.add("className", getClassName());
return stringHelper.toString();
case MEMBER:
stringHelper.add("type", getType());
stringHelper.add("packageName", getPackageName());
stringHelper.add("memberName", getMemberName());
return stringHelper.toString();
}
throw new IllegalStateException("Unknown feature type " + type);
}
//endregion
}